[FE] React 初窥门径(七):hook 状态创建/更新原
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
![](https://img.haomeiwen.com/i1023733/7947bb1f306e2488.png)
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
![](https://img.haomeiwen.com/i1023733/f4ef72ec4e8abfab.png)
currentlyRenderingFiber$1
为 <App />
节点(Fiber Node),并且,Fiber Node 的
memorizedState
指向了 hook
链表的第一个 hook
。
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
(2)第二个 hook
设置 第一个 hook
的 next
属性指向 第二个 hook
,
![](https://img.haomeiwen.com/i1023733/9ec21f9378744eb0.png)
通过 Fiber Node(currentlyRenderingFiber$1
)观察一下 hook 链表的结构,
currentlyRenderingFiber$1.memorizedState -> hook1
hook1.next -> hook2
hook2.next -> null
![](https://img.haomeiwen.com/i1023733/abcb108ace051dcc.png)
2.4 dispatch(setState1 setState2)
虽然 hook 是通过链表结构来存储的,但实际调用 setState1
setState2
的时候,却并不是通过链表来取的。
这是是因为虽然 setState
只传入了一个参数 action
,
![](https://img.haomeiwen.com/i1023733/e9fff00092b4bf2f.png)
但实际 React 已通过 bind
传入了其他参数,另外两个参数是 fiber
和 queue
,
![](https://img.haomeiwen.com/i1023733/82afde8a17777032.png)
fiber
就是上文那个 currentlyRenderingFiber$1
,queue
就是 setState1
对应 hook 的 queue
属性值(hook 相关的 update quque,下文介绍)
![](https://img.haomeiwen.com/i1023733/40d99b0bc9ac1e78.png)
所以调用 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 行
![](https://img.haomeiwen.com/i1023733/3e54bc106e3a9edf.png)
我们看到 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 行
![](https://img.haomeiwen.com/i1023733/4e2374bc5ea68866.png)
其中
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 四者的关系如下,
![](https://img.haomeiwen.com/i1023733/e4beca7c3d98d8f8.png)
-
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 原理:多次调用