1. 首页

React Hook源码解析(二)

写在前面

在上一篇文章中,

juejin.im/post/5e7cc0…

主要分析了Hook在React中是如何保存的,以及Hook的更新过程。本文中,我们将通过下面两个问题,继续深入研究Hook,以弥补上文中略过的一些细节。

1、如果我连续多次调用setState,Hook会怎么处理呢?

2、Hook的useEffect 是如何工作的?

连续多次setState

先看示例代码:


const App = () => { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); setCount(count + 2); setCount(count + 3); }; return <React.Fragment> <span style={{ marginRight: '10px' }}>{count}</span> <button onClick={handleClick}>点击</button> </React.Fragment> };

我们点击一次button,最终页面上会输出多少呢?熟悉React的朋友们,很快就会得到答案:3

在上一篇源码解析中,这部分内容被忽略了。本文我们来看看这里的内部逻辑。

首先,先复习一下hook的结构:


var hook = { memoizedState: null, // 当前的state值 baseState: null, queue: null, // 存储更新信息 baseUpdate: null, next: null // 指向下一个hook对象的指针 };

我们先看一下组件挂载完成后的hook,注意queue字段的值:

React Hook源码解析(二)

之前讲过,调用setCount的时候,实际上调用的是dispatchAction.bind(null, currentlyRenderingFiber$1, queue)这个函数。这个函数通过闭包保存了对应的Fiber和hook对象的queue的引用。

首次setCount的时候,hook对queue的处理如下:


// 更新信息 var _update2 = { expirationTime: expirationTime, // Fiber调度相关 suspenseConfig: suspenseConfig, action: action, // setCount函数接受的参数 eagerReducer: null, eagerState: null, next: null }; var last = queue.last; // 首次更新时 if (last === null) { // This is the first update. Create a circular list. // 第一次更新,构建一个 环 _update2.next = _update2; } else { // 后续更新 var first = last.next; if (first !== null) { // Still circular. _update2.next = first; } last.next = _update2; } // queue的最近一次更新指向_update2 queue.last = _update2;

第一次setCount后,会构造一个环形结构:

React Hook源码解析(二)

第二次、第三次setCount时,会继续构造queue链:


var first = last.next; if (first !== null) { // Still circular. _update2.next = first; } last.next = _update2;

最终会形成下图的结构:

React Hook源码解析(二)

组件重新渲染时,react会从hook的queue链中,找到最新的值,赋值给hook的memoizedState,我们就可以拿到最新的state了:


// 代码有省略 ... // 循环,直到拿到queue链上最新的值 do { var updateExpirationTime = _update.expirationTime; if (updateExpirationTime < renderExpirationTime$1) { ... } else { ... if (_update.eagerReducer === reducer) { ... } else { var _action = _update.action; _newState = reducer(_newState, _action); } } prevUpdate = _update; _update = _update.next; } while (_update !== null && _update !== first); hook.memoizedState = _newState; // 最新的state值,本例中为3 hook.baseUpdate = newBaseUpdate; // 最新的基础更新信息,action=3 hook.baseState = newBaseState; // 最新的基础state值,本例中为3 queue.lastRenderedState = _newState; // 最近渲染的state值,本例中为3 return [hook.memoizedState, dispatch];

这里有一个注意事项,在上一篇文章中,我们提到过,setState中是支持传入函数的。假设我们在setState中传入的参数是一个函数,在本例中,如果我们点击按钮后的代码改成:


const handleClick = () => { setCount(count => count + 1); setCount(count => count + 2); setCount(count => count + 3); };

最终的count值就不是3了,而是6。这是因为传入reducer的是最新的state:


... do { var action = update.action; // 这里的action是我们传入的回调函数 newState = reducer(newState, action); // newState 是最新的 state update = update.next; // 取hook对象queue链上的下一次更新 } while (update !== null); ...

useEffect是如何工作的

首先上示例代码:

const fakeReq = function(input) {
    return new Promise( resolve => {
        setTimeout(() => {
            resolve(`${input} - ${Date.now()}`);
        }, 500);
    });
}

const App = () => {
    const [input, setInput] = useState('');
    const [res, setRes] = useState('');

    useEffect(() => {
        fakeReq(input).then(res => {
            setRes(res);
        });
    },[input]);

    return <React.Fragment>
        <input value={input} onChange={e => setInput(e.target.value)} />
        <div>
            返回结果为:<span>{res}</span>
        </div>
    </React.Fragment>
};

上面的代码中,我们在输入框进入输入的同时,会发起一个请求,并且将返回的结果显示在页面上。首先,我们来看看React是怎么保存useEffect的。

在代码中,调用useEffect后,同样会生成一个hook对象,只是这个hook对象的memoizedState字段不太一样:


... // fiberEffectTag 和 hookEffectTag 是两个标识 // create、deps是我们传入useEffect的两个参数 function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) { var hook = mountWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; sideEffectTag |= fiberEffectTag; // useEffect生成的hook对象的memoizedState是一个特殊的对象 hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps); } ...

我们来看看pushEffect干了什么:


function pushEffect(tag, create, destroy, deps) { // effect对象 var effect = { tag: tag, create: create, destroy: destroy, deps: deps, // Circular next: null }; // componentUpdateQueue是一个全局变量,用来保存组件的最新的副作用 if (componentUpdateQueue === null) { componentUpdateQueue = createFunctionComponentUpdateQueue(); componentUpdateQueue.lastEffect = effect.next = effect; } else { var lastEffect = componentUpdateQueue.lastEffect; if (lastEffect === null) { componentUpdateQueue.lastEffect = effect.next = effect; } else { var firstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; } } return effect; }

构造一个带环的链:

React Hook源码解析(二)

在本例中,初始化完成后,最终Fiber对象的hook链为:

React Hook源码解析(二)

当我们在输入框进行输入时,来看看useEffect是如何起作用的。

输入时,会触发组件的重新渲染,假设我们输入了3,此时传入useEffect的依赖变成了:


useEffect(() => { fakeReq(input).then(res => { setRes(res); }); },['3']);

useEffect的更新代码为:


function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) { // 当前处理的hook var hook = updateWorkInProgressHook(); // 最新传入的依赖 var nextDeps = deps === undefined ? null : deps; var destroy = undefined; if (currentHook !== null) { // 上一次的effect var prevEffect = currentHook.memoizedState; destroy = prevEffect.destroy; if (nextDeps !== null) { var prevDeps = prevEffect.deps; // 对比两次依赖是否相同。如果相同,则在componentUpdateQueue上增加一个 tag = NoEffect$1 的 effect。这里的 NoEffect$1 是一个常量, 值为 0。 这里很重要 if (areHookInputsEqual(nextDeps, prevDeps)) { pushEffect(NoEffect$1, create, destroy, nextDeps); return; } } } sideEffectTag |= fiberEffectTag; // 如果两次依赖不同,在 componentUpdateQueue 上增加一个 effect,并且更新hook的memorizedState hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps); }

经过React的调度,会在 commitHookEffectList 这个函数中,判断是否需要执行 useEffect 中传入的函数:


function commitHookEffectList(unmountTag, mountTag, finishedWork) { var updateQueue = finishedWork.updateQueue; var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { var firstEffect = lastEffect.next; var effect = firstEffect; do { if ((effect.tag & unmountTag) !== NoEffect$1) { // Unmount var destroy = effect.destroy; effect.destroy = undefined; if (destroy !== undefined) { destroy(); } } if ((effect.tag & mountTag) !== NoEffect$1) { // Mount var create = effect.create; effect.destroy = create(); { var _destroy = effect.destroy; if (_destroy !== undefined && typeof _destroy !== 'function') { var addendum = void 0; if (_destroy === null) { addendum = ' You returned null. If your effect does not require clean ' + 'up, return undefined (or nothing).'; } else if (typeof _destroy.then === 'function') { ... } else { addendum = ' You returned: ' + _destroy; } ... } } } effect = effect.next; } while (effect !== firstEffect); } }

NoEffect$1是一个等于0的全局常量,从上面代码的do...while...部分可以看到,当一个 effect 的 tag 为 0时,和任何变量做与运算,值都为0,不会进行任何操作。而上面的分析也提到了,useEffect的dep没有变时,会声明一个 tag = NoEffect$1 的effect。因此,useEffect的dep没有变化时,useEffect的函数不会被执行。

我们再来看看,react是怎么比较两次的deps是否相同的:


// useEffect中传入的Deps是否相同 function areHookInputsEqual(nextDeps, prevDeps) { ... for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) { // 这里的 is$1 ,就是 Object.is 这个方法 if (is$1(nextDeps[i], prevDeps[i])) { continue; } return false; } return true; }

总结

回到本文开头的两个问题:

1、如果我连续多次调用setState,Hook会怎么处理呢?

2、Hook的useEffect 是如何工作的?

对于每一个hook,react会在hook对象的queue字段上,以有环链的形式,存储更新信息。连续多次更新,会沿着queue链计算出最新该hook最新的值。

使用useEffect,也会生成一个hook对象。只是该hook对象与useState生成的hook对象有区别。组件重新渲染时,会判断传入useEffect的dep依赖是否与上一次相同,相同的话,则会为此次更新打上特殊的tag,保证不会执行useEffect中传入的函数。

写在后面

本文在前一篇文章的基础上,进一步分析了hook中state的更新机制。另外,大致分析了useEffect是如何存储,如何工作的。由于本文不涉及react的调度更新过程,看起来不太连贯,请多包涵。关于react hook的更多解析,请关注我后续的文章。

作者:CoyPan
链接:https://juejin.im/post/5e8495e5f265da47d537bbcd

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:React Hook源码解析(二)

链接:https://www.javascriptc.com/3832.html

« 终极指南:提高Nginx服务器硬度的12个技巧
项目中CSS 性能优化,都有哪些方法?»
Flutter 中文教程资源

相关推荐

QR code