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
能够正确检测使用的钩子。