在第一篇文章里我们了解了初次渲染过程react内部的处理流程和执行机制,接下里继续看看在状态更新阶段react是怎么处理的
现在触发demo中onclick事件,也就是执行setCount方法
同样从两个基础hook出发
- useState
- useEffect
更新阶段核心流程
useState
在开始之前我们带着两个问题:
- 执行setCount后,内部发生了什么?
- 如果多次执行setCount,它是怎么样取到最新的值的?
首先解答第一个问题
在第一篇文章说到了,在mountState阶段会绑定一个叫dispatchAction
的方法然后作为参数返回,这个方法在我们的demo中就是setCount方法,没有印象的看下下面的代码
function mountState(initialState) {
// 还记不记得这个熟悉的方法
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
继续深入看下diapatchAction干了什么
function dispatchAction(fiber, queue, action) {
var update = {
expirationTime: expirationTime,
action: action,
next: null
};
// 处理当前hook的queue队列
var pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
// 进入调度环节
scheduleWork(fiber, expirationTime);
}
}
其实干了两件事
- 创建update节点,连接到当前hook(也就是useState)的queue后面(
这个queue忘记的伙伴可以翻回第一篇文章中看看
),这样每次调用dispatchAction都会在后面连接一个update节点,从而生成一个更新队列(这个更新队列后面会详细讲
) - 然后开始这一轮的scheduleWork调度(
关于调度做了什么详看这篇文章,因为内容非常多,这里不做过多说明:https://segmentfault.com/a/1190000020737020?utm_source=tag-newest
),大概流程就是将所有更新任务按照优先级排列,最后遍历整个fiberTree执行更新操作,更新阶会调用beginWork
方法,这就又回到了我们初次渲染的流程,因为初次渲染时也会调用这个方法,就对应起来我们第一篇文章的初次渲染流程图
我们继续走
按照上面的流程会走到这一步,又是熟悉的代码,此时我们会把HooksDispatcherOnUpdateInDEV
赋值到dispatcher
上
{
// 首次执行currentDispatcher = null,所以进入else分支;在更新阶段会进入if分支
if (currentDispatcher !== null) {
currentDispatcher = HooksDispatcherOnUpdateInDEV;
} else {
currentDispatcher = HooksDispatcherOnMountInDEV;
}
}
继续看看HooksDispatcherOnUpdateInDEV
是什么
HooksDispatcherOnUpdateInDEV = {
useCallback: function (callback, deps) {
return updateCallback(callback, deps);
},
useEffect: function (create, deps) {
return updateEffect(create, deps);
},
useMemo: function (create, deps) {
return updateMemo(create, deps);
},
useState: function (initialState) {
return updateState(initialState);
}
}
发现在更新阶段遍历执行到useState时实际执行的是updateState方法,那继续看看updateState做了什么
function updateState(initialState) {
return updateReducer(basicStateReducer);
}
继续看看updateReducer
function updateReducer(reducer, initialArg, init) {
// 获取到当前hook,其实也就是直接.next就可以
var hook = updateWorkInProgressHook();
var queue = hook.queue;
// 取到待更新的队列
var pendingQueue = queue.pending;
// 如果待更新队列不为空,那么遍历处理
if (pendingQueue !== null) {
var first = pendingQueue.next;
var newState = null;
var update = first;
queue.pending = null;
// 循环遍历,是更新阶段的核心和关键,
do {
var action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first);
// 最新的状态值赋值给memoizedState
hook.memoizedState = newState;
}
// 将状态值和更新方法返回,就和初次渲染一样的流程
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
上面是是核心流程代码,源码要比这个更加复杂和健全一些,我们这里不做过多涉及,其实主要做了两件事
- 获取当前hook的更新队列pendingQueue也就是上面通过queue连接起来的更新队列,举个形象的例子,比如我们执行了三次setCount方法,这个时候我们当前useState hook的queue队列中就会有三项
- 拿到我们的更新队列pendingQueue,
循环遍历
进行计算和赋值操作,最终会将最新的state值复制到hook的memorizedState上并返回
综上就是我们抛出的第一个问题的答案,接下来回答第二个问题,在多次setCount后是怎么获取到最新值的?
所以综上我们知道了进行状态更新后方法执行顺序为dispatchAction->updateReducer
, 我们把dispatchAction
方法的核心代码拿出来,如下
// dispatchAction核心代码
var pending = queue.pending;
// 这里是链表创建和连接的核心
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
假设当前共执行了三次setCount,分别是setCount(1),setCount(2),setCount(3),拆开三次来看,当执行第一次setCount(1)时会执行下面的代码
// dispatchAction内
var pending = queue.pending;
if(pending == null){
update(1) = update(1).next;
queue.pending = update(1);
}
// updateReducer内,此时first是update(1)
first = queue.pending.next;
执行完成后queue队列如上图所示,fisrt是用来记录最开始的update的一个节点,此时就是update(1)
,先不用关心,继续执行setCount(2),同上
// 这里的pending其实也就是update(1)
var pending = queue.pending;
// 因为此时pending != null,所以代码走到else中
else{
update(2).next = pending.next;
pending.next = update(2);
}
queue.pending = update(2);
// 此时first仍是update(1)
first = queue.pendind.next;
执行完setCount(2)后的queue队列如上,继续执行setCount(3)
// 这里的pending其实也就是update(2)
var pending = queue.pending;
// 因为此时pending != null,所以代码走到else中
else{
// 这一步很关键,结合上面的图,是把update(1)赋值到了update(3)的next上
update(3).next = pending.next;
// 因为此时pending是update(2),所以这一步就是把update(3)赋值到update(2)的next上
pending.next = update(3);
}
queue.pending = update(3);
// 此时first仍是update(1)
first = queue.pendind.next;
综上,执行完三次setCount后的queue队列为上图所示,接下来react内部会遍历queue队列(也就是update环形链表)
上面说过setCount后react内部方法执行顺序为dispatchAction -> updateReducer
,现在开始执行updateReducer
的遍历过程,根据核心代码
// updateReducer核心代码
var pendingQueue = queue.pending;
if (pendingQueue !== null) {
// first是update(1)
var first = pendingQueue.next;
var newState = null;
var update = first;
// 循环遍历,是更新阶段的核心和关键,
do {
var action = update.action;
// reducer其实就是判断我们传入的值是否为函数如果是的话执行函数放回新值;如果不是直接返回新值
newState = reducer(newState, action);
// 然后遍历下一个update
update = update.next;
} while (update !== null && update !== first);
// 最新的状态值赋值给memoizedState
hook.memoizedState = newState;
}
一开始将update(1)赋值给update,然后获取newState也就是1,接下来update=update.next
,此时update成了update(2),依次遍历,终止条件为update === first
,也就是当update = update(3)
时满足了终止条件,此时newState = 3,取到了最新值。
这样可以保证整个update链表都循环了一遍同时取到的是链表中的最后一个节点(也就是最新节点)
综上,解答了我们一开始抛出的第二个问题。
useEffect
同上,看到HooksDispatcherOnUpdateInDEV
内部useEffect具体执行的是updateEffect
HooksDispatcherOnUpdateInDEV = {
useCallback: function (callback, deps) {
return updateCallback(callback, deps);
},
useEffect: function (create, deps) {
return updateEffect(create, deps);
},
useMemo: function (create, deps) {
return updateMemo(create, deps);
},
useState: function (initialState) {
return updateState(initialState);
}
}
继续看updateEffect
function updateEffect(create, deps) {
{
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
if ('undefined' !== typeof jest) {
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber$1);
}
return updateEffectImpl(Update | Passive, Passive$1, create, deps);
}
继续看updateEffectImpl
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
// 获取到当前hook
var hook = updateWorkInProgressHook();
// 比较依赖项是否发生了变化
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果相同则不对当前hook的属性进行更新
pushEffect(hookEffectTag, create, destroy, nextDeps);
return;
}
// 如果依赖项发生了变化,更新当前hook的memoizedState,这里的赋值只是做一个记录,并没有实际意义
currentlyRenderingFiber$1.effectTag |= fiberEffectTag;
hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, destroy, nextDeps);
}
会发现无论useEffect的依赖项是否变化,都会执行pushEffect方法,那我们一探究竟
function pushEffect(tag, create, destroy, deps) {
var effect = {
tag: tag,
create: create,
destroy: destroy,
deps: deps,
next: null
};
var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
// 创建/更新componentUpdateQueue队列
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
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;
}
主要做了两件事情:
1、创建一个effect对象并返回(和初次渲染的流程相同)
2、同时创建/更新componentUpdateQueue队列,这个队列是用来专门存取当前组件中所有的useEffect这个hook的队列(因为useEffect的回调其实是异步执行的,这里专门用一个队列存取是为了在调度阶段对所有的回调函数更方便的进行遍历处理
),componentUpdateQueue队列不存在的话会进行创建,如果存在,会和mountState阶段一样创建一个effect的循环链表(这里就不画图了,具体参考上面的update更新队列的图片,只是把update替换成了effect
),每个effect对象中有一个tag属性(tag的值类似于0和1
),刚才说到在调度阶段回遍历每一个effect,这个属性就是在遍历过程中用来判断useEffect回调是否需要被执行(这里再推荐一个讲解useEffect执行时机的深度好文:https://www.cnblogs.com/iheyunfei/p/13065047.html
)
到这里更新阶段两个核心hook的执行流程就讲解完毕
以下是我们在使用hook过程中遇到的一些问题,也可以一一解答了
1、为什么hook之间一定要固定顺序/不能用条件判断?
因为在某一组件中,每个hook之间是通过next指针依次按顺序连接的,所以一旦使用条件判断后会导致某个hook在某个情况下不存在,那么整个hook链表就被中断,无法正常遍历以及hook的获取,从而引发问题
2、多次state的更新,是如何以最后一次为准的,内部机制是
怎样的?
一句话陈述:通过一个update队列存储多次state的值依次遍历获取到最新值
具体参考上文的详细讲解
3、useEffect如何实现仅执行单次/根据依赖项变动执行多次?
初次遍历都会执行,更新阶段每个effect通过tag标识来判断是都需要执行回调
4、hooks内部为什么使用链表结构而不使用其他数据结构实现?
个人的看法是归结于链式结构和顺序结构的区别以及适用场景:
1、链式结构对内存要求不苛刻,可以随意存放
2、链式结构更适合数据的增/删操作,在分析源码过程发现增/删的场景偏多(update、effect循环队列的生成等)
如果本文对你有帮助,那就请大佬们点个小赞或者收藏,如若分析有误也请及时纠正。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。