[FE] React Hook: useState & setS
背景
React 16.8 引入了 Hook 的概念,一直以来都没时间仔细研究它的实现方式,
本文从一个最简单的例子出发,记录了 react@16.13.1 的 useState 源码逻辑。
概览
示例代码:github: debug-react
function App() {
debugger;
const [state, setState] = useState(0);
debugger;
const increment = () => {
debugger;
setState(s => {
debugger;
return s + 1;
});
debugger;
setState(s => {
debugger;
return s + 2;
});
debugger;
};
debugger;
return <button onClick={increment}>{state}</button>;
}
以上代码中,使用 useState
并设置初始值为 0
,先完成了首次渲染。
然后,点击页面中的按钮,触发 increment
中的 setState
对页面进行更新。
精简版的业务逻辑如下,
image
标红的是首次渲染与更新不同的逻辑,留意以下几个函数,
(1)首次渲染:mountIndeterminateComponent
,mountState
,createFiberFromElement
(2)更新:setState
,updateFunctionComponent
,updateState
,useFiber
首次渲染
由 ReactDOM.render
触发,调用了组件的装载方法 mountIndeterminateComponent
,
接着就执行组件(函数 App
)内部的逻辑了。
App
内部第一行就是 useState
,
它会创建了一个 hook
,然后把它挂载到 fiber
上,
每个 hook
还包含了一个 queue
。
fiber
hook
quque
的关系如下,
fiber: { // Fiber
// 节点相关
sibling, child, return, index,
// 状态相关
memoizedState: { // Hook
memorizedState, baseState, baseQueue, next,
queue: { // UpdateQueue
pending, dispatch, lastRenderedReducer, lastRenderedState,
},
},
updateQueue,
// 渲染相关
effectTag, nextEffect, alternate,
}
fiber
构成了一棵树,通过 sibling, child, return, index
相连。
多个 hook
构成一个循环链表,通过 next
相连。
建好这些数据结构之后,useState
就返回了,
return [hook.memoizedState, dispatch];
它返回了两个值,一个是 hook.memoizedState
表示 hook
的当前状态,
在首次渲染时,这个状态就是初始值 0
。
另一个是 dispatch
函数,也就是后文要用的 setState
。
useState
及外层的 App
组件函数执行完后,
mountIndeterminateComponent
会接着调用 reconcileChildren
创建新的 fiber
。
fiber
整棵树都更新完之后,
就执行 commitRoot
一次性的渲染到页面上。
更新
更新逻辑在渲染到页面之前,分为两步
(1)响应事件
(2)更新状态
(1)响应事件
点击页面上的按钮,React 会响应绑定到 button
上的 onClick
事件,
从而调用了 increment
回调函数。
在这个 increment
函数中,我们的示例调用了两次 setState
,
用于说明这个两个 setState
的处理逻辑其实是不同的。
每个 setState
都会重新创建一个 update
对象,
update
构成了一个循环链表,用于记录所有待更新的操作,结构如下,
update: {
expirationTime, suspenseConfig, action,
eagerReducer, eagerState,
next, priority,
}
虽然 update
对象都会创建,但却只有第一个 setState
的 update
会被执行。
剩下 setState
的 update
会记录在链表中。
(2)更新状态
update
链表中的后续操作,会在下次调用 useState
的时候执行。
仔细来看的话,React 响应完事件后,会进入 workLoopSync
循环,
然后调用了 updateFunctionComponent
,更新组件状态。
此时会再次调用 App
函数,useState
也被再次调用。
然后 useState
执行了和首次渲染不同的逻辑,调用了 updateState
,
执行 update
循环链表中的所有的操作,
最后把更新的结果返回。
return [hook.memoizedState, dispatch];
此时 hook.memoizedState
是所有 setState
都执行后的结果了。
Reconcile & Commit
剩下的流程就跟首次渲染大同小异了,
先是调用 reconcileChildren
更新 fiber
树,然后 commitRoot
渲染到页面上。
总结
本文简单梳理了 react@16.13.1 useState
和 setState
的业务逻辑,
可以看到 hook 的主要神秘之处在于,将 update
暂存起来异步更新。
以上分析还只是 React 源码的九牛之一毛,还需要不断的学习探索。