基于 React 18 讲解 Hooks 原理
本文使用 React 源码版本:18.2.0
整体代码运行的 debug 过程,有录制视频教程,可以配合观看:b23.tv/QyQqyK6
主要讲解常用 react hook 的内部运行机制,状态保存逻辑,以及不同 hook 对象在 Fiber 节点上的挂载方式。通过本文,可以了解到为什么不能中途改变 hook 的使用顺序,以及为什么要使用环状链表保存 effect 和 update 对象等常见 hook 问题。
前置知识点
Fiber 架构
react 16.18.0 版本引入 fiber 架构,实现异步可中断更新。先把 vdom 树转成 fiber 链表,然后再渲染 fiber。主要是解决之前由于直接递归遍历 vdom,不可中断,导致当 vdom 比较大的,频繁调用耗时 dom api 容易产生性能问题。
下面是 fiber 树的示意图:
- reconcile 阶段将 vdom 转换成 fiber,确定节点操作,并创建用到的 DOM
- commit 阶段执行实际 DOM 操作

Fiber 数据结构
主要分下面几块:
- 节点基础信息的描述
- 描述与其它 fiber 节点连接的属性
- 状态更新相关的信息
- 优先级调度相关
这边和 hook 关联比较大的主要是 memoizedState 和 updateQueue 属性。函数组件会将内部用到的所有的 hook 通过单向链表的形式,保存在组件对应 fiber 节点的 memoizedState 属性上。updateQueue 是 useEffect 产生的 effect 连接成的环状单向链表。
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性
this.tag = tag; // Fiber对应组件的类型 Function/Class/Host...
this.key = key; // key属性
this.elementType = null; // 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.type = null; // 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
this.stateNode = null; // Fiber对应的真实DOM节点
// 用于连接其他Fiber节点形成Fiber树
this.return = null; // 指向父级Fiber节点
this.child = null; // 指向子Fiber节点
this.sibling = null; // 指向右边第一个兄弟Fiber节点
this.index = 0;
this.ref = null;
// 作为动态的工作单元的属性 —— 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null; // class 组件 Fiber 节点上的多个 Update 会组成链表并被包含在 fiber.updateQueue 中。 函数组件则是存储 useEffect 的 effect 的环状链表。
this.memoizedState = null; // hook 组成单向链表挂载的位置
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}
Hook 数据结构
hook 的 memoizedState 存的是当前 hook 自己的值。
const hook: Hook = {
memoizedState: null, // 当前需要保存的值
baseState: null,
baseQueue: null, // 由于之前某些高优先级任务导致更新中断,baseQueue 记录的就是尚未处理的最后一个 update
queue: null, // 内部存储调用 setValue 产生的 update 更新信息,是个环状单向链表
next: null, // 下一个hook
};
不同类型hook
的memoizedState
保存不同类型数据,具体如下:
-
useState:对于
const [state, updateState] = useState(initialState)
,memoizedState
保存state
的值 -
useEffect:
memoizedState
保存包含useEffect回调函数
、依赖项
等的链表数据结构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
函数的执行结果。
示例
可以从截图中看到,代码中使用的 useState 和 useRef 两个 hook 通过 next 连接成链表。另外 useState 的 hook 对象的 queue 中存储了调用 setValue 时用到的函数。
function App() {
const [value, setValue] = useState(0);
const ref = useRef();
ref.current = "some value";
return (
<div className="App">
<h1>目前值:{value}</h1>
<div>
<button onClick={() => {
setValue(v => v + 1)
}}>增加</button>
</div>
</div>
);
}

Hooks 链表创建过程
每个 useXxx 的 hooks 都有 mountXxx 和 updateXxx 两个阶段。链表只创建一次,在 mountXxx 当中,后面都是 update。
mountXxx 阶段代码:HooksDispatcherOnMountInDEV 代码地址
以 useState 为例,mount 时会进入 HooksDispatcherOnMountInDEV
的 useState
方法,最终执行 mountState
:
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 内部会创建当前 hook 的 hook 对象,不同 useXXX 的差异主要就在 mountXXX 函数里面,每种 hooks api 都有不同的使用 hook.memorizedState
数据的逻辑,后面会介绍几个重点的。

mountWorkInProgressHook
是个通用方法,所有 hook 都会执行**,通过它新建 hook 对象,如果前面没有hook 对象,就将该 hook 挂到当前 fiber 节点的 memoizedState上面,否则接到前一个 hook 对象的 next 上,构成单向链表。**

