1. 回顾
上一篇 文章我们介绍了 React 函数组件的更新过程。
我们来总结以下 组件加载 和 更新 的全流程。
(1)组件载入时,会创建两棵 Fiber Tree
一棵为当前已写入 DOM 的 Fiber Tree(名为 current
)。
在 commit 阶段 之前这个 Fiber Tree 只有一个根节点。
另一棵为当前正在渲染的 Fiber Tree,(名为 workInProgress
)。
render 阶段 就是在创建它。
到了 commit 阶段,React 会将 workInProgress
的 Fiber Tree 实际写到 DOM 中,
然后将 current
指向这个 Fiber Tree。
这样就完成了组件的首次加载。
(2)事件触发组件更新时
首先是由 React 的事件系统监听到用户事件,然后触发用户绑定的事件处理函数。
在这个事件处理函数中,示例中我们用了 hook setState
来更新组件组件状态。
执行过程中,会将 performSyncWorkOnRoot
放到 syncQueue
中。
然后,用户事件就执行完了。
用户事件执行完之后,React 会紧接着执行 flushSyncCallbackQueue
,
获取到 syncQueue
中的 performSyncWorkOnRoot
进行执行。
performSyncWorkOnRoot
实际上就是组件的 render 和 commit 方法。
(在组件的第一次更新时)它会创建一棵 workInProgress
的 Fiber Tree,然后在 commit 阶段 写到 DOM 中(之后,将 current
指向这棵 Fiber Tree)。
(如果是组件非首次更新,此时内存中已经有了两棵 Fiber Tree 了,此时 render 阶段,并不会重新创建一棵全新的 Fiber Tree,而是尽可能利用现有 Fiber Tree 的节点,这个逻辑在 createWorkInProgress
中控制)。
如此这般,就完成了组件的更新。
以上分析中,我们是从 Fiber Tree 的角度,从 render 和 commit 的角度来看待组件的更新过程,
略过了组件的状态的计算过程。
在实际开发中,常见的场景是,
- 有多个 hook(
setState
) - 一次更新调用了多次
setState
React 内部是如何处理这个状态计算的呢?本文我们来仔细研究下这个问题。
2. 场景:多个 hook
2.1 示例项目的修改
参考 example-project/src/AppTwoState.js
我们修改了 App
组件如下,
const App = () => {
debugger;
const [state1, setState1] = useState(0);
debugger;
const [state2, setState2] = useState('a');
debugger;
const onDivClick = () => {
debugger;
setState1(1);
debugger;
setState2('b');
debugger;
};
debugger;
return <div onClick={onDivClick}>
{state1}-{state2}
</div>;
}
其中用到了两个 hook(都是 useState
),这样会给 App
组件创建两个独立的状态 state1
state2
。
2.2 两个 hook 的更新流程
我们来跟踪一下两个 useState
和 setState1
setState2
的执行过程。
完整的执行流程在这里:7.1 hook 原理:多个 hook,总共分为三个部分:
-
组件首次加载时,调用
useState
(第 1-50 行)
-
用户点击 div 时,
setState
调用lastRenderedReducer
更新状态(第 51-96 行)
-
事件响应完之前,React 调用
flushSyncCallbackQueue
更新状态(第 97-154 行)
2.3 多个 hook 是怎么存储的
我们看到组件载入的时候,useState
会调用 mountWorkInProgressHook
function mountWorkInProgressHook() {
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook; // 第一个 useState
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook; // 第二个 useState
}
return workInProgressHook;
}
每次调用 useState
会创建一个新的 hook
,多个 hook
构成了一个链表结构(第二个 hook
的 next
指向 第一个 hook
)
(1)第一个 hook
currentlyRenderingFiber$1
为 <App />
节点(Fiber Node),并且,Fiber Node 的
memorizedState
指向了 hook
链表的第一个 hook
。
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
(2)第二个 hook
设置 第一个 hook
的 next
属性指向 第二个 hook
,
通过 Fiber Node(currentlyRenderingFiber$1
)观察一下 hook 链表的结构,
currentlyRenderingFiber$1.memorizedState -> hook1
hook1.next -> hook2
hook2.next -> null
2.4 dispatch(setState1 setState2)
虽然 hook 是通过链表结构来存储的,但实际调用 setState1
setState2
的时候,却并不是通过链表来取的。
这是是因为虽然 setState
只传入了一个参数 action
,
但实际 React 已通过 bind
传入了其他参数,另外两个参数是 fiber
和 queue
,
fiber
就是上文那个 currentlyRenderingFiber$1
,queue
就是 setState1
对应 hook 的 queue
属性值(hook 相关的 update quque,下文介绍)
所以调用 setState1
setState2
时不用在 hook 链表中进行查找,而是直接进入 dispatchAction
函数中。
3. 场景:多次 dispatch
上文介绍了多个 hook 的存储和调用原理,在实际项目中,还会有一个事件中多次调用了 dispatch(setState),
这些 dispatch 函数也许是同一个状态的 dispatch(多次调用 setState
),也许是不同状态的(先后调用 setState1
setState2
)。
原理其实是大同小异的,为了简单起见,本文只介绍后者,即,一个事件中,多次调用了同一个 hook 的 dispatch(setState
)的执行流程。
3.1 示例项目的修改
示例项目的修改如下,example-project/src/AppAsyncState.js
(为了便于跟踪,setState
采用了回调方式进行编写)
const App = () => {
debugger;
const [state, setState] = useState(0);
debugger;
const onDivClick = () => {
debugger;
setState(s => {
debugger;
return s + 1;
});
debugger;
setState(s => {
debugger;
return s + 2;
});
debugger;
};
debugger;
return <div onClick={onDivClick}>
{state}
</div>;
}
3.2 多次调用 setState 的执行流程
完整的执行流程可参考 7.2 hook 原理:多次调用,包含以下两个部分,
(省略了组件首次加载的流程)
(1)用户点击 div 触发事件,事件中调用了两次 setState
,第 1-65 行
我们看到 React 只执行了第一个状态更新函数(第一次
setState
的 action
参数),
s => {
debugger;
return s + 1;
}
第二次 setState
的 action
并未在这个阶段执行,而是将更新过程,放到了一个名为 update
的循环队列中。
参考 dispatchAction L16620
function dispatchAction(fiber, queue, action) {
...
var update = {
lane: lane,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
var pending = queue.pending;
if (pending === null) {
update.next = update; // 第一次调用 setState 时,循环队列只有一个元素(自己指向自己)
} else {
update.next = pending.next;
pending.next = update; // <- 将 update 放到循环队列中(逻辑见下文解释)
}
queue.pending = update;
var alternate = fiber.alternate;
if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
...
} else {
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
...
if (lastRenderedReducer !== null) {
...
try {
var currentState = queue.lastRenderedState;
var eagerState = lastRenderedReducer(currentState, action);
update.eagerReducer = lastRenderedReducer; // <- 用来标记这个 update 元素已经计算过了
update.eagerState = eagerState;
if (objectIs(eagerState, currentState)) {
return;
}
} catch (error) {// Suppress the error. It will throw again in the render phase.
} finally {
...
}
}
}
...
}
...
}
update
逻辑如下,
- 每个
hook
维护了一个 update quque,hook
的pending
属性指向了这个 quque 的队尾(队尾的next
为队首) - 每次调用
setState
(=dispatchAction
)都会创建一个update
节点 - 第一次调用
setState
,update quque 只包含了一个元素(自己指向自己),然后设置hook.pending
指向这个update
元素 - 第二次调用
setState
,会在 update quque 队尾添加一个元素,再设置当前这个队尾元素指向队首,
hook.pending -> 当前的 update 元素
(当前的 update 元素).next -> 队首
原队尾.next -> 当前的 update 元素
以上这样设置的好处是,可以从队尾元素开始,循环获取 next
元素,将队列按顺序处理一遍。
值得一提的是,React 采用了给 update.eagerReducer
赋值为 lastRenderedReducer
的方式,来标记这个 update 元素已经处理过了,
update.eagerReducer = lastRenderedReducer;
这里要留意一下,下文会用到。
(2)事件完成之前,React 通过 flushSyncCallbackQueue
,更新 Fiber Tree,并写入到 DOM 中,第 67-166 行
其中
syncQueue
中保存了 performSyncWorkOnRoot
,React 用它在事件结束之前更新页面(见 前一篇 的分析)update quque 是本文介绍的内容,React 在每次调用
setState
的时候,会创建一个循环队列,然后在 performSyncWorkOnRoot
的 render 阶段 再执行计算。
代码逻辑在这里 updateReducer L15761
function updateReducer(reducer, initialArg, init) {
var hook = updateWorkInProgressHook();
var queue = hook.queue;
...
if (baseQueue !== null) {
...
do { // <- 从队首开始处理 update quque
...
if (!isSubsetOfLanes(renderLanes, updateLane)) {
...
} else {
...
if (update.eagerReducer === reducer) { // 用来标记第一个 setState 已经计算过了
newState = update.eagerState;
} else {
var action = update.action;
newState = reducer(newState, action); // 后续未计算过的 setState,会按顺序执行计算
}
}
update = update.next;
} while (update !== null && update !== first);
...
}
...
}
这里出现了对 update 元素 update.eagerReducer
的判定,来区分这个元素所表示的 setState
是否已经计算过了。
所以,除了第一个 setState
是 “同步”(setState
返回之前)执行的之外,
后续各个 setState
都是 “异步”(setState
返回后,由 React 通过 flushSyncCallbackQueue
在 render 阶段) 执行的。
4. fiber, hook, update
Fiber Tree,Fiber Node,hook,Update Queue 四者的关系如下,
Fiber Tree 有两棵
一棵是已写入到的 DOM 的(称为current
),一棵是用于 render 阶段处理的(称为workInProgress
)
Fiber Tree 的根节点的tag
为HostRoot
两棵 Fiber Tree 的根节点通过stateNode
指向FiberRootNode
,它通过containerInfo
保存了 html 元素div#root
Fiber Tree 的节点有三个属性,return
指向父节点,child
指向子节点,alternate
指向同级的另一棵 Fiber Tree一个 React 组件可以使用多个 hook(创建多个独立的状态)
hook 保存在了 Fiber Node (代表<App />
元素的那个)的memorizedState
属性中,多个 hook 以链表形式存储
(同层级的 Fiber Node 共用一个 hook 对象)(可能会出现复制的情况)
每一个 hook(比如useState
)返回一个新的dispatch
方法,
特定dispatch
方法的每次调用,都会创建一个update
元素,并添加到 update quque 中。
hook.queue.pending
指向了 update queue 的队尾,队尾指向队首(循环队列)。组件通过
setState
进行状态更新时
只有第一个 更新 会在setState
返回值之前执行,不论是setState(action)
中的action
是数值还是函数
后续所有(同一个或其他 hook)的setState
调用,都会将更新放到 update quque 中,
然后由 React 通过flushSyncCallbackQueue
调用performSyncWorkOnRoot
在 render 阶段按顺序执行计算。
参考
React 初窥门径(六):React 组件的更新过程
github: thzt/react-tour
7.1 hook 原理:多个 hook
7.2 hook 原理:多次调用