useState的源码解析
从全面拥抱hooks后,我们在代码中经常使用useState
的钩子,再使用的过程中,我有一些思考点,好奇
react内部是怎么实现的,于是就简单的研究了一下react的useState
源码部分。
思考点
带着思考点去研究源码,可以让我们有明确的目标。
- 初始化的时候,传入
useState
的初始值怎么返回的 - 组件重新渲染后,不是当前
useState
的set
触发的,是怎么返回初始化的值的 - 设置新的值后,是怎么返回最新的值,而不返回初始化的值的
名词说明
current: 当前正在页面渲染的node,如果是第一次渲染,则为空
workInProgress: 新的node,用于下一次页面的渲染更新
component: node对应的组件
currentlyRenderingFiber$1 = workInProgress (在
renderWithHooks
中执行)
// 函数组件中的memoizeState
interface State {
memoizedState: state数据,和baseState值相同,
baseState: state数据,
baseQueue: 本次更新之前没执行完的queue,
next: 下一个state,
queue: {
pending: 更新state数据(这个数据是一个对象,里面有数据,还有其他key用于做其他事情。),
dispatch: setState方法本身,
lastRenderedReducer: useReducer用得上,
lastRenderedState: 上次渲染的State.memoizedState数据,
}
}
代码片段
我们研究的代码片段是下面这样的,使用了2个useState
,分别是count
和name
, 然后有2个更新值的按钮
function Index() {
const [count, setCount] = useState(0)
const [name, setName] = useState('yx')
return (
<div>
<p>count: {count}</p>
<p>{name}</p>
<button onClick={() => {
setName(name => name + 'y')
}}>change name</button>
<button onClick={() => {
setCount(count => count + 1)
}}>change count</button>
</div>
)
}
源码解析
我们基于上面的代码片段,将流程分为2个大部分,初始化和更新。分别探讨useState
在初始化和更新表现。
react对于钩子函数(hooks)分为几个阶段,每一个阶段调用的hooks的函数都不同。
- 在组件初始化的时候,调用的是
HooksDispatcherOnMountInDEV
下的钩子 - 在更新的时候,调用的是
HooksDispatcherOnUpdateInDEV
下的钩子
初始化
在初始化的时候,调用useState实际上是调用HooksDispatcherOnMountInDEV
下面的useState
, 我们只关注主要流程。通过下面代码可以看出,实际上是调用了mountState(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;
}
},
}
mountState()
函数主要是执行了一些初始化的操作,主要的是分为几步。
- 执行
mountWorkInProgressHook
函数 - 赋值初始化的值到当前组件的fiber中
- 包装dispatch函数
function mountState(initialState) {
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
mountWorkInProgressHook
的执行
- 执行
mountWorkInProgressHook()
在当前组件的fiber上挂载一个memoizedState
属性,记录初始化的值、下一个钩子的链表以及初始化workInProgressHook
function mountWorkInProgressHook () {
currentFiber.memoizedState = workInProgressHook = {
memoizedState: null, // 初始化的值
baseState: null,
baseQueue: null,
queue: null, // 包含有待更新的值
next: null // 记录下一个钩子函数
};
return workInProgressHook
}
总结
从上面的代码中,我们可以看出,useState
初始化的时候,主要是执行了mountState
函数,然后将传入初始化的值挂载当前组件的fiber的memoizedState, 并直接返回,这样我就得到了需要渲染的值, 至此初始化的操作就全部完成。
更新其他的state
值
当我们点击change name按钮 的时候,执行的是setName
, 并没有修改count的值。当组件重新渲染的时候,useState(count)
是怎么返回的值呢? 由于已经初始化过了,当我们执行完name的dispatch
后。组件重新渲染,当再次执行到useState(count)
的时候,经历了如下逻辑:
- 在更新阶段执行的是
HooksDispatcherOnUpdateInDEV
下面的useState
方法, 主要是保留dispatch
方法和执行updateState(initialState)
并传入初始化的值
HooksDispatcherOnUpdateInDEV = {
useState: function (initialState) {
currentHookNameInDev = 'useState';
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return updateState(initialState);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
},
}
- 接下来我们看看
updateState()
做了什么操作。它主要是执行了updateReducer
方法, 并传入了一个basicStateReducer
函数。
function updateState(initialState) {
return updateReducer(basicStateReducer);
}
function basicStateReducer(state, action) {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
- 看来我们的重点应该是
updateReducer()
函数的执行中做了什么操作。
updateReducer()
做了什么
- 执行了
updateWorkInProgressHook()
函数,返回一个hook。这个函数主要是通过react的双缓存树,获取到当前hooks记录的状态。文章最后有这个函数的执行分析,我们先只关注返回的值hook
。
function updateReducer(reducer, initialArg, init) {
/*
hook = {
baseQueue: null
baseState: 0
memoizedState: 0
next: null
queue: {
dispatch: ƒ ()
lastRenderedReducer: ƒ basicStateReducer(state, action)
lastRenderedState: 0
pending: null
}
}
*/
var hook = updateWorkInProgressHook();
var queue = hook.queue;
var current = currentHook; // The last rebase update that is NOT part of the base state.
var baseQueue = current.baseQueue; // The last pending update that hasn't been processed yet.
var pendingQueue = queue.pending;
if(pendingQueue !== null) {
....
}
if(baseQueue !== null) {
...
}
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
- 取出
currentHook
有没有baseQueue
或者hook
下面的queue
的pendingQueue
值,由于我们没有修改count的值,只修改了name的值,所以pendingQueue
和baseQueue
都是null, 直接就返回了之前记录初始化的值,并没有做什么改变。
总结
从上面我们可以看出,当更新流程不是当前的useState
触发的时候,由于没有对应的更新队列,其实react内部并没有做什么操作,直接返回了初始化的时候记录在组件fiber中的memoizedState
值。
更新对应的state
如果我们执行的是setCount
的时候, 修改了count
的值,那useState
的执行逻辑是什么样的呢?
- 第一步还是肯定是执行更新
updateWorkInProgressHook
下面的useState
- 从上面的介绍,我们知道主要的流程还是在执行
updateReducer()
后。
updateReducer()
分析
- 进入
updateWorkInProgressHook()
获取hook,我们得到类似这样的一个结构的hook和currentHook
, 用于之后的updateReducer
中分析
// 其实当前的hook就是把currentHook的next的值设置为null
let hook = {
baseQueue: null,
baseState: 0, // 上一次的值
memoizedState: 0, // 上一次的值
next: null,
queue: {
lastRenderedState: 0,
lastRenderedReducer: basicStateReducer,
dispatch: func,
pending: { // 待更新的队列
action: ƒ (count),
eagerReducer: ƒ basicStateReducer(state, action),
eagerState: 1, // 新的值
lane: 1, // 更新优先级
next: {
action: ƒ (count),
eagerReducer: ƒ basicStateReducer(state, action),
eagerState: 1,
lane: 1,
next: ....
}
}
}
}
let currentHook = {
baseQueue: null,
baseState: 0, // 上一次的值
memoizedState: 0, // 上一次的值
next: {
baseQueue: null,
baseState: "yx",
memoizedState: "yx",
next: null, //最后一个钩子
queue: { // 更新队列
pending: null,
...
}
},
queue: {
lastRenderedState: 0,
lastRenderedReducer: basicStateReducer,
dispatch: func,
pending: { // 待更新的队列
action: ƒ (count),
eagerReducer: ƒ basicStateReducer(state, action),
eagerState: 1, // 新的值
lane: 1, // 更新优先级
next: {
action: ƒ (count),
eagerReducer: ƒ basicStateReducer(state, action),
eagerState: 1,
lane: 1,
next: .... //重复
}
}
}
}
- 执行完
updateWorkInProgressHook
后,我们得到了一个hook对象以及currentHook
的值,重新进入到updateReducer
中。
function updateReducer(reducer, initialArg, init) {
var hook = updateWorkInProgressHook(); // 得到一个更新链表
var queue = hook.queue;
var current = currentHook; // 当前的队列
var baseQueue = current.baseQueue; // 此时当前的队列为null
var pendingQueue = queue.pending; // 找到hook下面的queue更新队列
// 进入条件,将pendingQueue同步到baseQueue
if (pendingQueue !== null) {
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null; // 同步更新后,重置掉pending
}
// 有待更新的队列,进入更新
if (baseQueue !== null) {
var first = baseQueue.next; // 获取到准备更新到队列 {eagerState: 1}
var newState = current.baseState; // 获取到当前的值
var newBaseState = null;
var newBaseQueueFirst = null;
var newBaseQueueLast = null;
var update = first;
do {
var updateLane = update.lane;
if (update.eagerReducer === reducer) {
// If this update was processed eagerly, and its reducer matches the
// current reducer, we can use the eagerly computed state.
newState = update.eagerState; // 此时newState的值为1
} else {
var action = update.action;
newState = reducer(newState, action);
}
}
update = update.next; // 相同退出循环
} while (update !== null && update !== first);
// 赋值最新的值
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}。
// 得到新的值并返回给useState
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
- 执行完
updateReducer
后得到最新的值返回,到这里就全部结束了。其他部分都是双缓存树进行对比然后进行虚拟dom渲染过程。
总结
到这里,我们就明白了,如果更新的是当前的state,主要是通过updateWorkInProgressHook
获取当前的hook以及currentHook
,之后再次进入updateReducer
去计算值。
在updateReducer
中主要是通过是否有待更新的队列pendingQueue
(来自于hook中的queue
)来判断流程,如果没有,就直接返回了一开始记录的值。如果有,就同步到baseQueue
中,然后根据baseQueue
, 通过一个while去计算值,然后更新值到组件fiber的memoizedState
值上。
更多
在看源码的过程中,我发现了很多钩子在执行的过程中都执行了下面的这个代码。在updateReducer
中也看到了currentHook
,我们来分析一下updateWorkInProgressHook
的内部执行过程。
var hook = updateWorkInProgressHook();
updateWorkInProgressHook做了什么
updateWorkInProgressHook
大致上做了以下几件事情:
- 返回了当前执行的hook的带更新队列和当前的值1
- 给当前的hook(
currentHook
)进行赋值和迭代,通过next属性
代码分析
先看看整体的代码,之后我们单个的分析执行的流程。
// 整体代码
function updateWorkInProgressHook() {
var nextCurrentHook;
// 第一次走到这里
if (currentHook === null) {
var current = currentlyRenderingFiber$1.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
var nextWorkInProgressHook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
currentHook = nextCurrentHook;
var newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null
};
if (workInProgressHook === null) {
currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
执行第一个useState
主要是通过2种情况,当刚开始执行第一个useState
的时候currentHook
和workInProgressHook
都为null,我们需要给它赋值。所有会走到currentHook===null
分支,获取到缓存树的fiber,将nextCurrentHook
的值赋值为memoizedState
的值。
此时nextCurrentHook
的结构如下。我们知道当前的执行hook,以及下一个hook。
let nextCurrentHook = {
baseQueue: null,
baseState: 0, // 上一次的值
memoizedState: 0, // 上一次的值
next: {
baseQueue: null,
baseState: "yx",
memoizedState: "yx",
next: null, //最后一个钩子
queue: { // 更新队列
pending: null,
...
}
},
queue: {
lastRenderedState: 0,
lastRenderedReducer: basicStateReducer,
dispatch: func,
pending: { // 待更新的队列
action: ƒ (count),
eagerReducer: ƒ basicStateReducer(state, action),
eagerState: 1, // 新的值
lane: 1, // 更新优先级
next: {
action: ƒ (count),
eagerReducer: ƒ basicStateReducer(state, action),
eagerState: 1,
lane: 1,
next: .... //重复
}
}
}
}
得到nextCurrentHook
的值后,继续执行,我们需要赋值currentHook
的值,以及构建一个新的newHook
值并返回。
function updateWorkInProgressHook() {
var nextCurrentHook;
if (currentHook === null) {
var current = currentlyRenderingFiber$1.alternate;
nextCurrentHook = current.memoizedState;
}
// 此时workInProgressHook也为null
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState; (也为null)
}
// 这里给currentHook进行赋值,这样下次执行的时候就有值了。
currentHook = nextCurrentHook;
var newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue, // 保留对应hook的更新队列
next: null // 清空next,只关注单个hook的
};
// 这里给workInProgressHook进行复制操作,由于上一步它为空值
if (workInProgressHook === null) {
currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
}
return workInProgressHook
}
到这里我们就返回了第一个hook执行updateWorkInProgressHook
的情况。
总结:就是根据缓存fiber获取memoizedState
属性。将其赋值给nextCurrentHook
和currentHook
。此时currentHook
和workInProgressHook
的值为如下:
currentHook = {
baseQueue: null,
baseState: 0, // 上一次的值
memoizedState: 0, // 上一次的值
next: {
baseQueue: null,
baseState: "yx",
memoizedState: "yx",
next: null, //最后一个钩子
queue: { // 更新队列
pending: null,
...
}
},
queue: {
lastRenderedState: 0,
lastRenderedReducer: basicStateReducer,
dispatch: func,
pending: { // 待更新的队列
action: ƒ (count),
eagerReducer: ƒ basicStateReducer(state, action),
eagerState: 1, // 新的值
lane: 1, // 更新优先级
next: {
action: ƒ (count),
eagerReducer: ƒ basicStateReducer(state, action),
eagerState: 1,
lane: 1,
next: .... //重复
}
}
}
}
// 其实就是将currentHook.next 重置为null,赋值给workInProgressHook,返回给updateReducer中使用
workInProgressHook = {
baseQueue: null,
baseState: 0, // 上一次的值
memoizedState: 0, // 上一次的值
next: null,
queue: {
lastRenderedState: 0,
lastRenderedReducer: basicStateReducer,
dispatch: func,
pending: { // 待更新的队列
action: ƒ (count),
eagerReducer: ƒ basicStateReducer(state, action),
eagerState: 1, // 新的值
lane: 1, // 更新优先级
next: {
action: ƒ (count),
eagerReducer: ƒ basicStateReducer(state, action),
eagerState: 1,
lane: 1,
next: .... //重复
}
}
}
}
执行第二个useState
由于currentHook
和workInProgressHook
在执行第一个hook的时候已经被赋值。所以会走到和执行第一个hook不同的分支。在第一步中,workInProgressHook
被赋值为newHook
被清空了next, 所以现在nextWorkInProgressHook
的值也为null
function updateWorkInProgressHook() {
var nextCurrentHook;
nextCurrentHook = currentHook.next; // 这就获取了name的那个hook
var nextWorkInProgressHook
nextWorkInProgressHook = workInProgressHook.next; // null
currentHook = nextCurrentHook;
var newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null
};
// 这一步主要是给上面的nextWorkInProgressHook和workInProgressHook都赋值为newHook,
// 因为之前nextWorkInProgressHook的值为null
workInProgressHook = workInProgressHook.next = newHook;
return workInProgressHook
}
此时的currentHook
和workInProgressHook
的结构为下面的格式:
currentHook = {
baseQueue: null
baseState: "yx"
memoizedState: "yx"
next: null
queue: {
dispatch: func
lastRenderedReducer: basicStateReducer
lastRenderedState: "yx"
pending: null
}
}
workInProgressHook = {
memoizedState: "yx",
baseState: "yx",
baseQueue: null,
queue: {
dispatch: func
lastRenderedReducer: basicStateReducer
lastRenderedState: "yx"
pending: null
},
next: null
};
到这里updateWorkInProgressHook
就分析完成了。在dubugger
的过程中,我发现:
currentHook
在每次的渲染之后,都会被renderWithHooks
重置为null, 这样就能保证每一次的执行都是同样的条件。