为什么不能在循环、条件或嵌套函数中调用 Hooks?
同样的问题是“为什么不能改变 hook 的执行顺序?”
通过上面介绍已经知道各个 hook 在 mount 时会以链表的形式挂到 fiber.memoizedState
上。
update 时会进入到 HooksDispatcherOnUpdateInDEV
,执行不同 hook 的 updateXxx 方法。
updateXxx 阶段代码:HooksDispatcherOnUpdateInDEV 代码地址
HooksDispatcherOnUpdateInDEV = {
...
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDev = 'useState';
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return updateState(initialState);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
...
};
最终会通过 updateWorkInProgressHook
方法获取当前 hook 的对象,获取方式就是从当前 fiber.memoizedState
上依次获取,遍历的是 mount 阶段创建的链表,故不能改变 hook 的执行顺序,否则会拿错。(updateWorkInProgressHook
也是个通用方法,updateXXX 都是走到这个地方)
// Hooks are stored as a linked list on the fiber's memoizedState field. The
// current hook list is the list that belongs to the current fiber. The
// work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
let currentHook: Hook | null = null; // 当前在执行的 hook 对象
let workInProgressHook: Hook | null = null;
...
function updateWorkInProgressHook(): Hook {
let nextCurrentHook: null | Hook;
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
// 刚开始更新,从 fiber.memoizedState 获取第一个 hook 对象
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 如果不是,则获取链表中的下一个 hook
nextCurrentHook = currentHook.next;
}
...
return workInProgressHook;
}
具体 hook
useRef
- mount 时:把传进来的 value 包装成一个含有 current 属性的对象,然后放在
memorizedState
属性上。 - update 时:直接返回,没做特殊处理


对于设置了 ref 的节点,什么时候 ref 值会更新?
组件在 commit 阶段的 mutation 阶段执行 DOM 操作,所以对应 ref 的更新也是发生在 mutation 阶段。
useCallback
- mount 时:在 memorizedState 上放了一个数组,第一个元素是传入的回调函数,第二个是传入的 deps。
- update 时:更新的时候把之前的那个 memorizedState 取出来,和新传入的 deps 做下对比,如果没变,那就返回之前的回调函数,否则返回新传入的函数。


比对是依赖项是否一致的时候,用的是
Object.is
:Object.is() 与 === 不相同。差别是它们对待有符号的零和 NaN 不同,例如,=== 运算符(也包括 == 运算符)将数字 -0 和 +0 视为相等,而将 Number.NaN 与 NaN 视为不相等。
![]()
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
// is() 用的是 Object.is,只是多了些兼容代码
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
useMemo
代码位置:mountMemo、updateMemo
和 useCallback 大同小异。
- mount 时:在 memorizedState 上放了个数组,第一个元素是传入函数的执行结果,第二个元素是 deps。
- update 时:取出之前的 memorizedState,和新传入的 deps 做下对比,如果没变,就返回之前的值。如果变了,创建一个新的数组放在 memorizedState,第一个元素是新传入函数的执行结果,第二个元素是 deps。


useEffect
useLayoutEffect 在 mount 和 update 这块和 useEffect 差不多,就不展开讲了。
mount 时和 update 时涉及的主要方法都是 pushEffect
,update 时判断依赖是否变化的原理和useCallback 一致。像上面提到的 memoizedState 存的是创建的 effect 对象的环状链表。

