How to deal with the second parameter of useEffect from the source code

useEffect is a commonly used hook, it supports two parameters, the first parameter is the callback function, and the second parameter is the dependency.

When the second parameter is null or undefined, the callback function will be executed every time it is rendered, and when the parameter is an array, it will be executed only when the dependencies are changed.

These are all familiar to us, but how is it achieved? Let's find the answer from the source code.

The second parameter of useEffect

Let's first try the effect when the second parameter is passed in undefined, empty array, and dependent array.

Prepare a piece of code like this:

import { useEffect, useRef, useState } from 'react';

function Dong() {
    const ref = useRef(1);

    const [,setState] = useState();

    useEffect(() => {
        console.log(111);
    });

    useEffect(() => {
        console.log(222);
    }, []);

    useEffect(() => {
        console.log(333);
    }, [ref.current]);

    useEffect(() => {
        setInterval(() => {
            setState([]);
        }, 1000);
    
        setTimeout(() => {
            ref.current = 2;
        }, 3000);
    }, []);

    return <div>dong</div>;
}
复制代码

We wrote three useEffects, the second parameter is undefined, [], there is a dependent array, and the callback function prints 111, 222, and 333 respectively.

Then useState declares a state and uses setInterval to modify it regularly, so that render can be triggered continuously.

An object is declared with useRef. Its characteristic is that the same object is returned every time the render is performed. We use setTimeout to modify its value after 2s.

The result of execution should be easy to think of:

111 is printed every time because the second parameter is undefined.

222 is only printed once because the second parameter is [].

333 is printed twice, because the second parameter has a dependency, which changes once in 2s.

These are all familiar to us, but why is it like this?

Let's look at the source code:

useEffect related source code

The principle of react hooks was written in the previous article, and we will go through it again:

jsx is compiled to generate a render function, and the execution returns to vdom. However, in order to improve performance, React 16 introduces a fiber architecture, which will first convert vdom to fiber, and then update it to dom.

The process of converting vdom to fiber is called reconcile, and the process of updating to dom is called commit. The process of reconcile is interruptible and requires a schedule.

hooks 也是基于 fiber 来实现的,它在 fiber 节点上维护了一个链表(memorizedState 属性),用来保存数据,每个 hook 都是从对应的链表元素上存取各自的数据。

比如上面那个组件的 6 个 hook 就对应着 fiber 节点上 memorizedState 链表的 6 个元素:

每个 hook 都是在对应的链表元素上存取数据的。

这个链表有个建立的过程,叫做 mount,后面只需要 update,所以每个 hook 的实现都会分为 mount 和 update 两个阶段。

我们看下 useEffect 相关的源码:

它也是分为了 mountEffect 和 updateEffect 两个函数,最终都是在 hook.memorizedState 存取元素的。这就是 hook 的通用原理。

第二个参数对应的就是 deps,它是怎么判断是否要更新的呢?

我们着重看下这段逻辑:

deps 是新传入的参数,如果是 undefined 会作为 null。

hook.memorizedState.deps 取到的是之前的 deps。

然后新旧 deps 会做下对比,如果返回 true 才会执行 effect。

对比的逻辑在 areHookInputsEqual 这个函数里:

如果 prevDeps 是 null,那就直接返回 false,这就是 useEffect 第二个参数传 undefined 或者 null 的话 effect 函数都会执行的原因。

否则,才会新旧的 deps 数组中每个元素做对比,有一个不一样就返回 false。

这已经解释了上面那个案例,deps 数组传 undefined、[]、[dep] 时 effect 执行的不同情况。

其实还有一种情况也会导致 effect 执行,就是上面这段逻辑:

当热更新的时候,就算依赖没有变,也需要重新执行 effect,这个是通过 ignorePreviousDependencies 变量来控制的。

这个估计很多人都不知道,因为热更新是工具实现的。

我们从源码层面解释清楚了 useEffect 第二个参数的处理机制。

其实 useCallback、useMemo 的 deps 参数处理逻辑也是一样的,源码都差不多:

总结

useEffect 第二个参数传入 undefined、[]、[a,b,c] 时执行的效果不同, undefined 每次都会执行,而依赖数组只有在依赖变了才会执行,空数组只会执行一次。

我们从源码层面解释了原因:

hooks 是在 fiber 节点的 memorizedState 属性上存取数据的,会组织一个和 hook 一一对应的链表。

构建这个链表的阶段叫 mount,后面只需要 update,所以所有的 hook 的实现都分为了 mountXxx 和 updateXxx 两部分。

useEffect 在 update 时会对比新传入的 deps 和之前存在 memorizedState 上的 deps 来确定是否执行 effect 回调,它做了这样的处理:

当 dep 是 null(undefined 也会处理成 null)时,判定为不相等。如果是热更新的时候,判定为不相等。否则会对比数组的每个依赖项来判断是否相等。只要新旧 deps 不相等就执行 effect。

useCallback、useMemo 的 deps 处理也是一样的,我们从源码层面理清楚了 deps 参数的处理机制。

Guess you like

Origin juejin.im/post/7083230365027926053