ID: 符合预期的CoyPan
CoyPan,BAT某厂符合预期的FE,正努力成为一名出色的工程师
在上一篇文章中,主要分析了Hook在React中是如何保存的,以及Hook的更新过程。本文中,我们将通过下面两个问题,继续深入研究Hook,以弥补上文中略过的一些细节。
1、如果我连续多次调用setState
,Hook会怎么处理呢?
2、Hook的useEffect 是如何工作的?
先看示例代码:
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
字段的值:
之前讲过,调用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
时,会继续构造queue链:
var first = last.next; if (first !== null) { // Still circular. _update2.next = first; } last.next = _update2;
最终会形成下图的结构:
组件重新渲染时,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); ...
首先上示例代码:
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; }
构造一个带环的链:
在本例中,初始化完成后,最终Fiber对象的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的更多解析,请关注我后续的文章。