1. 工作原理
先从最简单的Hooks:useState
看起。
它的工作原理很好理解:
-
通过一些途径产生更新,更新会造成组件render。
-
组件render时useState返回的num为更新后的结果。
function App() {
const [num, setNum] = useState(0);
return <p onClick={() => setNum(num => num + 1)}>{num}</p>;
}
其中步骤1的更新可以分为mount和update:
-
调用ReactDOM.render会产生
mount
的更新,更新内容为useState的initialValue
。 -
点击p标签触发setNum会产生一次
update
的更新,更新内容为num => num + 1
。
接下来讲解这两个步骤如何实现。
2. 函数式组件中的Update
更新该怎么理解,已经在前一篇博客讲得很清楚了,不管是Update
本身还是储存它的Update queue
。
但是存在一个问题,不同于ClassComponent
的实例可以存储数据,对于FunctionComponent
,queue存储在哪里呢?
答案是:FunctionComponent
对应的fiber中。
我们使用如下精简的fiber结构:
// App组件对应的fiber对象
const fiber = {
// 保存该FunctionComponent对应的Hooks链表
memorizedState: null,
// 指向App函数
stateNode: App
};
3. Hook数据结构
接下来我们关注fiber.memorizedState
这一链表中保存的Hook
的数据结构。
Hook与update类似,都通过链表连接。不过Hook是无环的单向链表。
hook = {
// 保存update的queue,即上文介绍的queue
queue: {
pending: null
},
// 保存hook对应的state
memorizedState: initialState,
// 与下一个Hook连接形成单向无环链表
next: null
}
注意区分fiber里的memorizedState和hook里的memorizedState
所以实际上的结构是这样的:
每个useState
对应一个hook对象。
调用const [num, setNum] = useState(0)
时setNum
产生的update
保存在useState
对应的hook.queue
中。
4. 模拟React调度更新流程
在setNum函数的末尾,我们会触发schedule()
模拟React调度更新流程。
我们用isMount
变量指代是mount还是update。
// 首次render时是mount
isMount = true;
function schedule() {
// 更新前将workInProgressHook重置为fiber保存的第一个Hook
workInProgressHook = fiber.memoizedState;
// 触发组件render
fiber.stateNode();
// 组件首次render为mount,以后再触发的更新为update
isMount = false;
}
在组件render时,每当遇到下一个useState
,我们移动workInProgressHook
的指针。
workInProgressHook = workInProgressHook.next;
这样,只要每次组件render时useState
的调用顺序及数量保持一致,那么始终可以通过workInProgressHook
找到当前useState
对应的hook
对象。
5. 模拟useState
先来看大体代码,还是比较好理解的,留意对照着上面的hook数据结构看即可。
function useState(initialState) {
// 当前useState使用的hook会被赋值该该变量
let hook;
if (isMount) {
// ...mount时需要生成hook对象
} else {
// ...update时从workInProgressHook中取出该useState对应的hook
}
let baseState = hook.memoizedState;
if (hook.queue.pending) {
// ...根据queue.pending中保存的update更新state
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}
5.1 获取到hook对象
完善上面大体代码中的mount时生成hook和update时获取hook的代码:
if (isMount) {
// mount时为该useState生成hook
hook = {
queue: {
pending: null
},
memoizedState: initialState,
next: null
}
// 将hook插入fiber.memoizedState链表末尾
if (!fiber.memoizedState) {
fiber.memoizedState = hook;
} else {
workInProgressHook.next = hook;
}
// 移动workInProgressHook指针
workInProgressHook = hook;
} else {
// update时找到对应hook
hook = workInProgressHook;
// 移动workInProgressHook指针
workInProgressHook = workInProgressHook.next;
}
5.2 根据Update更新State
接下来再完善如果hook.queue.pending
不为空(有更新)的情况下,如何根据queue中的Update更新State:
// update执行前的初始state
let baseState = hook.memoizedState;
if (hook.queue.pending) {
// 获取update环状单向链表中第一个update
let firstUpdate = hook.queue.pending.next;
do {
// 执行update action
const action = firstUpdate.action;
baseState = action(baseState);
firstUpdate = firstUpdate.next;
// 最后一个update执行完后跳出循环
} while (firstUpdate !== hook.queue.pending.next);
// 清空queue.pending
hook.queue.pending = null;
}
// 将update action执行完后的state作为memoizedState
hook.memoizedState = baseState;
6. 总结
这样一来就实现了一个简洁版的useState,但是和正式的useState相比,这个实现出来的有以下不足:
-
React Hooks没有使用isMount变量,而是在不同时机使用不同的dispatcher。换言之,mount时的useState与update时的useState不是同一个函数。
-
React Hooks有中途跳过更新的优化手段。
-
React Hooks有batchedUpdates,当在click中触发三次updateNum,精简React会触发三次更新,而React只会触发一次。
-
React Hooks的update有优先级概念,可以跳过优先级低的update。