pushEffect
的作用:是创建 effect 对象,并将组件内的 effect 对象串成环状单向链表,放到fiber.updateQueue
上面。即 effect 除了保存在 fiber.memoizedState 对应的 hook 中,还会保存在 fiber 的 updateQueue 中。
function pushEffect(tag, create, destroy, deps) {
// 创建 effect 对象
var effect = {
tag: tag, // effect的类型,区分是 useEffect 还是 useLayoutEffect
create: create, // 传入use(Layout)Effect函数的第一个参数,即回调函数
destroy: destroy, // 销毁函数
deps: deps, // 依赖项
// Circular
next: null
};
// 获取 fiber 的 updateQueue
var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
// 如果前面没有 effect,则将componentUpdateQueue.lastEffect指针指向effect环状链表的最后一个
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 如果前面已经有 effect,将当前生成的 effect 插入链表尾部
var firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
// 把最后收集到的 effect 放到 lastEffect 上面
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
function createFunctionComponentUpdateQueue() {
return {
lastEffect: null,
stores: null
};
}
hook 内部的 effect 主要是作为上次更新的 effect,为本次创建 effect 对象提供参照(对比依赖项数组),updateQueue 的 effect 链表会作为最终被执行的主体,带到 commit 阶段处理。即 fiber.updateQueue
会在本次更新的 commit 阶段中被处理,其中 useEffect 是异步调度的,而 useLayoutEffect
的 effect 会在 commit 的 layout 阶段同步处理。等到 commit 阶段完成,更新应用到页面上之后,开始处理 useEffect 产生的 effect,简单说:
- useEffect 是异步调度,等页面渲染完成后再去执行,不会阻塞页面渲染。
- uselayoutEffect 是在 commit 阶段新的 DOM 准备完成,但还未渲染到屏幕前,同步执行。
为什么如果不把依赖放到 deps,useEffect 回调执行的时候拿的会是旧值?
从 updateEffectImpl
的逻辑可以看出来,effect 对象只有在 deps 变化的时候才会重新生成,也就保证了,如果不把依赖的数据放到 deps 里面,用的 effect.create
还是上次更新时的回调,函数内部用到的依赖自然就还是上次更新时的。即不是 useEffect 特意将回调函数内部用到的依赖存下来,而是因为,用的回调函数就是上一次的,自然也是从上一次的上下文中取依赖值,除非把依赖加到 deps 中,重新获取回调函数。
依照这个处理方式也就能了解到:对于拿对象里面的值的情况,如果对象放在组件外部,或者是通过 useRef 存储,即使没有把对象放到 deps 当中,也能拿到最新的值,因为 effect.create
拿的只是对象的引用,只要对象的引用本身没变就行。
useState
代码位置:mountState、updateState
- mount 时:将初始值存放在
memoizedState
中,queue.pending
用来存调用 setValue(即 dispath)时创建的最后一个 update ,是个环状链表,最终返回一个数组,包含初始值和一个由dispatchState
创建的函数。
为什么要是环状链表?—— 在获取头部或者插入尾部的时候避免不必要的遍历操作
(上面提到的 fiber.updateQueue 、 useEffect 创建的 hook 对象中的 memoizedState 存的 effect 环状链表,以及 useState 的 queue.pending 上的 update 对象的环状链表,都是这个原因)
方便定位到链表的第一个元素。updateQueue 指向它的最后一个 update,updateQueue.next 指向它的第一个update。
若不使用环状链表,updateQueue 指向最后一个元素,需要遍历才能获取链表首部。即使将updateQueue指向第一个元素,那么新增update时仍然要遍历到尾部才能将新增的接入链表。
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 = {
pending: null, // update 形成的环状链表
interleaved: null, // 存储最后的插入的 update
lanes: NoLanes,
dispatch: null, // setValue 函数
lastRenderedReducer: basicStateReducer, // 上一次render时使用的reducer
lastRenderedState: initialState // 上一次render时的state
};
hook.queue = queue;
var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
- update 时:可以看到,其实调用的是
updateReducer
,只是 reducer 是固定好的,作用就是用来直接执行 setValue(即 dispath) 函数传进来的 action,即 useState 其实是对 useReducer 的一个封装,只是 reducer 函数是预置好的。

updateReducer 主要工作:
-
将 baseQueue 和 pendingQueue 首尾合并形成新的链表
-
baseQueue 为之前因为某些原因导致更新中断从而剩下的 update 链表,pendingQueue 则是本次产生的 update链表。会把 baseQueue 接在 pendingQueue 前面。
-
从 baseQueue.next 开始遍历整个链表执行 update,每次循环产生的 newState,作为下一次的参数,直到遍历完整个链表。即整个合并的链表是先执行上一次更新后再执行新的更新,以此保证更新的先后顺序。
-
最后更新 hook 上的参数,返回 state 和 dispatch。
function updateReducer(reducer, initialArg, init) {
var hook = updateWorkInProgressHook();
// hook.queue.pending 指向update环转链表的最后一个update,即链表尾部
var queue = hook.queue;
queue.lastRenderedReducer = reducer;
var current = currentHook; // The last rebase update that is NOT part of the base state.
// 由于之前某些高优先级任务导致更新中断,baseQueue 记录的就是尚未处理的最后一个 update
var baseQueue = current.baseQueue; // The last pending update that hasn't been processed yet.
// 当前 update 链表最后一个 update
var pendingQueue = queue.pending;
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
if (baseQueue !== null) {
// 合并 baseQueue 和 pendingQueue,baseQueue 排在 pendingQueue 前面
var baseFirst = baseQueue.next;
var pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
// 合并后的 update 链表不为空时开始循环整个 update 链表计算新 state
if (baseQueue !== null) {
// We have a queue to process.
var first = baseQueue.next;
var newState = current.baseState; // useState hook当前的state
var newBaseState = null;
var newBaseQueueFirst = null;
var newBaseQueueLast = null;
var update = first;
do {
var updateLane = update.lane;
...
if (update.hasEagerState) {
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = update.eagerState;
} else {
// 取得当前的update的action,可能是函数也可能是具体的值
var action = update.action;
newState = reducer(newState, action);
}
update = update.next;
} while (update !== null && update !== first);
...
// 把最终得倒的状态更新到 hook上
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
...
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
dispath 调用时做了什么事情?
主要是执行 dispatchSetState
函数,创建本次更新的 update 对象,计算本地更新后的新值,存储到 update.eagerState
中,并把该 update 和之前该 hook 已经产生的 update 连成环状链表。
- 创建 update 对象:
var update = {
lane: lane,
action: action, // 执行的具体数据操作
hasEagerState: false,
eagerState: null, // 依据当前 state 和 action 计算出来的新 state
next: null //指向下一个update的指针
};
- 构建 update 环状链表:如果前面没有 update,则直接自己连自己,如果有update,则将自己插入到原本最后一个 update 与 第一个 update 之间,并将自己赋值给存储最后一个 update 的
queue.interleaved
dispatchSetState
的作用:
function dispatchSetState(fiber, queue, action) {
// 创建 update
var update = {
lane: lane,
action: action,
hasEagerState: false,
eagerState: null,
next: null
};
// 是否在渲染阶段更新
if (isRenderPhaseUpdate(fiber)) {
// 将 update 存到 queue.pending 当中
enqueueRenderPhaseUpdate(queue, update);
} else {
...
var lastRenderedReducer = queue.lastRenderedReducer;
...
// 计算当前 reducer 下生成的 state
var currentState = queue.lastRenderedState;
var eagerState = lastRenderedReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.hasEagerState = true;
update.eagerState = eagerState;
// 将新增的 update 插入 update 链表尾部并返回 root 节点
var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
var eventTime = requestEventTime();
// 执行调度方法,实现更新
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane);
}
// 将新增的 update 插入 update 链表尾部
function enqueueConcurrentHookUpdate(fiber, queue, update, lane) {
var interleaved = queue.interleaved;
if (interleaved === null) {
// This is the first update. Create a circular list.
update.next = update; // At the end of the current render, this queue's interleaved updates will
// be transferred to the pending queue.
pushConcurrentUpdateQueue(queue);
} else {
update.next = interleaved.next;
interleaved.next = update;
}
queue.interleaved = update;
return markUpdateLaneFromFiberToRoot(fiber, lane);
}
// 将 update 存到 queue.pending 当中
function enqueueRenderPhaseUpdate(queue, update) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
var pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
简单实现
详细讲解原文地址:极简 hook 实现
涵盖了dispath、创建 update、形成 update 环状链表、更新时遍历整个 update 链表、通过 action 计算新 state 的大概逻辑。
let workInProgressHook;
let isMount = true; // 是mount还是update。
const fiber = {
memoizedState: null, // 保存该FunctionComponent对应的Hooks链表
stateNode: App
};
function schedule() {
/*
更新前将workInProgressHook重置为fiber保存的第一个Hook,
workInProgressHook变量指向当前正在工作的hook,
在组件render时,每当遇到下一个useState,我们移动workInProgressHook的指针。
这样,只要每次组件render时useState的调用顺序及数量保持一致,那么始终可以通过workInProgressHook找到当前useState对应的hook对象。
*/
workInProgressHook = fiber.memoizedState;
// 触发组件render
const app = fiber.stateNode();
// 组件首次render为mount,以后再触发的更新为update
isMount = false;
return app;
}
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();
}
function useState(initialState) {
let hook; // 当前useState使用的hook会被赋值该该变量
if (isMount) {
// mount时为该useState生成hook
hook = {
// 保存update的queue,即上文介绍的queue
queue: {
pending: null // 始终指向最后一个插入的 update,是一个环状单向链表
},
// 保存hook对应的state
memoizedState: initialState,
// 与下一个Hook连接形成单向无环链表
next: null
}
// 将hook插入fiber.memoizedState链表末尾
if (!fiber.memoizedState) {
fiber.memoizedState = hook;
} else {
workInProgressHook.next = hook;
}
workInProgressHook = hook; // 移动workInProgressHook指针
} else {
// update时从workInProgressHook中取出该useState对应的hook
hook = workInProgressHook; // update时找到对应hook
workInProgressHook = workInProgressHook.next; // 移动workInProgressHook指针
}
let baseState = hook.memoizedState; // update执行前的初始state
if (hook.queue.pending) {
// 根据queue.pending中保存的update更新state
let firstUpdate = hook.queue.pending.next; // 获取update环状单向链表中第一个update
do {
// 执行update action
const action = firstUpdate.action;
baseState = action(baseState);
firstUpdate = firstUpdate.next;
// 最后一个update执行完后跳出循环
} while (firstUpdate !== hook.queue.pending)
hook.queue.pending = null; // 清空queue.pending
}
hook.memoizedState = baseState; // 将update action执行完后的state作为memoizedState
return [baseState, dispatchAction.bind(null, hook.queue)];
}
function App() {
const [num, updateNum] = useState(0);
console.log(`${isMount ? 'mount' : 'update'} num: `, num);
return {
click() {
updateNum(num => num + 1);
}
}
}
window.app = schedule();
自定义 hook
自定义 hook 和直接在组件内使用自定义 hook 中用到的 hook,形成的fiber.memoizedState
hook 链表结构一致,没啥特殊的,不做过多描述。
参考
转载自:https://juejin.cn/post/7119102104337121316