前言
在深究 React 的 setState 原理的时候,我们先要考虑一个问题:setState 是异步的吗? 首先以 class component 为例,请看下述代码(demo-0)
class App extends React.Component {
state = {
count: 0
}
handleCountClick = () => {
this.setState({
count: this.state.count + 1
});
console.log(this.state.count);
}
render() {
return (
<div className='app-box'>
<div onClick={this.handleCountClick}>the count is {this.state.count}</div>
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('container')
);
count 初始值为 0,当我们触发handleCountClick 事件的时候,执行了count + 1 操作,并打印了count ,此时打印出的count 是多少呢?答案不是 1 而是 0
类似的 function component 与 class component 原理一致。现在我们以 function component 为例,请看下述代码 (demo-1)
const App = function () {
const [count, setCount] = React.useState(0);
const handleCountClick = () => {
setCount((count) => {
return count + 1;
});
console.log(count);
}
return <div className='app-box'>
<div onClick={handleCountClick}>the count is {count}</div>
</div>
}
ReactDOM.render(
<App />,
document.getElementById('container')
);
同样的,这里打印出的 count 也为 0
相信大家都知道这个看起来是异步的现象,但他真的是异步的吗?
为什么 setState 看起来是『异步』的?
首先得思考一个问题:如何判断这个函数是否为异步?
最直接的,我们写一个 setTimeout ,打个 debugger 试试看
我们都知道 setTimeout 里的回调函数是异步的,也正如上图所示,chrome 会给 setTimeout 打上一个 async 的标签。
接下来我们 debugger setState 看看
React.useState 返回的第二个参数实际就是这个 dispatchSetState 函数(下文细说)。但正如上图所示,这个函数并没有 async 标签,所以 setState 并不是异步的。
那么抛开这些概念来看,上文中 demo-1 的类似异步的现象是怎么发生的呢?
简单的来说,其步骤如下所示。基于此,我们接下来更深入的看看 React 在这个过程中做了什么
从 first paint 开始
first paint 就是『首次渲染』,为突出显示,就用英文代替。
- 这里先简单看一下
App 往下的 fiber tree 结构。每个 fiber node 还有一个return 指向其 parent fiber node,这里就不细说了
我们都知道 React 渲染的时候,得遍历一遍 fiber tree,当走到 App 这个 fiber node 的时候发生了什么呢?
接下来我们看看详细的代码(这里的 workInProgress 就是整在处理的 fiber node,不关心的代码已删除)
- 首先要注意的是,虽然 App 是一个
FunctionComponent ,但是在 first paint 的时候,React 判断其为 IndeterminateComponent 。
switch (workInProgress.tag) { // workInProgress.tag === 2
case IndeterminateComponent:
{
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes
);
}
// ...
case FunctionComponent:
{ /** ... */}
}
- 接下来走进这个
mountIndeterminateComponent ,里头有个关键的函数 renderWithHooks ;而在 renderWithHooks 中,我们会根据组件处于不同的状态,给 ReactCurrentDispatcher.current 挂载不同的 dispatcher 。而在first paint 时,挂载的是HooksDispatcherOnMountInDEV
function mountIndeterminateComponent(_current, workInProgress, Component, renderLanes) {
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderLanes
);
}
function renderWithHooks() {
if (current !== null && current.memoizedState !== null) {
ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
} else if (hookTypesDev !== null) {
} else {
ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
}
var children = Component(props, secondArg);
}
- 这个
HooksDispatcherOnMountInDEV 里就是组件 first paint 的时候所用到的各种 hooks,相关参考视频讲解:进入学习
HooksDispatcherOnMountInDEV = {
useState: function (initialState) {
currentHookNameInDev = 'useState';
mountHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountState(initialState);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
}
- 接下里走进我们的
App() ,我们会调用 React.useState ,点进去看看,代码如下。这里的 dispatcher 就是上文挂载到 ReactCurrentDispatcher.current 的 HooksDispatcherOnMountInDEV
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
HooksDispatcherOnMountInDEV = {
useState: function (initialState) {
currentHookNameInDev = 'useState';
mountHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountState(initialState);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
},
}
function mountState(initialState) {
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
hook.queue = queue;
var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
-
这个函数做了这么几件事情:
-
执行 mountWorkInProgressHook 函数: function mountWorkInProgressHook() {
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
- 创建一个
hook - 若无
hook 链,则创建一个 hook 链;若有,则将新建的 hook 加至末尾 - 将新建的这个
hook 挂载到 workInProgressHook 以及当前 fiber node 的 memoizedState 上 - 返回
workInProgressHook ,也就是这个新建的 hook -
判断传入的 initialState 是否为一个函数,若是,则调用它并重新赋值给 initialState (在我们的demo-1里是『0』) -
将 initialState 挂到 hook.memoizedState 以及 hook.baseState -
给 hook 上添加一个 queue 。这个 queue 有多个属性,其中queue.dispatch 挂载的是一个 dispatchSetState 。这里要注意一下这一行代码
var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
Function.prototype.bind 的第一个参数都知道是绑 this 的,后面两个就是绑定了 dispatchSetState 所需要的第一个参数(当前fiber)和第二个参数(当前queue)。
这也是为什么虽然 dispatchSetState 本身需要三个参数,但我们使用的时候都是 setState(params) ,只用传一个参数的原因。
- 返回一个数组,也就是我们常见的
React.useState 返回的形式。此时这个 state 是 0
- 至此为止,
React.useState 在 first paint 里做的事儿就完成了,接下来就是正常渲染,展示页面
触发组件更新
- 要触发组件更新,自然就是点击这个绑定了事件监听的
div ,触发 setCount 。回忆一下,这个 setCount 就是上文讲述的,暴露出来的 dispatchSetState 。并且正如上文所述,我们传进去的参数实际上是 dispatchSetState 的第三个参数 action 。(这个函数自然也涉及一些 React 执行优先级的判断,不在本文的讨论范围内就省略了)
function dispatchSetState(fiber, queue, action) {
var update = {
lane: lane,
action: action,
hasEagerState: false,
eagerState: null,
next: null
};
enqueueUpdate(fiber, queue, update);
}
-
dispatchSetState 做了这么几件事
-
创建一个 update ,把我们传入的 action 放进去 -
进入 enqueueUpdate 函数:
- 若
queue 上无 update 链,则在 queue 上以 刚创建的 update 为头节点构建 update 链 - 若
queue 上有 update 链,则在该链的末尾添加这个 刚创建的 update function enqueueUpdate(fiber, queue, update, lane) {
var pending = queue.pending;
if (pending === null) {
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
var lastRenderedReducer = queue.lastRenderedReducer;
var currentState = queue.lastRenderedState;
var eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
}
- 根据
queue 上的各个参数(reducer、上次计算出的 state)计算出 eagerState ,并挂载到当前 update 上 -
到此,我们实际上更新完 state 了,这个新的 state 挂载到哪儿了呢?在 fiber.memoizedState.queue.pending 上。注意:
fiber 即为当前的遍历到的 fiber node;pending 是一个环状链表 -
此时我们打印进行打印,但这里打印的还是 first paint 里返回出来的 state ,也就是 0
更新、渲染 fiber tree
现在我们更新完 state,要开始跟新 fiber tree 了,进行最后的渲染。逻辑在 performSyncWorkOnRoot 函数里,同样的,不关心的逻辑我们省略
function performSyncWorkOnRoot(root) {
var exitStatus = renderRootSync(root, lanes);
}
- 同样的我们先看一眼 fiber tree 更新过程中 与 useState 相关的整个流程图
- 首先我们走进
renderRootSync ,这个函数作用是遍历一遍 fiber tree,当遍历的 App 时,此时的类型为 FunctionComponent 。还是我们前文所说的熟悉的步骤,走进 renderWithHooks 。注意此时 React 认为该组件在更新了,所以给 dispatcher 挂载的就是 HooksDispatcherOnUpdateInDEV
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
var children = Component(props, secondArg);
}
- 我们再次走进
App ,这里又要再次调用 React.useState 了
const App = function () {
const [count, setCount] = React.useState(0);
const handleCountClick = () => {
setCount(count + 1);
}
return <div className='app-box'>
<div onClick={handleCountClick}>the count is {count}</div>
</div>
}
- 与之前不同的是,这次所使用的
dispatch 为 HooksDispatcherOnUpdateInDEV 。那么这个 dispatch 下的 useState 具体做了什么呢?
useState: function (initialState) {
currentHookNameInDev = 'useState';
updateHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return updateState(initialState);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
- 可以看到大致都差不多,唯一不同的是,这里调用的是
updateState ,而之前是 mountState 。
function updateState(initialState) {
return updateReducer(basicStateReducer);
}
function updateReducer(reducer, initialArg, init) {
var first = baseQueue.next;
var newState = current.baseState;
do {
update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
queue.lastRenderedState = newState;
return [hook.memoizedState, dispatch];
}
写在最后
上文只是描述了一个最简单的 React.useState 使用场景,各位可以根据本文配合源码,进行以下两个尝试:
Q1. 多个 state 的时候有什么变化?例如以下场景时:
const App = () => {
const [count, setCount] = React.useState(0);
const [str, setStr] = React.useState('');
}
A1. 将会构建一个上文所提到的 hook 链
Q2. 对同个 state 多次调用 setState 时有什么变化?例如以下场景:
const App = () => {
const [count, setCount] = React.useState(0);
const handleCountClick = () => {
setCount(count + 1);
setCount(count + 2);
}
return <div className='app-box'>
<div onClick={handleCountClick}>the count is {count}</div>
</div>
}
A2. 将会构建一个上文所提到的 update 链
|