前言
2021.11.7 立冬 , 北京昨晚一夜雨夹雪,气温骤然下降,闲着无事,看一看Hooks的源码吧
Hooks源码量并不大,相信很多小伙伴在网上都了解过Hooks的大概实现。看别人的文章,只能体会到九牛一毛。
古人云:’纸上得来终觉浅 绝知此事要躬行’, 那我们就从源码开始,揭开Hooks神秘的面纱吧!
React版本:17.0.2
调试源码见:github React源码调试demo
前置知识
如果你对Hooks的基本使用以及链表的基本知识还不清楚,请移步:
- React Hooks基本使用 React Hooks
- 链表相关 算法-链表
本文默认你已经对Fiber架构、双缓冲等概念比较了解,如果不了解,需要补课了 Fiber架构。
Hooks疑问
在使用Hooks的时候,大家可能都会有疑问,为什么Hooks不可以在条件语句中使用呢?Class组件可以将状态挂载到实例上,Hooks本质上就是函数,那么它是怎么记住状态state的呢?函数式组件有那么多Hooks,他是怎么管理区分Hooks的呢?下面罗列了我在使用过程中的一些疑问,本文也是围绕Hooks的源码,针对这些疑问一一解答:
- 1 在无状态组件每一次函数上下文执行的时候,react用什么方式记录了hooks的状态?
- 2 多个hooks用什么来记录每一个hooks的顺序的 ? 换个问法!为什么不能条件语句中,声明hooks? hooks声明为什么在组件的最顶部?
- 3 function函数组件中的useState,和 class类组件 setState有什么区别?
- 4 react 是怎么捕获到hooks的执行上下文,是在函数组件内部的?
- 5 useEffect,useMemo 中,为什么useRef不需要依赖注入,就能访问到最新的改变值?
- 6 useMemo是怎么对值做缓存的?如何应用它优化性能?
- 7 为什么两次传入useState的值相同,函数组件不更新?
- 8 useState和useReducer如何在每次渲染时,返回最新的值?
- 9 useRef是如何在函数式组件重新渲染的时候,能访问到最新的值?
- 10 为什么useEffect的回调不能用async?
- …
源码探索之旅
什么是Hook?
我们使用的useState、useEffect、useMemo、useRef等都会生成一个Hook对象,Hook类型定义在
my-debug-react/src/react/packages/react-reconciler/src/ReactFiberHooks.old.js文件中
hook定义
export type Hook = {|
memoizedState: any, // 保持在内存中的局部状态 -上次渲染之后的state
baseState: any, // 通过处理好的update,计算出来的state
baseQueue: Update<any, any> | null,// 存储update对象的环形链表, 只包括高于本次渲染优先级的update对象.
queue: UpdateQueue<any, any> | null,// 存储update对象的环形链表, 包括所有优先级的update对象
next: Hook | null,//指向下一个Hook
|};
无论是挂载还是更新,都会产生hook,产生的hook对象依次排列,形成链表存储到函数组件fiber.memoizedState上。在这个过程中,有一个十分重要的指针:workInProgressHook,它通过记录当前生成(更新)的hook对象,可以间接反映在组件中当前调用到哪个hook函数了。每调用一次hook函数,就将这个指针的指向移到该hook函数产生的hook对象上。
不同的hook方法,存贮在memoizedState 中的信息不一样,
useState/useReducer | state |
---|---|
useEffect/useLayoutEffect | effect对象 |
useMemo/usecallback | [callback,deps] |
这里有个大概印象,后续会对每种的useXX进行梳理,自然就明白了
useState
我们都知道useState是useReducer的阉割版,我们就先从useState开始探索
useState、useEffect、useRef等Hooks是从 my-debug-react/src/react/packages/react/index.js 包中导出,沿着这条线往下看,找到了useState的位置
my-debug-react/src/react/packages/react/src/ReactHooks.js
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
console.log('====> useState',initialState)
return dispatcher.useState(initialState);
}
从源码中可以看到,我们调用的其实是 ReactCurrentDispatcher.js 中的dispatcher.useState(),那什么是dispatcher呢?沿着文件路径找下去 my-debug-react/src/react/packages/react-reconciler/src/ReactInternalTypes.js
export type Dispatcher = {|
getCacheForType?: <T>(resourceType: () => T) => T,
readContext<T>(context: ReactContext<T>): T,
useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S,
): [S, Dispatch<A>],
useContext<T>(context: ReactContext<T>): T,
useRef<T>(initialValue: T): {|current: T|},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
useCallback<T>(callback: T, deps: Array<mixed> | void | null): T,
useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T,
...
|};
这里我们可以看到,不同的useXX会执行对应Dispatch里的dispatch。
useState入口
类组件的setState和函数组件的useState有什么区别呢?前文中我们已经介绍过,useState在异步处理方面的差异。
从之前的源码梳理中 我们知道, 调用顺序是 beginWork —> updateFunctionComponent —> renderWithHooks, 这个函数是FunctionComponent的render主函数。
//my-debug-react/src/react/packages/react-reconciler/src/ReactFiberHooks.old.js
// ...省略无关代码
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
// --------------- 1. 设置全局变量 -------------------
renderLanes = nextRenderLanes; // 当前渲染优先级
currentlyRenderingFiber = workInProgress; // 当前fiber节点, 也就是function组件对应的fiber节点
// 清除当前fiber的遗留状态
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
// --------------- 2. 调用function,生成子级ReactElement对象 -------------------
// 指定dispatcher, 区分mount和update
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 执行function函数, 其中进行分析Hooks的使用
let children = Component(props, secondArg);
// --------------- 3. 重置全局变量,并返回 -------------------
// 执行function之后, 还原被修改的全局变量, 不影响下一次调用
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
return children;
}
- 调用function前: 设置全局变量, 标记渲染优先级和当前fiber, 清除当前fiber的遗留状态.
- 调用function: 构造出Hooks链表, 最后生成子级ReactElement对象(children).
- 调用function后: 重置全局变量, 返回children.调用Component(props, secondArg);执行函数组件,hooks被依次执行,把hooks信息依次保存到workInProgress树上
- 为了保证不同的function节点在调用时renderWithHooks互不影响, 所以退出时重置全局变量.
看21行,
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
workInProgress.memoizedState: 在class组件中,memoizedState存放state信息,在function组件中,memoizedState在一次调和渲染过程中,以链表的形式存放hooks信息。 useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象
HooksDispatcherOnMount 和 HooksDispatcherOnUpdate,它们内部的hooks函数是不同的实现,区别之一在于不同阶段对于hooks链表的处理是不同的。初次挂载时要创建链表,后续更新的时候要更新链表
挂载阶段
挂载的时候,执行线路 HooksDispatcherOnMount => mountState => mountWorkInProgress
初次挂载时,组件上没有任何hooks的信息,所以,这个过程主要是在fiber上创建hooks链表。挂载调用的是mountWorkInProgressHook,它会创建hook并将他们连接成链表,同时更新workInProgressHook,最终返回新创建的hook,也就是hooks链表。
// react-reconciler/src/ReactFiberHooks.js
function mountState (initialState) {
// 1. 创建hook
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
// 2. 初始化hook的属性
// 2.1 设置 hook.memoizedState/hook.baseState
// 2.2 设置 hook.queue
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer,
lastRenderedState,
});
// 2.3 设置 hook.dispatch
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
)));
// 3. 返回[当前状态, dispatch函数]
return [hook.memoizedState, dispatch];
}
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) {
// 当前workInProgressHook链表为空的话,
// 将当前Hook作为第一个Hook
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// 否则将当前Hook添加到Hook链表的末尾
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
注释:currentlyRenderingFiber就是workInProgress节点
可以看到,mount的时候,主要:
1. 创建hook节点
2.将hook节点添加到hook链表上
3.将hook链表挂载到fiberNode的 memoizedState 上
来吧 我们看一下demo
function HooksDemo() {
const [name, setName] = React.useState("Guan");
const [age, setAge] = React.useState(18);
const [color, setColor] = React.useState("red");
const handleChange = () => {
setName(name + "~qing");
setName(name + "~chao");
};
const handleChangeAge = () => {
setAge(100);
setAge(200);
setAge(300);
};
return (
<div>
Hooks
<div>This is name:{name} </div>
<div>This is age:{age} </div>
<div>My favourite Color is: {color}</div>
<br />
<br />
<br />
<button onClick={handleChange}>点击改变Name</button>
<button onClick={handleChangeAge}>点击改变Age</button>
</div>
);
}
本demo中我们定义了多个状态,打印看下hook
可以看到,mount的时候,
- 每次调用useState的时候,都会生成一条hook记录
- 多次调用useState的时候会形成一条hook链表
本示例中创建了3个hook,3个hook通过next进行连接形成hook链表, 如下:
继续看源码,moutState的时候,会返回当前的状态和改变状态的dispatch,继续看dispatchAction
更新阶段
对于更新过程中,由于存在current树,所以workInProgress节点也就有对应的current节点。
对于一次函数式组件的更新,当再次执行hooks函数的时候,例如useState(‘Joy’)
- 首先从current的hooks中找到与当前workInProgressHook对应的 currentHook,复制一份给workInProgressHook
- hooks函数执行的时候,把最新的状态更新到workInProgressHook,保证hooks状态不丢失
current树中对应的Hook指针为 currentHook,复用currentHook来创建新的hook对象
当调用setName、setAge的时候会触发dispatchAction
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
// 1. 创建update对象
const eventTime = requestEventTime();
const lane = requestUpdateLane(fiber); // Legacy模式返回SyncLane
const update: Update<S, A> = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
// 2. 将update对象添加到hook.queue.pending队列
const pending = queue.pending;
if (pending === null) {
// 首个update, 创建一个环形链表
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
console.log('Hooks ====> dispatchAction =====>',queue,update);
const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
// 渲染时更新, 做好全局标记
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
} else {
// ...省略性能优化部分
if (is(eagerState, currentState)) {
return;
}
// 3. 发起调度更新, 进入`reconciler 运作流程`中的输入阶段.
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
}
逻辑十分清晰了
- 创建update对象, 其中update.lane代表优先级,每一次dispatch都会生成一个update对象
- 将update对象添加到hook.queue.pending环形链表.
- 环形链表的特征: 为了方便添加新元素和快速拿到队首元素(都是O(1)), 所以pending指针指向了链表中最后一个元素. (稍后图解)
- 发起调度更新: 调用scheduleUpdateOnFiber, 进入reconciler 运作流程中的输入阶段.
看37行,React会根据最新的state和上一次的state进行浅比较,如果相同,就会跳过。
这就证实了为什么useState,两次值相等的时候,组件不渲染的原因了,这个机制和Component模式下的setState有一定的区别。
if (is(eagerState, currentState)) {
return;
}
我们先来点击本例子中【点击改变Name】
demo1:
setName(‘qing’)
demo2:
setName(‘qing’),setName(‘chao’)
注:多次执行dispatch,由于调度中心优化,最后只会执行一次渲染
demo3:
【点击改变Age】触发3次dispatch setAge(100),setAge(200),setAge(300)
在fiber树构造(对比更新)过程中,再次调用function,调用路线 HooksDispatcherOnUpdate => updateState => updateReducer
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 1. 获取workInProgressHook对象
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
let baseQueue = current.baseQueue;
// 2. 链表拼接: 将 hook.queue.pending 拼接到 current.baseQueue
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
// 3. 状态计算
if (baseQueue !== null) {
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateLane = update.lane;
// 3.1 优先级提取update
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// 优先级不够: 加入到baseQueue中, 等待下一次render
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else {
// 优先级足够: 状态合并
if (newBaseQueueLast !== null) {
// 更新baseQueue
const clone: Update<S, A> = {
lane: NoLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
if (update.eagerReducer === reducer) {
// 性能优化: 如果存在 update.eagerReducer, 直接使用update.eagerState.避免重复调用reducer
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
// 调用reducer获取最新状态
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
// 3.2. 更新属性
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
// 把计算之后的结果更新到workInProgressHook上
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
updateWorkInProgress主要功能是取出保存在当前fiber的hooks链表中对应的hook节点。
function updateWorkInProgressHook(): Hook {
// 1. 移动currentHook指针
let nextCurrentHook: null | Hook;
if (currentHook === null) { /* 如果 currentHook = null 证明它是第一个hooks */
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else { /* 不是第一个hooks,那么指向下一个 hooks */
nextCurrentHook = currentHook.next;
}
// 2. 移动workInProgressHook指针
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) { //第一次执行hooks
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// 渲染时更新: 本节不讨论
} else {
currentHook = nextCurrentHook;
// 3. 克隆currentHook作为新的workInProgressHook.
// 随后逻辑与mountWorkInProgressHook一致
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null, // 注意next指针是null
};
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
代码有点长,总结下
- 调用updateWorkInProgressHook获取workInProgressHook对象,updateWorkInProgressHook的逻辑很清晰,主要是让currentHook和WIPHook两个指针同时向后移动(图解如上),复用当前currentHook
- 链表拼接: 将 hook.queue.pending 拼接到 current.baseQueue
- 状态计算
- update优先级不够: 加入到 baseQueue 中, 等待下一次 render
- update优先级足够: 状态合并
- 更新属性
Talk is cheap, show me the picture
执行updateReducer之前
放大pendingQueue部分
链表拼接
状态计算
循环baseState中的update,依次执行update中的action
如果是setAge(100),那么只用更新值
如果是setAge(age=>age+100),就传入上一次state更新的值
useEffect/useLayoutEffect
挂载阶段
mount阶段,useEffect执行mountEffect,useEffectLayout执行mountLayoutEffect
二者内部都会调用mountEffectImpl,参数不一样
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
UpdateEffect | PassiveEffect, // fiberFlags
HookPassive, // hookFlags
create,
deps,
);
}
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
UpdateEffect, // fiberFlags
HookLayout, // hookFlags
create,
deps,
);
}
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 1. 创建hook
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 2. 设置workInProgress的副作用标记
currentlyRenderingFiber.flags |= fiberFlags; // fiberFlags 被标记到workInProgress
// 2. 创建Effect, 挂载到hook.memoizedState上
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags, // hookFlags用于创建effect
create, //effect中的回调函数
undefined,
nextDeps,//依赖
);
}
mountEffectImpl逻辑:
- 创建hook
- 设置workInProgress的副作用标记: flags |= fiberFlags
- 创建effect(在pushEffect中), 挂载到hook.memoizedState上, 即 hook.memoizedState = effect
创建effect,挂载updateQueue
function pushEffect(tag, create, destroy, deps) {
// 1. 创建effect对象
const effect: Effect = {
tag,
create,
destroy,
deps,
next: (null: any),
};
// 2. 把effect对象添加到环形链表末尾
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) { //如果是第一个useEffect
// 新建 workInProgress.updateQueue 用于挂载effect对象
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
// updateQueue.lastEffect是一个环形链表
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
// 3. 返回effect
return effect;
}
pushEffect逻辑:
- 创建effect.
- 把effect对象添加到环形链表末尾.
- 返回effect.
effect数据结构
export type Effect = {|
tag: HookFlags, //effect副作用的类型
create: () => (() => void) | void, //传入useEffect的函数
destroy: (() => void) | void,
deps: Array<mixed> | null, // 依赖
next: Effect,
|};
export const NoFlags = /* */ 0b000;
export const HasEffect = /* */ 0b001; // 有副作用, 可以被触发
export const Layout = /* */ 0b010; // Layout, dom突变后同步触发
export const Passive = /* */ 0b100; // Passive, dom突变前异步触发
3种flag分别会在commit提价阶段处理对应的useEffect的回调 这里先不展开详细说,后续我们专门写篇文章探究
mount阶段和useState的处理一样,会根据Hook形成Hook链表,之前的demo中只有状态hook,没有副作用的hook
如图,mount阶段创建了hooks链表,如果有effect hook,将创建effect list环形链表,effect list会存贮在2个地方:
- use(Layout)Effect对应hook元素的memoizedState中。
- fiber.updateQueue中,本次更新的updateQueue,它会在本次更新的commit阶段中被处理。
effect list
作为拓展介绍下effect list, 可以理解为是一个存储 effectTag 副作用列表容器。它是由 fiber 节点和指针 nextEffect 构成的单链表结构,这其中还包括第一个节点 firstEffect ,和最后一个节点 lastEffect。
React 采用深度优先搜索算法,在 render 阶段遍历 fiber 树时,把每一个有副作用的 fiber 筛选出来,最后构建生成一个只带副作用的 effect list 链表。 在 commit 阶段,React 拿到 effect list 数据后,通过遍历 effect list,并根据每一个 effect 节点的 effectTag 类型,执行每个effect,从而对相应的 DOM 树执行更改。
更新阶段
更新阶段执行 updateEffect => updateEffectImpl
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 1. 获取当前hook
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
// 2. 分析依赖
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
// 继续使用先前effect.destroy
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 比较依赖是否变化
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 2.1 如果依赖不变, 新建effect(tag不含HookHasEffect)
pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 2.2 如果依赖改变, 更改fiber.flag, 新建effect
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
update阶段判断依赖deps,是否变化
如果没有变化就会被打上NoHookEffect标签,最后会在commit阶段跳过改effect的执行。
如果依赖有变化,打上 HookHasEffect | hookEffectTag, 然后在commit阶段,react会通过标签来判断,是否执行当前的 effect 函数。
小结
useEffect做了什么?
- useEffect会生成一个effect对象,保存在hook节点的memoizedState中,同时也push到fiber节点的updateQueue里,组成循环链表。
每次render时,都会对比一下cur hook和wip hook里保存的effect的deps有没有改变,如果改变了,那就更新memoizedState为最新的effect,并且把effect的tag打上HasHookEffect的标记,然后push到fiber的updateQueue里。
- 在commit阶段,beforeMutation中,对有副作用的fiber,发起一个异步调度。等到layout结束后,这个异步调度的回调开始执行,处理effect的创建和销毁回调。它会先调用effect的destroy,再调用create。(这部分单独更新)
useMemo
初始化useMemo,同useState、useEffect一样,先创建Hook,然后执行useMemo中的第一个参数,将执行后的数值,和依赖保存在memoizedState中。function mountMemo<T>( nextCreate: () => T, deps: Array<mixed> | void | null, ): T { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; }
在组件更新过程中 判断两次 deps是否相等,function updateMemo( nextCreate, deps, ) { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; // 新的 deps 值 const prevState = hook.memoizedState; if (prevState !== null) { if (nextDeps !== null) { const prevDeps = prevState[1]; // 之前保存的 deps 值 if (areHookInputsEqual(nextDeps, prevDeps)) { //判断两次 deps 值 return prevState[0]; } } } const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; }
如果不相等,证明依赖项发生改变,那么执行 useMemo的第一个函数,得到新的值,然后重新赋值给hook.memoizedState;
如果相等 证明没有依赖项改变,那么直接获取缓存的值。定制化缓存,存值取值而已。
注意,nextCreate()执行,如果里面引用了useState等信息,变量会被引用,无法被垃圾回收机制回收,就是闭包原理,那么访问的属性有可能不是最新的值,所以需要把引用的值,添加到依赖项 dep 数组中。每一次dep改变,重新执行,就不会出现问题了。
useCallback
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
初始化useCallback,同useState、useEffect一样,先创建Hook,然后中将回调函数和依赖保存在memoizedState中。
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
在组件更新过程中 判断两次 deps是否相等,
如果不相等,返回相应的回调和依赖
如果相等,直接返回上次的回调
useRef
function mountRef(initialValue) {
const hook = mountWorkInProgressHook();
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
useRef的初始化很简单,创建Hook,将值保存在current的属性中,然后将ref保存到memoizedState中
function updateRef(initialValue){
const hook = updateWorkInProgressHook()
return hook.memoizedState
}
函数组件更新useRef做的事情更简单,就是返回了缓存下来的值,也就是无论函数组件怎么执行,执行多少次,hook.memoizedState内存中都指向了一个对象
所以解释了useEffect,useMemo 中,为什么useRef不需要依赖注入,就能访问到最新的改变值。
useContext(TODO)
https://github.com/7kms/react-illustration-series/blob/master/docs/main/context.md
总结
无论是哪种Hook,在挂载阶段都会创建Hook,不同useXXX将不同信息保存在hook.memoized
中,多个Hook通过next行程链表
在更新过程中,找到当前fiber的hooks链表中对应的hook,进行对应的更新。
Q1: React 如何管理区分Hooks
通过链表来管理Hooks,按照Hooks的执行顺序依次添加到链表中
Q2: 为什么不能在条件语句等中使用Hooks?
主要原因是Hooks链表!
根据上面篇幅中的源码介绍,这张图也就不难理解了。
挂载阶段,将会形成一个Hooks链表,其中包含StateHook、EffectHook、MemoHook等,每个Hook中存贮的memoizedState信息不一样,如图。
在更新阶段,通过updateWorkInProgress从current memoizedState的Hooks链表中取出对应的hook,如果hook放到了if条件语句中,会导致链表结构被破坏,导致更新操作发生异常。
Q3: useState和useReducer如何在每次渲染时,返回最新的值
每个Hook节点通过循环链表记录所有的更新操作,在update阶段会依次执行update循环链表中的所有更新操作,最终拿到最新的state返回
References
https://juejin.cn/post/6844904080758800392
https://juejin.cn/post/6954352486312312845#heading-6
https://juejin.cn/post/6844904080758800392#heading-5
https://react.iamkasong.com/hooks/prepare.html
https://github.com/7kms/react-illustration-series/blob/master/docs/main/hook-summary.md
一文吃透Hooks原理