Appearance
为了更好理解Hooks
原理,这一节我们遵循React
的运行流程,实现一个不到100行代码的极简useState Hook
。建议对照着代码来看本节内容。
工作原理
对于useState Hook
,考虑如下例子:
js
function App() {
const [num, updateNum] = useState(0);
return <p onClick={() => updateNum(num => num + 1)}>{num}</p>;
}
可以将工作分为两部分:
通过一些途径产生
更新
,更新
会造成组件render
。组件
render
时useState
返回的num
为更新后的结果。
其中步骤1
的更新
可以分为mount
和update
:
调用
ReactDOM.render
会产生mount
的更新
,更新
内容为useState
的initialValue
(即0
)。点击
p
标签触发updateNum
会产生一次update
的更新
,更新
内容为num => num + 1
。
接下来讲解这两个步骤如何实现。
更新是什么
- 通过一些途径产生
更新
,更新
会造成组件render
。
首先我们要明确更新
是什么。
在我们的极简例子中,更新
就是如下数据结构:
js
const update = {
// 更新执行的函数
action,
// 与同一个Hook的其他更新形成链表
next: null
}
对于App
来说,点击p
标签产生的update
的action
为num => num + 1
。
如果我们改写下App
的onClick
:
js
// 之前
return <p onClick={() => updateNum(num => num + 1)}>{num}</p>;
// 之后
return <p onClick={() => {
updateNum(num => num + 1);
updateNum(num => num + 1);
updateNum(num => num + 1);
}}>{num}</p>;
那么点击p
标签会产生三个update
。
update数据结构
这些update
是如何组合在一起呢?
答案是:他们会形成环状单向链表
。
调用updateNum
实际调用的是dispatchAction.bind(null, hook.queue)
,我们先来了解下这个函数:
js
function dispatchAction(queue, action) {
// 创建update
const update = {
action,
next: null
}
// 环状单向链表操作
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
// 模拟React开始调度更新
schedule();
}
环状链表操作不太容易理解,这里我们详细讲解下。
当产生第一个update
(我们叫他u0
),此时queue.pending === null
。
update.next = update;
即u0.next = u0
,他会和自己首尾相连形成单向环状链表
。
然后queue.pending = update;
即queue.pending = u0
js
queue.pending = u0 ---> u0
^ |
| |
---------
当产生第二个update
(我们叫他u1
),update.next = queue.pending.next;
,此时queue.pending.next === u0
, 即u1.next = u0
。
queue.pending.next = update;
,即u0.next = u1
。
然后queue.pending = update;
即queue.pending = u1
js
queue.pending = u1 ---> u0
^ |
| |
---------
你可以照着这个例子模拟插入多个update
的情况,会发现queue.pending
始终指向最后一个插入的update
。
这样做的好处是,当我们要遍历update
时,queue.pending.next
指向第一个插入的update
。
状态如何保存
现在我们知道,更新
产生的update
对象会保存在queue
中。
不同于ClassComponent
的实例可以存储数据,对于FunctionComponent
,queue
存储在哪里呢?
答案是:FunctionComponent
对应的fiber
中。
我们使用如下精简的fiber
结构:
js
// App组件对应的fiber对象
const fiber = {
// 保存该FunctionComponent对应的Hooks链表
memoizedState: null,
// 指向App函数
stateNode: App
};
Hook数据结构
接下来我们关注fiber.memoizedState
中保存的Hook
的数据结构。
可以看到,Hook
与update
类似,都通过链表
连接。不过Hook
是无环
的单向链表
。
js
hook = {
// 保存update的queue,即上文介绍的queue
queue: {
pending: null
},
// 保存hook对应的state
memoizedState: initialState,
// 与下一个Hook连接形成单向无环链表
next: null
}
注意
注意区分update
与hook
的所属关系:
每个useState
对应一个hook
对象。
调用const [num, updateNum] = useState(0);
时updateNum
(即上文介绍的dispatchAction
)产生的update
保存在useState
对应的hook.queue
中。
模拟React调度更新流程
在上文dispatchAction
末尾我们通过schedule
方法模拟React
调度更新流程。
js
function dispatchAction(queue, action) {
// ...创建update
// ...环状单向链表操作
// 模拟React开始调度更新
schedule();
}
现在我们来实现他。
我们用isMount
变量指代是mount
还是update
。
js
// 首次render时是mount
isMount = true;
function schedule() {
// 更新前将workInProgressHook重置为fiber保存的第一个Hook
workInProgressHook = fiber.memoizedState;
// 触发组件render
fiber.stateNode();
// 组件首次render为mount,以后再触发的更新为update
isMount = false;
}
通过workInProgressHook
变量指向当前正在工作的hook
。
js
workInProgressHook = fiber.memoizedState;
在组件render
时,每当遇到下一个useState
,我们移动workInProgressHook
的指针。
js
workInProgressHook = workInProgressHook.next;
这样,只要每次组件render
时useState
的调用顺序及数量保持一致,那么始终可以通过workInProgressHook
找到当前useState
对应的hook
对象。
到此为止,我们已经完成第一步。
- 通过一些途径产生
更新
,更新
会造成组件render
。
接下来实现第二步。
- 组件
render
时useState
返回的num
为更新后的结果。
计算state
组件render
时会调用useState
,他的大体逻辑如下:
js
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)];
}
我们首先关注如何获取hook
对象:
js
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;
}
当找到该useState
对应的hook
后,如果该hook.queue.pending
不为空(即存在update
),则更新其state
。
js
// 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;
完整代码如下:
js
function useState(initialState) {
let hook;
if (isMount) {
hook = {
queue: {
pending: null
},
memoizedState: initialState,
next: null
}
if (!fiber.memoizedState) {
fiber.memoizedState = hook;
} else {
workInProgressHook.next = hook;
}
workInProgressHook = hook;
} else {
hook = workInProgressHook;
workInProgressHook = workInProgressHook.next;
}
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = action(baseState);
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending.next)
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}
对触发事件进行抽象
最后,让我们抽象一下React
的事件触发方式。
通过调用App
返回的click
方法模拟组件click
的行为。
js
function App() {
const [num, updateNum] = useState(0);
console.log(`${isMount ? 'mount' : 'update'} num: `, num);
return {
click() {
updateNum(num => num + 1);
}
}
}
在线Demo
至此,我们完成了一个不到100行代码的Hooks
。重要的是,他与React
的运行逻辑相同。
精简Hooks的在线Demo
调用window.app.click()
模拟组件点击事件。
你也可以使用多个useState
。
js
function App() {
const [num, updateNum] = useState(0);
const [num1, updateNum1] = useState(100);
console.log(`${isMount ? 'mount' : 'update'} num: `, num);
console.log(`${isMount ? 'mount' : 'update'} num1: `, num1);
return {
click() {
updateNum(num => num + 1);
},
focus() {
updateNum1(num => num + 3);
}
}
}
与React的区别
我们用尽可能少的代码模拟了Hooks
的运行,但是相比React Hooks
,他还有很多不足。以下是他与React Hooks
的区别:
React Hooks
没有使用isMount
变量,而是在不同时机使用不同的dispatcher
。换言之,mount
时的useState
与update
时的useState
不是同一个函数。React Hooks
有中途跳过更新
的优化手段。React Hooks
有batchedUpdates
,当在click
中触发三次updateNum
,精简React
会触发三次更新,而React
只会触发一次。React Hooks
的update
有优先级
概念,可以跳过不高优先的update
。
更多的细节,我们会在本章后续小节讲解。