React Hooks 原理分析

hooks 出现的原因
我们知道在React V16.8版本之前React组件主要由类组件和无状态组件组成,而两者之间存在明显的差异:类组件可以使用状态(State)和生命周期钩子(Lifecycle Hook),但不能使用无状态(Functional)组件的优点——易于使用和定义。在React项目开发中,类组件不仅仅难于理解和阅读,而且操作和维护也很困难,代码量比较大和代码重复。而无状态组件优点在于简单明了,易于测试和重复使用,但又缺乏状态处理的能力。因此为了弥补无状态组件没有生命周期,没有数据管理状态的缺陷,React Hooks就应运而生了。
什么是 hooks 以及 hooks 解决了什么问题。
在React 16.8版本中Hooks作为React的一种全新特性,它可以让开发者在不编写类组件的情况下,使用 React 的一系列功能。Hooks 是以函数组件为基础开发的,它使函数式组件具有类组件一样的能力,包括状态管理、生命周期钩子和副作用等。使用函数式组件代替类组件解决了以下几个问题。
- 原来类组件的状态难以复用,必须使用高阶组件
(HOC)等方式进行解决,导致代码复杂。使用Hooks以后,可以通过自定义Hooks将状态送进封装,不再需要使用HOC等方式进行复用。 - 类组件的生命周期方法繁杂,必须写多个生命周期函数才能完成一些操作。使用
Hooks后,可以根据需要使用不同的Hooks来替代某些生命周期函数,比如使用useEffect来替代componentDidMount等生命周方法。 - 类组件中的
this指向问题必须时刻关注着。在函数组中,没有this和生命周方法的概念,也不必使用bind()来绑定this,这样代码更简单明了。
hooks 原理与执行时机
从我上篇 一文读懂react Fiber 可知,在Render阶段会从rootFiber节点开始向下深度优先遍历,每个fiber节点调用 beginWork 方法,其中摘录项目代码如下:
function beginWork(current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const updateLanes = workInProgress.lanes;
// 根据tag类型不同执行不同 action
switch (workInProgress.tag) {
// .......
case FunctionComponent: {
// .....
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
}
}
这里我们重点看FunctionComponent类型下执行了updateFunctionComponent方法,通过点击查看 源码 ,同样我们取其中有关代码如下:
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderLanes,
) {
let context;
let nextChildren;
prepareToReadContext(workInProgress, renderLanes);
// 调用renderWithHooks方法
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
// ......
// return workInProgress.child;
}
通过查看updateFunctionComponent方法,我们知道该方法最终调用了renderWithHooks,下面我们来看看 renderWithHooks 方法主要做了什么事情,同样其中主要源码如下:
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
// 渲染优先级
renderLanes = nextRenderLanes;
// 当前渲染节点
currentlyRenderingFiber = workInProgress;
// 重置 workInProgress 节点中的 memoizedState 等状态信息
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
// 设置当前 dispatcher,根据 current 和 memoizedState 判断是初次渲染或者更新调用不同的 dispatcher
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 调用Component函数执行函数组件
let children = Component(props, secondArg);
// 检查RenderPhase, 是否是render阶段触发的更新,防止无限循环重复更新。
if (didScheduleRenderPhaseUpdateDuringThisPass) {
// .....;
}
// 设置 React 当前的 dispatcher 为只包含上下文的 dispatcher。
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
// 当前渲染状态
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
// 重置相关参数
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
return children;
}
该方法接受如下几个参数:
current:当前fiber节点,初始化时为nullworkInProgress:正在处理的fiber节点Component:函数组件本身props:组件属性secondArg:组件属性以外的其他参数,可以是null或者省略nextRenderLanes:渲染优先级
实现逻辑大致如下:
- 保存渲染所需的状态信息,包括当前渲染
Fiber节点(currentlyRenderingFiber)、渲染优先级(renderLanes)、Hooks链表(currentHook、workInProgressHook)、是否进行过RenderPhase更新(didScheduleRenderPhaseUpdate)等。 - 重置
workInProgress节点中的状态信息,并根据当前状态设置React当前的dispatcher(当前正在处理哪个优先级的任务)。 - 调用待渲染组件的函数,并将其结果保存在
children变量中。 - 判断是否需要进行后续的渲染操作(如出现了
RenderPhase更新)。 - 设置
React当前的dispatcher,处理只包含上下文的dispatcher。 - 根据当前状态判断是否渲染了全部
Hook函数。 - 重置渲染所需的状态信息,以准备进行下一次的渲染。
- 返回执行结果
需要注意的是:
- 这里通过判断
current树上是否存在memoizedState信息来确认是初次渲染还是更新,以便调用不同的Dispatcher。
// 判断 mount 与 update
current === null || current.memoizedState === null
// mount时的Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
// ......
};
// update时的Dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
// ....
};
表明不同的调用栈上下文为ReactCurrentDispatcher.current赋值不同的dispatcher,函数组件 render 时调用的hook也是不同的函数。当然在React中还有其他dispatcher,如有兴趣可以点击 这里 查看。
2. 其次这里通过调用Component函数来执行函数组件,函数组件在这里被正式执行。
3. 通过ContextOnlyDispatcher对象来判断hooks是否包含在上下文里面,否则抛出异常。以错误调用两个Hook为例:
useEffect(() => {
useState(1);
},[])
此时ContextOnlyDispatcher 格式如下:
const ContextOnlyDispatcher = {
useState:throwInvalidHookError,
useEffect: throwInvalidHookError,
// ....
}
function throwInvalidHookError() {
// ......
invariant(
//....
);
}
这时的ReactCurrentDispatcher.current已经指向ContextOnlyDispatcher,所以调用useState实际会调用throwInvalidHookError,直接抛出异常。
接下来我们来分别看下函数组件mount时和update时不同阶段内部数据结构和具体流程。分析这两个阶段之前我们先了解一下hook数据结构,源码点击 这里。
const hook: Hook = {
memoizedState: null, // 单一hook对应的数据
baseState: null, // 最新state值
baseQueue: null, // 基础更新队列
queue: null, // 待更新队列
next: null, // 指向下一个hooks指针对象
};
这里需要注意的是memoizedState属性,不同的hook类型保存不同的数据信息。
useState、useReducer:memoizedState保存的为state的值。useEffect:memoizedState保存的useEffect回调函数、依赖项等的链表数据结构effect。useReducer:格式如const [state, dispatch] = useReducer(reducer, {});中memoizedState保存state的值。useRef:memoizedState中保存的{current: xxx}。useMemo:格式如useMemo(callback, dep[A])中,memoizedState保存的是[callback(), depA]。useCallback:格式如useCallback(callback, dep[A]),memoizedState保存[callback, depA]。与useMemo的区别:useCallback保存的是回调函数本身,而useMemo保存的是回调函数执行的结果。useContext无memoizedState属性。
注意:
FunctionComponent fiber也存在memoizedState属性,两者不能混淆。fiber.memoizedState:FunctionComponent对应fiber保存的Hooks链表。hook.memoizedState:Hooks链表中保存的单一hook对应的数据。
接下来我们分别看看几个常用的API(useState,useEffect,useReducer,useRef,useMeomo,useCallback)在mount阶段和update阶段不同的处理流程。
常用API不同阶段处理流程
在分析常用API之前我们得知每个API在初始化阶段都会调用 mountWorkInProgressHook 函数,在更新阶段会调用 updateWorkInProgressHook 函数,下面我们先分别来看看这两个函数做了什么。
-
mountWorkInProgressHook
function mountWorkInProgressHook(): Hook { // 创建hook对象 const hook: Hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null, }; // 通过判断workInProgressHook是否存在来设置hook对象数据 if (workInProgressHook === null) { // 设置当前正在渲染的fiber节点memoizedState属性并给hook对象赋值 currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { // hook对象添加到链表的末尾,同时设置workInProgressHook和hook对象的next属性 workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }该函数的功能是创建一个
hook对象,并将其添加到Fiber节点的链表中,用于记录组件状态或执行副作用。如果链表中没有任何hook对象,则将其添加为第一个hook对象;如果已经存在其他hook对象,则将其添加到链表的末尾。函数返回创建的hook对象。具体的hook对象属性代表的含义我们上述已经提前说过了。
-
updateWorkInProgressHook
function updateWorkInProgressHook(): Hook { // 下一个当前hook指针 let nextCurrentHook: null | Hook; // 是否是第一个hook if (currentHook === null) { const current = currentlyRenderingFiber.alternate; if (current !== null) { nextCurrentHook = current.memoizedState; } else { nextCurrentHook = null; } } else { // 指向下一个hook nextCurrentHook = currentHook.next; } // 下一个nextWorkInProgressHook let nextWorkInProgressHook: null | Hook; if (workInProgressHook === null) { // 当前渲染节点的memoizedState nextWorkInProgressHook = currentlyRenderingFiber.memoizedState; } else { // 赋值当前workInProgressHook链表中的下一个hook nextWorkInProgressHook = workInProgressHook.next; } //是否存在 nextWorkInProgressHook if (nextWorkInProgressHook !== null) { // 存在则直接复用,将当前workInProgressHook指向下一个hook workInProgressHook = nextWorkInProgressHook; nextWorkInProgressHook = workInProgressHook.next; // currentHook 指向下一个hook currentHook = nextCurrentHook; } else { // 克隆新hook对象 currentHook = nextCurrentHook; const newHook: Hook = { memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, baseQueue: currentHook.baseQueue, queue: currentHook.queue, next: null, }; // 是否是链表的第一个hook if (workInProgressHook === null) { currentlyRenderingFiber.memoizedState = workInProgressHook = newHook; } else { // 将新hook添加到workInProgressHook链表中 workInProgressHook = workInProgressHook.next = newHook; } } // 返回 return workInProgressHook; }总的来说,这个函数的主要作用是为当前进行的
fiber节点创建一个workInProgressHook,并返回该hook对象。具体实现逻辑如下:- 定义了变量
nextCurrentHook用于存储下一个当前 hook 的指针,其中如果当前 hook 不为 null,则将其指向下一个 hook,否则将它指向当前正在渲染的fiber节点的备用节点上的memoizedState(lastRenderedState)。 - 定义了变量
nextWorkInProgressHook用于存储下一个workInProgressHook,其中如果workInProgressHook不为null,则将其指向当前workInProgressHook链表中的下一个 hook,否则将其指向当前fiber节点的第一个 hook 对象(memoizedState)。 - 如果下一个
workInProgressHook已经存在,则直接复用它,并将nextCurrentHook和workInProgressHook的指针指向下一个 hook。如果不存在下一个workInProgressHook,则需要从当前 hook 对象克隆一个新的 hook 对象,并添加到workInProgressHook链表的末尾。如果这是链表中的第一个 hook,则将其赋值给currentlyRenderingFiber.memoizedState。 - 最后将
workInProgressHook对象返回,以供下一次使用。如果没有发生错误,则workInProgressHook对象就会包含渲染或更新过程中要使用的所有 hook 对象。
- 定义了变量
useState
源码如下:
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 当前环境的dispatcher,不通的场景获取不同的dispatch对象
const dispatcher = resolveDispatcher();
// 当前的函数组件中创建一个新的状态,并返回一个数组对象
return dispatcher.useState(initialState);
}
-
mount
mount时对于useState,会调用 mountState 方法。function mountState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] { // 创建当前hook对象 const hook = mountWorkInProgressHook(); if (typeof initialState === 'function') { // 第一次参数为函数时执行函数得到返回值 initialState = initialState(); } hook.memoizedState = hook.baseState = initialState; // 创建queue const queue = (hook.queue = { pending: null, dispatch: null, lastRenderedReducer: basicStateReducer, // 最新state lastRenderedState: (initialState: any), // 最后一次的state }); // 创建更新函数并返回 const dispatch: Dispatch< BasicStateAction<S>, > = (queue.dispatch = (dispatchAction.bind( null, currentlyRenderingFiber, queue, ): any)); return [hook.memoizedState, dispatch]; }初始化时,
useState会首先获取当前的hook对象,然后根据传入的initialState,设置hook对象的memoizedState和baseState为初始值,同时为hook对象创建更新队列queue。
-
update
update时会调用 updateState 方法。function updateState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] { return updateReducer(basicStateReducer, (initialState: any)); }阅读该方法发现方法内部会调用
updateReducer方法,接下来我们看看updateReducer方法内部做了什么,源码如下:function updateReducer<S, I, A>( reducer: (S, A) => S, // 纯函数 initialArg: I, // 初始值 init?: I => S, // 可选参数 ): [S, Dispatch<A>] { const hook = updateWorkInProgressHook(); // 创建更新hook对象 const queue = hook.queue; // 更新队列 // 处理错误 ...... // 将当前reducer标记为lastRenderedReducer queue.lastRenderedReducer = reducer; const current: Hook = (currentHook: any); // 基础更新队列信息 let baseQueue = current.baseQueue; // 当前待处理的更新队列 const pendingQueue = queue.pending; if (pendingQueue !== null) { if (baseQueue !== null) { // 与当前baseQueue进行合并为一个新的基础队列 const baseFirst = baseQueue.next; const pendingFirst = pendingQueue.next; baseQueue.next = pendingFirst; pendingQueue.next = baseFirst; } // ....... current.baseQueue = baseQueue = pendingQueue; queue.pending = null; } // 处理基础对列 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 suspenseConfig = update.suspenseConfig; const updateLane = update.lane; // 优先级 const updateEventTime = update.eventTime; if (!isSubsetOfLanes(renderLanes, updateLane)) { // 创建clone对象,里面包含一些属性值 const clone: Update<S, A> = { // ...... }; 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) { // 创建副本 const clone: Update<S, A> = { // ........ }; // 将当前更新对象追加到新队列尾部 newBaseQueueLast = newBaseQueueLast.next = clone; } // 标记更新对象的 eventTime 和 suspenseConfig 属性 markRenderEventTimeAndConfig(updateEventTime, suspenseConfig); // 更新对象的 eagerReducer 属性等于当前的 reducer 函数,直接赋值 if (update.eagerReducer === reducer) { newState = ((update.eagerState: any): S); } else { // reducer函数计算新状态 const action = update.action; newState = reducer(newState, action); } } // 下一个更新对象 update = update.next; // 如果 update 不为空且不等于第一个更新对象,则继续进行循环 } while (update !== null && update !== first); if (newBaseQueueLast === null) { newBaseState = newState; } else { // 新基础状态队列的首元素链接到队列尾部 newBaseQueueLast.next = (newBaseQueueFirst: any); } // 比较新状态与memoizedState if (!is(newState, hook.memoizedState)) { markWorkInProgressReceivedUpdate(); } // 更新hook对象的属性值 hook.memoizedState = newState; hook.baseState = newBaseState; hook.baseQueue = newBaseQueueLast; queue.lastRenderedState = newState; } // 更新函数 const dispatch: Dispatch<A> = (queue.dispatch: any); // 最后返回memoizedState值与dispatch函数 return [hook.memoizedState, dispatch]; }源码比较长,但仔细阅读发现该函数主要是实现
hooks状态更新的逻辑。该函数接收一个reducer函数和一个初始化参数initArg,以及可选的初始化参数init,并返回当前memoized状态和一个dispatch函数。具体实现过程如下:- 创建当前更新
Hook对象,再从中取出更新队列queue。将当前reducer标记为lastRenderedReducer。获取当前Hook的memoizedState,如果它是null,则为Hook创建一个初始状态,并将其更新为memoizedState。如果eagerReducer存在并且与当前使用的reducer相同,则使用eagerState来更新currentState。 - 接下来是状态更的过程:获取当前基础状态队列
baseQueue,并检查是否存在等待处理的更新队列pendingQueue。如果存在,则将其与baseQueue合并为一个新的基础状态队列。 - 遍历更新队列中的每个更新,根据其更新优先级及有关状态来判断更新是否是需要被处理。如果更新优先级过低,则将其标记为跳过,并将一个相对应的更新对象放入到新的基础状态队列中。如果更新优先级足够高,则直接将其应用于
currentState,以便更新memoizedState。 - 当更新队列中有更新时,将
memoizedState的值更新为最新的值,同时将hook对象的基础状态(baseState)和基础状态更新队列(baseState updateQueue)属性也更新为新的状态队列。 - 最后返回
memoizedState值以及dispatch函数。
- 创建当前更新
useReducer
源码如下:
export function useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 获取当前环境的dispatcher
const dispatcher = resolveDispatcher();
// 返回当前状态和用于更新状态的函数
return dispatcher.useReducer(reducer, initialArg, init);
}
-
mount
mount时对于useReducer,会调用 mountReducer 方法。function mountReducer<S, I, A>( reducer: (S, A) => S, initialArg: I, init?: I => S, ): [S, Dispatch<A>] { // 创建当前hook对象 const hook = mountWorkInProgressHook(); let initialState; // 赋值初始state if (init !== undefined) { initialState = init(initialArg); } else { initialState = ((initialArg: any): S); } hook.memoizedState = hook.baseState = initialState; // 创建queue const queue = (hook.queue = { pending: null, dispatch: null, lastRenderedReducer: reducer, lastRenderedState: (initialState: any), }); // 创建dispatch更新函数并返回 const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind( null, currentlyRenderingFiber, queue, ): any)); return [hook.memoizedState, dispatch]; }阅读源码可以发现
mount时,useState和useReducer这两个hook的唯一区别为queue参数的lastRenderedReducer字段。其中queue的数据结构如下:const queue = (hook.queue = { pending: null, // 保存dispatchAction.bind()的值 dispatch: null, // 上一次render时使用的reducer lastRenderedReducer: reducer, // 上一次render时的state lastRenderedState: (initialState: any), });其中,
useReducer的lastRenderedReducer为传入的reducer参数。useState的lastRenderedReducer为basicStateReducer。basicStateReducer方法源码如下:function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S { return typeof action === 'function' ? action(state) : action; }可见,
useState即reducer参数为basicStateReducer的useReducer。 -
update
update时对于useReducer则和useState更新时调用的是同一个updateReducer函数,上面已经介绍过了,这里就不做介绍了。
通过以上分析我们知道对于useState、useReducer这两个hook都是通过 dispatchAction 函数来触发更新的。把FunctionComponent对应的fiber以及hook.queue通过调用bind方法作为参数传入,精简如下
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
// 更新相关信息
const eventTime = requestEventTime();
const suspenseConfig = requestCurrentSuspenseConfig();
const lane = requestUpdateLane(fiber, suspenseConfig);
// 创建update对象
const update: Update<S, A> = {
eventTime,
lane,
suspenseConfig,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
// 添加新的更新对象到更新队列中
const pending = queue.pending;
if (pending === null) {
// 队列中没有更新,新的更新对象为唯一更新对象,构建循环列表
update.next = update;
} else {
// 已有更新对象,插入到更新队列到最后
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
// 处理是否需要重新渲染
const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
// 当前节点正在渲染,则标记有新的更新任务
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
} else {
if (
// 当前 fiber 节点及其备份节点暂无待执行的更新任务
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// 获取队列上一次渲染时所使用的reducer
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
// 获取队列上一次渲染时的 state
const currentState: S = (queue.lastRenderedState: any);
// 根据新的 action 计算 eagerState
const eagerState = lastRenderedReducer(currentState, action);
// 存储到新的更新对象上
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
// 对比eagerState 与当前 state 是否相同,相同不需要做额外的操作
return;
}
}
}
}
// 当前 fiber 节点暂无可执行的任务,更新任务
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
}
以上代码实现流程大致如下:
- 获取事件时间、当前
suspense配置、待更新fiber节点及更新队列UpdateQueue、优先级用来创建一个更新对象并添加到队列的末尾。 - 判断当前
fiber节点是否正在被渲染或者其备份节点正在被渲染。如果是的话,标记didScheduleRenderPhaseUpdateDuringThisPass和didScheduleRenderPhaseUpdate为true。 - 如果不是第一次更新且当前
fiber节点没有可用的更新优先级(lane),并且其备份节点也没有可用的更新优先级,则判断队列中最后一次渲染的reducer是否存在,如果有则尝试调用该reducer生成eagerState,并将其与当前状态比较,如果两者相同则不需要更新。 - 如果以上条件都不满足,则调用
scheduleUpdateOnFiber函数,将当前更新任务推到任务队列中等待执行。
useRef
源码如下:
export function useRef<T>(initialValue: T): {|current: T|} {
const dispatcher = resolveDispatcher();
// 返回带有current属性的对象
return dispatcher.useRef(initialValue);
}
-
mount
mount时对于useRef,会调用 mountRef 方法,源码如下:function mountRef<T>(initialValue: T): {|current: T|} { // 创建当前 useRef hook const hook = mountWorkInProgressHook(); // 创建 ref const ref = {current: initialValue}; // ..... // 保存到hook对象的memoizedState属性上 hook.memoizedState = ref; return ref; }mountRef源码非常简单,也是创建一个当前hook对象,然后创建一个ref对象,其中ref对象的current属性用来保存初始化值,保存在hook对象的memoizedState属性上,最后返回这个ref对象。
-
update
update时对于useRef,会调用 updateRef 方法。源码如下:function updateRef<T>(initialValue: T): {|current: T|} { const hook = updateWorkInProgressHook(); // 返回缓存值 return hook.memoizedState; }该
updateRef函数方法内部同样简单,函数接受一个泛型参数initialValue,返回缓存值。其中hook.memoizedState在内存中指向了一个带有current属性的对象,无论函数组件执行多少次,总能访问到最新值。
useMemo
源码如下:
export function useMemo<T>(
create: () => T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
// memoized版本的值。根据deps是否改变重新计算缓存值
return dispatcher.useMemo(create, deps);
}
-
mount
mount时对于useMemo,会调用 mountMemo 方法。function mountMemo<T>( nextCreate: () => T, deps: Array<mixed> | void | null, ): T { // 创建当前hook对象 const hook = mountWorkInProgressHook(); // 判断依赖数组 const nextDeps = deps === undefined ? null : deps; // 调用函数创建memoized值 const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; }该方法接受一个创建函数和依赖数组作为参数,创建一个
memoized值并将该值保存在hook.memoizedState中,最后返回该值。其中通过比较依赖数组的值来减少组件重复渲染的次数,以达到提高性能的目的。
-
update
update时对于useMemo,会调用 updateMemo 方法。function updateMemo<T>( nextCreate: () => T, deps: Array<mixed> | void | null, ): T { const hook = updateWorkInProgressHook(); // 获取新的依赖值 const nextDeps = deps === undefined ? null : deps; // 获取上次保存的memoized值 const prevState = hook.memoizedState; if (prevState !== null) { // memoized值已经存在,根据依赖数组的变化情况来判断是否需要更新memoized值。 if (nextDeps !== null) { // 获取之前保存的deps值 const prevDeps: Array<mixed> | null = prevState[1]; // 判断两次获取的依赖值 if (areHookInputsEqual(nextDeps, prevDeps)) { // 直接返回上次保存的memoized值 return prevState[0]; } } } const nextValue = nextCreate(); // 更新 memoized 值 hook.memoizedState = [nextValue, nextDeps]; return nextValue; }该方法逻辑其实和
mountMemo方法逻辑类似,不同点在于它需要判断memoized值是否需要更新,以及更新时如何处理。都是用于创建memoized值,提高组件的性能和效率。
useCallback
源码如下:
export function useCallback<T>(
callback: T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
// memoized版本的回调函数,在 `deps` 数组中的某一项发生变化时才进行更新。
return dispatcher.useCallback(callback, deps);
}
-
mount
mount时对于useCallback,会调用 mountCallback 方法。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; }可以看到
mount时,useMemo与useCallback的逻辑基本一样,唯一的区别在于:mountMemo会将回调函数(nextCreate)的执行结果作为value保存。而mountCallback会将回调函数作为value保存。
-
update
update时对于useCallback,会调用 updateCallback 方法。function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T { const hook = updateWorkInProgressHook(); // 获取依赖值 const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; // memoized值是否存在 if (prevState !== null) { if (nextDeps !== null) { const prevDeps: Array<mixed> | null = prevState[1]; // 判断两次获取的依赖值 if (areHookInputsEqual(nextDeps, prevDeps)) { // 直接返回上次保存的值 return prevState[0]; } } } // 更新 memoized 值 hook.memoizedState = [callback, nextDeps]; return callback; }可见,对于
update时,useMemo,useCallback的唯一区别也是是回调函数本身还是回调函数的执行结果作为value。
useEffect
源码如下:
export function useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
// 根据deps是否改变处理副作用(异步请求、DOM操作)
return dispatcher.useEffect(create, deps);
}
-
mount
mount时对于useEffect,会调用 mountEffect 方法。function mountEffect( create: () => (() => void) | void, deps: Array<mixed> | void | null, ): void { if (__DEV__) { // ...... } return mountEffectImpl( UpdateEffect | PassiveEffect, HookPassive, create, deps, ); } function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { // 创建当前 hook 对象 const hook = mountWorkInProgressHook(); // 获取依赖值 const nextDeps = deps === undefined ? null : deps; // 根据参数 fiberEffectTag 和 hookEffectTag 计算出当前 effect 的 effectTag(副作用标记)并存储到当前渲染 fiber 节点上。 currentlyRenderingFiber.effectTag |= fiberEffectTag; // pushEffect 用于创建副作用对象,以便后续渲染时被处理,并执行相关的副作用函数以及清除函数 hook.memoizedState = pushEffect( HookHasEffect | hookEffectTag, create, // 副作用函数 undefined, nextDeps, // 依赖数组deps ); }通过以上源码可知
mountEffect方法,最终调用了mountEffectImpl方法。mountEffectImpl方法内部都会创建一个hook对象。然后将该hook对象的memoizedState属性上保存当前副作用对象信息,以便后续渲染时被处理,并执行相关的副作用函数以及清除函数。其中pushEffect方法用于创建副作用对象,根据组件是否初次渲染挂载到workInProgress的updateQueue属性上。然后将副作用(effect)放入updateQueue中,组成一个effect list。感兴趣的可以点击 这里 查看源码。 -
update
update时对于useEffect,会调用 updateEffect 方法。function updateEffect( create: () => (() => void) | void, deps: Array<mixed> | void | null, ): void { if (__DEV__) { // ...... } return updateEffectImpl( UpdateEffect | PassiveEffect, HookPassive, create, deps, ); } function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; let destroy = undefined; // 根据currentHook是否为空来判断上次渲染过程中是否使用了useEffect hook if (currentHook !== null) { const prevEffect = currentHook.memoizedState; // 赋值上次渲染时的清除函数 destroy = prevEffect.destroy; if (nextDeps !== null) { const prevDeps = prevEffect.deps; // 两次deps是否相等,memoized值无需更新 if (areHookInputsEqual(nextDeps, prevDeps)) { pushEffect(hookEffectTag, create, destroy, nextDeps); return; } } } currentlyRenderingFiber.effectTag |= fiberEffectTag; hook.memoizedState = pushEffect( HookHasEffect | hookEffectTag, create, destroy, nextDeps, ); }从以上代码分析
updateEffect方法最终会调用updateEffectImpl方法,与mountEffectImpl函数类似。该方法内部会根据两次的deps是否相等来决定此次更新是否需要执行。如果不相等,则更新effect并且重新赋值hook.memoizedState。
effect list
上面我们提到了effect list概念,接下来我们简要分析下effect list的创建和更新过程:
-
当我们在函数组件内使用
useEffect()或useLayoutEffect()等hook时,函数组件初次render时,React会调用mountEffect()函数来创建effect list。- 在
mountEffect()中,使用createEffect()函数创建一个副作用对象,包括effectTag、create(副作用函数)、destroy(清除函数)和依赖数组等信息。 - 副作用对象会被添加到一个单向链表中,而这个链表实际上就是
effect list。
- 在
-
当我们使用
useEffect()或useLayoutEffect()等hook时,如果传入的依赖数组发生了变化,函数组件会re-render,React会调用updateEffect()函数来更新effect list。- 在
updateEffect()中,React首先获取到上次render时保存的副作用对象(即memoizedState),然后比较当前的依赖数组是否等于上次的依赖数组。如果依赖数组没有变化,React 会跳过这个副作用对象。 - 如果依赖数组发生了变化,
React会调用createEffect()函数来创建一个新的副作用对象,并将其添加到effect list中。
- 在
-
在组件
render之后,React会执行effect list中保存的所有副作用。-
在
commit 阶段,React遍历effect list,并根据effectTag的不同,执行对应的副作用:- 如果
effectTag包含Unmount,则表示该副作用需要在组件卸载之前执行。 - 如果
effectTag包含Layout,则表示该副作用需要在commit阶段同步执行。 - 如果
effectTag包含Passive,则表示该副作用是在浏览器空闲时异步执行。
- 如果
-
在执行副作用时,
React会调用副作用函数create。create函数即是我们自定义传入useEffect()或useLayoutEffect()的函数。副作用函数可能会返回一个清除函数(即destroy 函数),这个清除函数会在如下情况下被调用- 组件
unmount时; - 下一次
commit时,因为组件re-render导致有新的副作用产生; - 当前副作用链表中,出现一个或多个
effectTag包含一个Update标志位的副作用,意味着它引用了上一个render输出中的某些值,在当前render输出中可能失效。
- 组件
-
简单来说 effect list 是 React 中用于存储组件副作用的一种数据结构。它是一个数组,存储了当前组件所有需要执行的副作用。React 通过 effect list 统一管理组件的副作用信息,并在需要执行副作用时,通过 effectTag 判断执行时机,确保副作用的正确性。在组件初次 mount 和 re-render 时,React 会创建和更新 effect list。在执行副作用时,React 会调用副作用函数 create,并根据返回值中是否包含清除函数 destroy,来判断该副作用是否需要清理和再次执行。
为什么不能在条件语句中调用hook
以下面代码为例:
import React, {useState, useEffect, useRef, useMemo, useCallback} from 'react';
import {Button} from 'antd';
const App = () => {
const [num, setNum] = useState(0);
const divRef = useRef(null);
const memoValue = useMemo(() => 'i am jackbtone', []);
const memoizedFunc = useCallback(() => {
console.log('func 函数被执行')
}, [num])
useEffct(() => {
console.log('组件开始执行......')
}, [])
return <>
<div ref={divRef}>
<span>{num}</span>
{memoValue}
<Button onClick{() => setNum(num + 1)}>{num}</Button>
<Button onClick{() => memoizedFunc()}>触发</Button>
</div>
</>
}
当函数组件被执行之后,我们通过以上分析可知,上面的五个hook被依次执行,执行各自hook对应的dispatch后会形成如下图所示的链表关系。
从上图可以看出在函数组件执行后,调用各自hook的dispatch执行mountWorkInProgressHook 方法后,形成了一个完整的hooks链表,通过next指针指向下一个hook。
假设我们在上面的代码中加入条件循环。
if(isMount) {
const memoValue = useMemo(() => 'i am jackbtone', []);
}
此时通过一次更新后会出现如下图所示

通过上图我们可以知道在组件更新时,
hooks链表的结构发生了破坏,current树的memoizedState值与workInProgress存储的memoizedState值不一致,导致next指针指向下一个hook的信息出错,此时涉及到存取State值的操作就会发生意想不到的结果。
使用 hooks 需遵循以下几个原则
- 只能在函数的顶层作用域调用
Hooks,不能在套函数里调用。 - 必须在
React的函数组件或者自定义的Hook函数中调用。 - 所有的
Hooks函数在每个渲染周期都会按相同的顺序执行,不能使用条件语句来改变它们之间的顺序,否则会导致组件状态逻辑的混乱和不可预测性。 Hooks的命名必须以use开头,这是为了让React能够正确检测使用的钩子。
参考文章
转载自:https://juejin.cn/post/7239652440461918269