我们知道,对于class组件,我们只需要实例化一次,实例中保存了组件的state等状态。对于每一次更新只需要调用render方法就可以。但是在function组件中,每一次更新都是一次新的函数执行,为了保存一些状态,执行一些副作用钩子,react-hooks应运而生,去帮助记录组件的状态,处理一些额外的副作用。
react-hooks 是函数组件解决没有state,生命周期,逻辑不能复用的一种技术方案。
在开始学习源码之前我们先来了解一些概念,便于后面理解源码。(本文源码取自于 react 17.3.0 版本,并做了精简,下载源码对照着看效果更好。如果想看 18.3.1 版本的源码简读,可以点击(animasling.github.io/front-end-b…
current fiber树 : 当完成一次渲染之后,会产生一个current树,current会在commit阶段替换成真实的Dom树。
workInProgress fiber树: 即将调和渲染的 fiber 树。在一次新的组件更新过程中,会从current复制一份作为workInProgress,更新完毕后,将当前的workInProgress树赋值给current树。
workInProgress.memoizedState: 在class组件中,memoizedState存放state信息,在function组件中,memoizedState在一次调和渲染过程中,以链表的形式存放hooks信息。
currentHook : 可以理解 current树上的指向的当前调度的 hooks节点。
workInProgressHook: 可以理解 workInProgress树上指向的当前调度的 hooks节点。
接下来我们看下一个fiber 节点包含了哪些属性。
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag; // 标记 Fiber 类型, 例如函数组件、类组件、宿主组件
this.key = key; // 子节点的唯一键, 即我们渲染列表传入的key属性
this.elementType = null;
this.type = null; // 节点元素类型, 是具体的类组件、函数组件、宿主组件(字符串)
this.stateNode = null; // 节点实例
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps; // 新的、待处理的props
this.memoizedProps = null; // 上一次渲染的props
this.updateQueue = null;
this.memoizedState = null; // 上一次渲染的组件状态
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags; // effect 标签
this.subtreeFlags = NoFlags;
this.deletions = null;
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null; // 指向旧树中对应节点 }
好了,现在我们了解了基础知识之后,我们现在开始直接进入主题吧。
当我们调用函数组件的时候。如果调用了react hooks, 则会执行renderWithHooks函数。
react-reconciler/src/ReactFiberBeginWork.js
function组件初始化:
renderWithHooks(
null, // current Fiber
workInProgress, // workInProgress Fiber
Component, // 函数组件本身
props, // prop
context, // 上下文
renderLanes, // 渲染 lanes
);
对于初始化是没有current树的,之后完成一次组件更新后,会把当前workInProgress树赋值给current树。
function组件更新:
renderWithHooks(
current,
workInProgress,
render,
nextProps,
context,
renderLanes,
);
renderWithHooks
react-reconciler/src/ReactFiberHooks.js
通过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;
// 1.置空即将调和渲染的workInProgress树的memoizedState和updateQueue,
// 为什么这么做,因为在接下来的函数组件执行过程中,要把新的hooks信息挂载到这两个属性上,
// 然后在组件commit阶段,将workInProgress树替换成current树,替换真实的DOM元素节点。并在current树保存hooks信息。
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
// The following should have already been reset
// currentHook = null;
// workInProgressHook = null;
// didScheduleRenderPhaseUpdate = false;
// 2.判断函数组件是否是第一次渲染,然后赋予ReactCurrentDispatcher.current不同的hooks
// TODO 注意 如果在mount时没有使用hooks,但是在update阶段用了
// 目前我们会将 update 渲染识别为mount, 因为这个时候 memoizedState === null
// 这个是非常棘手的,因为他对组件中的一种是有效的(e.g. React.lazy)
// 只有在一个有状态的hook 被使用的时候才可以用momoizedState 来区分是mount 还是 update.
// 无状态 hooks (e.g. context) 将不会添加到 memoizedState
// 所以在updates 和 mounts 阶段,memoizedState 都有可能为 null
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 3.执行函数组件,hooks 被依次执行,并保存到workInProgress
let children = Component(props, secondArg);
// Check if there was a render phase update
if (didScheduleRenderPhaseUpdateDuringThisPass) {
// Keep rendering in a loop for as long as render phase updates continue to
// be scheduled. Use a counter to prevent infinite loops.
let numberOfReRenders: number = 0;
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
invariant(
numberOfReRenders < RE_RENDER_LIMIT,
'Too many re-renders. React limits the number of renders to prevent ’ +
‘an infinite loop.’,
);
numberOfReRenders += 1;
// Start over from the beginning of the list
currentHook = null;
workInProgressHook = null;
workInProgress.updateQueue = null;
ReactCurrentDispatcher.current = DEV ? HooksDispatcherOnRerenderInDEV : HooksDispatcherOnRerender; children = Component(props, secondArg); } while (didScheduleRenderPhaseUpdateDuringThisPass);
}
// We can assume the previous dispatcher is always this one, since we set it
// at the beginning of the render phase and there’s no re-entrancy.
// 4. 没有在函数组件中调用的hooks 都是ContextOnlyDispatcher 对象上的。
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
// This check uses currentHook so that it works the same in DEV and prod bundles.
// hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
if (enableLazyContextPropagation) {
if (current !== null) {
if (!checkIfWorkInProgressReceivedUpdate()) {
// If there were no changes to props or state, we need to check if there
// was a context change. We didn’t already do this because there’s no
// 1:1 correspondence between dependencies and hooks. Although, because
// there almost always is in the common case (readContext is an
// internal API), we could compare in there. OTOH, we only hit this case
// if everything else bails out, so on the whole it might be better to
// keep the comparison out of the common path.
const currentDependencies = current.dependencies;
if (
currentDependencies !== null &&
checkIfContextChanged(currentDependencies)
) {
markWorkInProgressReceivedUpdate();
}
}
}
}
return children;
}
hooks执行时,如果不在函数组件内部,则会赋予ReactCurrentDispatcher.current ContextOnlyDispatcher对象,抛出异常。
export const ContextOnlyDispatcher: Dispatcher = {
readContext,
useCallback: throwInvalidHookError,
useContext: throwInvalidHookError,
useEffect: throwInvalidHookError,
useImperativeHandle: throwInvalidHookError,
useLayoutEffect: throwInvalidHookError,
useMemo: throwInvalidHookError,
useReducer: throwInvalidHookError,
useRef: throwInvalidHookError,
useState: throwInvalidHookError,
useDebugValue: throwInvalidHookError,
useDeferredValue: throwInvalidHookError,
useTransition: throwInvalidHookError,
useMutableSource: throwInvalidHookError,
useOpaqueIdentifier: throwInvalidHookError,
unstable_isNewReconciler: enableNewReconciler,
};
function throwInvalidHookError() {
invariant(
false,
‘Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for’ +
’ one of the following reasons:\n’ +
‘1. You might have mismatching versions of React and the renderer (such as React DOM)\n’ +
‘2. You might be breaking the Rules of Hooks\n’ +
‘3. You might have more than one copy of React in the same app\n’ +
‘See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.’,
);
}
当函数第一次渲染组件时,ReactCurrentDispatcher.current 被赋予了HooksDispatcherOnMount 对象,更新组件时赋予HooksDispatcherOnUpdate对象。
/ 第一次渲染
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useOpaqueIdentifier: mountOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
};
// 更新渲染
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useOpaqueIdentifier: updateOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
};
我们可以通过如下流程图来总结 renderWithHook 函数主要做的事情。
好了,看到这里你应该大致了解了, 当函数执行的时候,会根据是否是初始化来调用不同的对象。下面我们将从函数初始化和更新2个方面,来学习下常用react-hooks 的主要源码。
最后推荐下我的个人网站- 【良月清秋的前端日志】(animasling.github.io/front-end-b…) ,希望我的文章对你有帮助。