react为什么不能在组件里面声明组件

1,388 阅读3分钟

前两天线上的推荐卡片出现了一个神奇的bug,就是一旦你加载下一列,所有的推荐卡片都会闪烁。线上情况类似如下:

12.gif

大胆猜测,闪烁是因为子组件卸载了,所以在子组件加入如下代码测试

  useEffect(() => {
    return () => {
      console.log("unmount");
    }
  }, []);

image.png

果然,发生了卸载,开始排查代码,发现代码中有类似如下代码:

const Test = function () {
  const [update, setUpdate] = useState(false);

  useEffect(() => {
    const timeSign = setInterval(() => {
      setUpdate(oldState => !oldState)
    }, 600);

    return () => {
      clearInterval(timeSign);
    }
  }, []);
  
  // 子组件
  const ChildrenComponent = () => {
    useEffect(() => {
      return () => {
        console.log("unmount");
      }
    }, []);

    return <img className="product-img" src="https://img.mrvcdn.com/g/fb/kf/Eee800a9c74584f758b617c8c446a089b2.jpg_2200x2200q75.avif" />
  }

  return <ChildrenComponent />
};

居然组件中声明了组件,导致每次父组件更新,子组件都会卸载并且重新挂载。当然这为什么会造成子组件卸载,这就要结合reactdiff算法来说了,reactdiff算法过程如下:

  1. 相同类型的节点:

    • 如果两个节点类型相同(都是元素节点或都是文本节点),则递归比较它们的属性和子节点。
    • 对于元素节点,会检查并更新属性(props),然后继续比较子节点。
    • 对于文本节点,直接更新文本内容。
  2. 不同类型的节点:

    • 如果节点类型不同,则 React 会认为它们是完全不相同的节点,直接替换整个子树。旧节点会被移除,新节点会重新创建并插入。
  3. 区分 key 属性:

    • React 利用 key 属性来高效处理子节点的重排和复用。
    • 如果子节点有 key 属性,React 会基于 key 进行对比和复用。这样做可以避免不必要的节点删除和创建。
    • React 会为每个子节点生成一个映射表,以 key 为索引来快速查找匹配的节点。
  4. 递归处理子节点(树的遍历):

    • 基于上述步骤,React 会递归地处理每个节点及其子节点,进行深度优先遍历(DFS)的对比和更新。

我们直接看第二条,当type不同时,react会移除并且重建dom,对应到我们项目中就是,每次父组件更新时,父组件内部声明的子组件都会重新被声明,导致type不同,也就是为什么子组件会一直卸载的原因。所以一般来说不要在组件当中声明组件,会导致奇怪的问题。

当然,如果你真的想在组件中声明并使用组件,那就需要使用useCallback,并且依赖设置为[],让返回的函数保持为同一个:

可以发现已经不闪烁了。但是此时子组件不能引用父组件的变量,否则会出现一些更新问题,假如我们在子组件里引用了父组件的变量count,并尝试更新:

可以发现count一直为0,这主要是函数实例和作用域的问题,这里就不多说了,有兴趣可以看一下这篇文章#别再说react闭包问题是react的错了