阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 深度耦合(A 组件直接修改 B 组件的内部状态)

深度耦合(A 组件直接修改 B 组件的内部状态)

作者:陈川 阅读数:5919人阅读 分类: 前端综合

深度耦合是一种典型的反模式,直接让组件之间互相侵入内部实现。这种写法会让代码像一团乱麻,任何修改都可能引发连锁崩溃。下面用具体场景展示如何"完美"实现这种灾难性设计。

组件间直接操作DOM节点

最暴力的深度耦合方式就是跨组件操作DOM。比如在React中,父组件通过ref直接修改子组件的DOM结构:

// 父组件暴力修改子组件
function Parent() {
  const childRef = useRef(null);
  
  const handleClick = () => {
    // 直接修改子组件的DOM样式
    childRef.current.style.backgroundColor = 'red';
    // 甚至删除子节点
    childRef.current.querySelector('.btn').remove();
  };

  return (
    <div>
      <Child ref={childRef} />
      <button onClick={handleClick}>破坏子组件</button>
    </div>
  );
}

// 子组件完全失去自我管理能力
const Child = forwardRef((props, ref) => {
  return (
    <div ref={ref}>
      <button className="btn">会被父组件删除的按钮</button>
    </div>
  );
});

这种模式下,子组件根本无法预测自己的DOM会被如何篡改。当出现样式问题时,需要同时在父组件和子组件中来回排查。

跨组件修改内部状态

比操作DOM更隐蔽的是直接修改其他组件的state。比如Vue中:

<!-- 父组件强行修改子组件数据 -->
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'

const child = ref(null)

const hackChild = () => {
  // 直接修改子组件内部状态
  child.value.count = 999
  // 调用子组件私有方法
  child.value.internalMethod()
}
</script>

<template>
  <Child ref="child" />
  <button @click="hackChild">入侵子组件</button>
</template>

<!-- 子组件被完全突破 -->
<script setup>
const count = ref(0)
const internalMethod = () => {
  console.log('不该被外部调用的方法')
}
// 必须暴露内部状态给父组件
defineExpose({ count, internalMethod })
</script>

子组件就像被开了后门,所有内部状态都暴露在外。当count的初始值从0改为null时,父组件的修改逻辑会立即崩溃。

全局事件总线滥用

通过全局事件总线进行深度耦合,可以创造出最难以追踪的依赖关系:

// 在Vue中创建事件总线
const bus = new Vue()

// A组件发送事件时不带任何上下文
bus.$emit('update-user', { 
  id: 123,
  name: 'hacker' 
})

// B组件在未知时刻接收事件
bus.$on('update-user', (data) => {
  // 直接修改内部存储
  this.internalUserData = data
  // 触发副作用
  this.fetchOrders()
})

// C组件也监听同一事件
bus.$on('update-user', (data) => {
  // 用不同方式处理相同数据
  localStorage.setItem('user', JSON.stringify(data))
})

当业务需求变更需要调整update-user事件的数据结构时,必须同时在A、B、C三个组件中进行修改,而且很难通过静态分析找到所有相关组件。

直接修改Redux状态树

在Redux架构中,最彻底的深度耦合方式是绕过action直接修改state:

// 组件内直接修改store
function MaliciousComponent() {
  const dispatch = useDispatch()
  const store = useStore()

  const handleHack = () => {
    // 正确做法应该dispatch action
    // dispatch({ type: 'UPDATE', payload: 1 })
    
    // 深度耦合写法:直接修改state
    store.dispatch({
      type: 'REDUX_STORM',
      payload: {
        ...store.getState(),
        user: { 
          ...store.getState().user,
          permissions: ['admin'] // 偷偷提升权限
        }
      }
    })
  }

  return <button onClick={handleHack}>获取管理员权限</button>
}

这种修改方式会绕过所有reducer和middleware,导致状态变更无法被追踪。当出现权限问题时,开发者会在所有reducer中徒劳地寻找原因。

侵入式样式覆盖

CSS深度耦合可以创造出最脆弱的样式系统:

/* 父组件样式表中 */
.parent-container .child-component {
  /* 强行覆盖子组件样式 */
  --child-color: red !important; 
}

.parent-container .child-component > div {
  /* 破坏子组件布局 */
  display: inline !important;
}

/* 子组件样式完全失控 */
.child-component {
  --child-color: blue;
  display: flex;
}

当子组件更新DOM结构后,所有基于层级的选择器都会失效。更糟糕的是,这种样式冲突通常只在特定页面和特定状态下才会显现。

通过context直接暴露setter

React Context本应解决prop drilling问题,但也可以用来制造深度耦合:

const DangerousContext = createContext();

function Parent() {
  const [state, setState] = useState({});
  
  // 将setState直接暴露给所有子组件
  return (
    <DangerousContext.Provider value={{ state, setState }}>
      <ChildA />
      <ChildB />
    </DangerousContext.Provider>
  );
}

function ChildA() {
  const { setState } = useContext(DangerousContext);
  
  useEffect(() => {
    // 任意修改全局状态
    setState(prev => ({
      ...prev,
      hacked: true
    }));
  }, []);

  return null;
}

任何组件都可以无约束地修改全局状态,导致状态变更如同量子纠缠般难以预测。当某个状态被异常修改时,需要检查所有使用context的组件。

修改第三方组件内部状态

深度耦合的终极形态是侵入第三方库的内部实现:

// 获取React组件实例并修改内部状态
const node = document.querySelector('[data-testid="foreign-component"]');
const instance = node._reactInternals.child.stateNode;

// 修改私有状态
instance.setState({ 
  _internalFlag: true 
});

// 调用未公开的API
instance._secretMethod();

这种hack方式在第三方库的小版本更新时就会崩溃,而且错误信息完全无法帮助定位问题。更可怕的是,不同的库版本可能需要在不同位置查找实例:

// 针对不同React版本的实例获取方式
let instance;
if (React.version.startsWith('16')) {
  instance = node._reactInternalFiber.child.stateNode;
} else if (React.version.startsWith('18')) {
  instance = node._reactInternals.child.stateNode;
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