理念
我们知道,React的架构遵循schedule - render - commit的运行流程,这个流程是React世界最底层的运行规律。 ClassComponent作为React世界的原子,他的生命周期(componentWillXXX/componentDidXXX)是为了介入React的运行流程而实现的更上层抽象,这么做是为了方便框架使用者更容易上手。 相比于ClassComponent的更上层抽象,Hooks则更贴近React内部运行的各种概念(state | context | life-cycle)。 作为使用React技术栈的开发者,当我们初次学习Hooks时,不管是官方文档还是身边有经验的同事,总会拿ClassComponent的生命周期来类比Hooks API的执行时机。 这固然是很好的上手方式,但是当我们熟练运用Hooks时,就会发现,这两者的概念有很多割裂感,并不是同一抽象层次可以互相替代的概念。 比如:替代componentWillReceiveProps的Hooks是什么呢? 可能有些同学会回答,是useEffect,但是componentWillReceiveProps是在render阶段执行,而useEffect是在commit阶段完成渲染后异步执行。 所以,从源码运行规律的角度看待Hooks,可能是更好的角度。这也是为什么上文说Hooks是React世界的电子而不是原子的原因。 Concurrent Mode是React未来的发展方向,而Hooks是能够最大限度发挥Concurrent Mode潜力的Component构建方式。
工作原理
对于useState Hook,考虑如下例子:
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。接下来讲解这两个步骤如何实现。
更新是什么
例子中,更新可以抽象为如下数据结构:
const update = {
// 更新执行的函数
action,
// 与同一个Hook的其他更新形成链表
next: null
}
对于App来说,点击p标签产生的update的action为num => num + 1。 如果我们改写下App的onClick:
// 之前
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是如何组合在一起呢? 答案是:他们会形成环状单向链表。 调用updateNum实际调用的是dispatchAction.bind(null, hook.queue),我们先来了解下这个函数:
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 当产生第二个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可以照着这个例子模拟插入多个update的情况,会发现queue.pending始终指向最后一个插入的update。 这样做的好处是,当我们要遍历update时,queue.pending.next指向第一个插入的update。
queue.pending = u1 ---> u0
^ |
| |
---------
如何保持状态
现在我们知道,更新产生的update对象会保存在queue中。 不同于ClassComponent的实例可以存储数据,对于FunctionComponent,queue存储在哪里呢? 答案是:FunctionComponent对应的fiber中。 我们使用如下精简的fiber结构:
// App组件对应的fiber对象
const fiber = {
// 保存该FunctionComponent对应的Hooks链表
memoizedState: null,
// 指向App函数
stateNode: App
};
Hook数据结构
接下来我们关注fiber.memoizedState中保存的Hook的数据结构。 可以看到,Hook与update类似,都通过链表连接。不过Hook是无环的单向链表。
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调度更新流程
function dispatchAction(queue, action) {
// ...创建update
// ...环状单向链表操作
// 模拟React开始调度更新
schedule();
}
现在我们来实现他。 我们用isMount变量指代是mount还是update。
// 首次render时是mount
isMount = true;
function schedule() {
// 更新前将workInProgressHook重置为fiber保存的第一个Hook
workInProgressHook = fiber.memoizedState;
// 触发组件render
fiber.stateNode();
// 组件首次render为mount,以后再触发的更新为update
isMount = false;
}
通过workInProgressHook变量指向当前正在工作的hook。
workInProgressHook = fiber.memoizedState;
在组件render时,每当遇到下一个useState,我们移动workInProgressHook的指针。
workInProgressHook = workInProgressHook.next;
这样,只要每次组件render时useState的调用顺序及数量保持一致,那么始终可以通过workInProgressHook找到当前useState对应的hook对象。 到此为止,我们已经完成第一步。
-
通过一些途径产生更新,更新会造成组件render。接下来实现第二步。
-
组件render时useState返回的num为更新后的结果。## 计算state 组件render时会调用useState,他的大体逻辑如下:
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对象:
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。
// 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;
这样基本就是useState的一套概念了
hook的数据结构
在上文的极简useState实现中,使用isMount变量区分mount与update。 在真实的Hooks中,组件mount时的hook与update时的hook来源于不同的对象,这类对象在源码中被称为dispatcher。
// mount时的Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
// ...省略
};
// update时的Dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
// ...省略
};
可见,mount时调用的hook和update时调用的hook其实是两个不同的函数。 在FunctionComponent render前,会根据FunctionComponent对应fiber的以下条件区分mount与update。
current === null || current.memoizedState === null// 这个早在之前的章节里提到过
并将不同情况对应的dispatcher赋值给全局变量ReactCurrentDispatcher的current属性。
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
在FunctionComponent render时,会从ReactCurrentDispatcher.current(即当前dispatcher)中寻找需要的hook。 换言之,不同的调用栈上下文为ReactCurrentDispatcher.current赋值不同的dispatcher,则FunctionComponent render时调用的hook也是不同的函数。
一个dispatcher使用场景
当错误的书写了嵌套形式的hook,如:
useEffect(() => {
useState(0);
})
此时ReactCurrentDispatcher.current已经指向ContextOnlyDispatcher,所以调用useState实际会调用throwInvalidHookError,直接抛出异常。
export const ContextOnlyDispatcher: Dispatcher = {
useCallback: throwInvalidHookError,
useContext: throwInvalidHookError,
useEffect: throwInvalidHookError,
useImperativeHandle: throwInvalidHookError,
useLayoutEffect: throwInvalidHookError,
// ...省略
结构
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
其中除memoizedState以外字段的意义与上一章介绍的updateQueue类似
memoizedState
需要注意的是,hook与FunctionComponent fiber都存在memoizedState属性,不要混淆他们的概念。
-
fiber.memoizedState:FunctionComponent对应fiber保存的Hooks链表。
-
hook.memoizedState:Hooks链表中保存的单一hook对应的数据。不同类型hook的memoizedState保存不同类型数据,具体如下:
-
useState:对于const [state, updateState] = useState(initialState),memoizedState保存state的值
-
useReducer:对于const [state, dispatch] = useReducer(reducer, {});,memoizedState保存state的值
-
useEffect:memoizedState保存包含useEffect回调函数、依赖项等的链表数据结构effect,你可以在这里(opens new window)看到effect的创建过程。effect链表同时会保存在fiber.updateQueue中
-
useRef:对于useRef(1),memoizedState保存{current: 1}
-
useMemo:对于useMemo(callback, [depA]),memoizedState保存[callback(), depA]
-
useCallback:对于useCallback(callback, [depA]),memoizedState保存[callback, depA]。与useMemo的区别是,useCallback保存的是callback函数本身,而useMemo保存的是callback函数的执行结果有些hook是没有memoizedState的,比如:
-
useContext 参考&转载:React技术揭秘