React Hook源码笔记(一):函数组件加载过程React 函数组件加载过程,理解 FiberNode 的 upda
博客:pionpill
本篇开始,我们讲解函数组件与 Hooks 原理,请确保有一定的 Fiber 框架基础,强烈建议先过一遍 Fiber 系列文章。
初始化函数组件
React 每个函数组件 FiberNode
的初始类型(tag)均为: IndeterminateComponent
,我们看一下 beginWork
方法对这一类型组件的处理(✨约4043行):
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
......
}
源码(✨约1811行):
function mountIndeterminateComponent(
_current: null | Fiber, // 界面上的当前节点
workInProgress: Fiber, // 构建中的当前节点
Component: $FlowFixMe,
renderLanes: Lanes,
) {
const props = workInProgress.pendingProps;
let content, value, hasId;
// 调用函数组件
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderLanes,
);
// useId hook 相关,暂时不用管,为 false
hasId = checkDidRenderIdHook();
if (
!disableModulePatternComponents &&
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
// 类组件处理逻辑
} else {
// 被打上函数组件的标签
workInProgress.tag = FunctionComponent;
if (getIsHydrating() && hasId) {
pushMaterializedTreeId(workInProgress);
}
// 创建子节点
reconcileChildren(null, workInProgress, value, renderLanes);
return workInProgress.child;
}
}
目前我们只关注 renderWithHooks
这个方法。
renderWithHooks
renderWithHooks
方法执行我们定义的函数组件,返回值就是函数 return
的内容(一般是 jsx 内容)(✨约476行):
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;
// 一个全局变量,设置当前渲染中的 FiberNode
currentlyRenderingFiber = workInProgress;
// 重置节点数据
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
// 设置首次加载的dispatcher,也是一个全局变量
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 执行我们的函数组件,返回值就是 children
let children = Component(props, secondArg);
// 重置一些属性
finishRenderingHooks(current, workInProgress, Component);
return children;
}
这个方法做了两件重要的事:
workInProgress
: 当前节点属性重制,并进行了ReactCurrentDispatcher
相关逻辑children
: 执行函数组件,将执行结果(也就是子节点)返回。
这里的 memoizedState, updateQueue 非常重要,贯穿函数组件的整个生命周期,最后会单独讲。
ReactCurrentDispatcher
ReactCurrentDispatcher
这个全局对象会根据当前节点是否存在决定使用如何调用各个钩子函数。钩子函数根据组件状态不同有三种具体实现,以 useState
为例:
mountState
: 创建新节点阶段。updateState
: 更新节点阶段。rerenderState
: 渲染阶段,但没有更新。
函数组件的每个hook
实际就是在调用 ReactCurrentDispatcher
中的同名方法,比如 setState
(✨约86行):
export function useState<S>(initialState: (() => S) | S,): [S, Dispatch<BasicStateAction<S>>] {
// 这个方法返回 ReactCurrentDispatcher.current
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
函数组件调用
let children = Component(props, secondArg)
这里的 Component
就是我们写的函数组件,他接受两个参数: prop
以及 context
。这里就表示执行一次我们定义的函数。
renderWithHooks
执行完成之后,返回组件的子元素,再依此创建子节点。
hooks 加载过程
前面我们知道,函数组件加载过程中会先执行一遍函数,因此 hooks 也会被执行,首先了解一下 Dispatcher
这个类型(✨约589行):
const Dispatcher: DispatcherType = {
use,
readContext,
useCacheRefresh,
useCallback,
useContext,
useEffect,
useImperativeHandle,
useDebugValue,
useLayoutEffect,
useInsertionEffect,
useMemo,
useMemoCache,
useOptimistic,
useReducer,
useRef,
useState,
useTransition,
useSyncExternalStore,
useDeferredValue,
useId,
useFormState,
};
它包含了我们开发过程中使用的所有 React 钩子,常用的 Dispatch
对象有三个:
- mount过程:
HooksDispatcherOnMount
- update过程:
HooksDispatcherOnUpdate
- rerender过程:
HooksDispatcherOnRerender
HooksDispatcherOnRerender
大部分属性和 HooksDispatcherOnUpdate
相同。
这篇我们以 setState 为例,简单说一下 hook 与 FiberNode 的关系,具体的 hook 逻辑会单独讲解。
mountState
初始化函数组件过程中,HooksDispatcherOnMount
对象的 useState
实际调用的是 mountState
方法(✨约1775行):
function mountState<S>(initialState: (() => S) | S,): [S, Dispatch<BasicStateAction<S>>] {
// 创建一个 hook 出来
const hook = mountStateImpl(initialState);
const queue = hook.queue;
// 这里是 useState 具体逻辑,先略过
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any);
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
mountStateImpl
创建钩子对象 mountStateImpl
的逻辑如下(✨约1750行):
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
// 创建一个 mount 阶段的 hook
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
// hook 也有一个 memoizedState 属性,用于存放钩子的状态
hook.memoizedState = hook.baseState = initialState;
// 也有一个 queue 属性
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null, // 待处理的 update 链表
lanes: NoLanes,
dispatch: null, // setState 方法
lastRenderedReducer: basicStateReducer, // 一个函数,通过 action 和 lastRenderedState 计算最新的 state
lastRenderedState: (initialState: any), // 上一次的 state
};
hook.queue = queue;
return hook;
}
mountWorkInProgressHook
hook 加载方法源码如下(✨约926行):
function mountWorkInProgressHook(): Hook {
// 创建 hook
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// 第一个 hook
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 后面的 hook 链表链上去
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
这个方法目前我们只关注一点:该方法一个空的 hook
对象,同时将 hook
挂载到 FiberNode
的 memoizedState
中。到这里我们知道 FiberNode 的 memoizedState
属性存的是组件所有 hook 的链表就行了。
函数组件更新
最常见的触发函数组件更新的方法是调用改变状态的 setXXX
方法,本质上是调用了 dispatchSetState
方法,这会触发一系列副作用逻辑,我们快进到 beginWork
处理函数组件:
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
核心方法是 updateFunctionComponent
(✨约926行):
function updateFunctionComponent(
current: null | Fiber,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
) {
let context;
let nextChildren;
let hasId;
prepareToReadContext(workInProgress, renderLanes);
// 同样调用 renderWithHooks 方法
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
// 是否满足 bailout 优化策略
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
if (getIsHydrating() && hasId) {
pushMaterializedTreeId(workInProgress);
}
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
更新阶段会多一个 bailout
优化判断,其他逻辑和初始化阶段类似。不同的是在 renderWithHooks
方法中,会执行如下代码使用 update
阶段的 Dispatcher
:
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate
updateWorkInProgressHook
我们重点关注 update 阶段创建 hook
的方法(✨约947行)。
function updateWorkInProgressHook(): Hook {
let nextCurrentHook: null | Hook;
if (currentHook === null) {
// 首个update阶段的 hook,下一个要处理的 hook 从 FiberNode.memoizedState 中取
const current = currentlyRenderingFiber.alternate;
nextCurrentHook = current ? current.memoizedState : null;
} else {
// 非首个,依此取
nextCurrentHook = currentHook.next;
}
// 第一个 hook 更新时,workInProgressHook 为 null
const nextWorkInProgressHook = workInProgressHook === null
? currentlyRenderingFiber.memoizedState
: workInProgressHook.next;
if (nextWorkInProgressHook !== null) {
// 不是最后一个要处理的 hook
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
if (nextCurrentHook === null) {
// 报错:hook 数量不一致
}
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null, // 明确最后一个 hook 清空 next,可能出现前后 hook 数量不一致的问题
};
// 挂载 hook
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
这个过程依此取出了原来 FiberNode.memoizedState
上的 hook,根据原来的 hook 复用/更新/创建新的 newHook
对象,最后按顺序一个一个添加到新的 FiberNode
的 memoizedState
属性上。
注意这个组件更新并重新拼接 hook
的过程:
- 这个过程会更新原有的 hook,将
queue
中的任务取出来执行,并更新状态 hook
链的更新过程完全依赖旧有的hook
链。
这下知道为啥 hook 不能写在条件语句中了。如果组件更新发现 hook 对不上,会直接抛出错误。
小结
整体流程
本篇简单描述了函数组件的加载与更新过程,包含如下操作:
renderWithHooks()
(核心部分):指定函数组件,创建FiberNode
- id 注入:如果存在 id,则插入
reconcileChildren()
:创建子节点
如果是更新节点,则会尝试 bailout
优化组件,满足优化条件,则无需进行最后的两个步骤,直接复用。
钩子节点
renderWithHooks
方法有三个重要逻辑:
ReactCurrentDispatcher
:获取目前阶段(加载/更新...)的Dispatcher
,执行对应的钩子逻辑Component()
:执行函数组件,过程中会获取并执行Dispatcher
的钩子函数finishRenderingHooks()
:收尾工作,重置一些属性
钩子函数
本篇简单看了一下 setState
钩子的实现,我们只关注所有钩子函数共性的部分:
mountWorkInProgressHook
:创建一个空的钩子,并将其挂到FiberNode
的memoizedState
属性上。updateWorkInProgressHook
:复用原有钩子,依此拼接到新FiberNode
的memoizedState
属性上,如果是最后一个hook
,则创建新的hook
,复用原来的属性,并指定next: null
。
由于 update
阶段复用原有的钩子链表,因此如果条件语句中出现新的钩子,导致钩子链表长度出现问题,就抛错。(如果钩子出现的顺序出了问题,也会出错。)
setState 钩子的逻辑一带而过,下文会详细讲解
重要的属性
在 FiberNode
和 Hook
上各有几个重要的属性需要记住:
FiberNode.memorizedState
: 记录Hook
链FiberNode.updateQueue
: 记录节点需要执行的更新操作,包括监听的事件回调,useState
导致的更新...Hook.memorizedState
:记录Hook
自己的状态Hook.queue
: 记录Hook
的更新队列
其他参考资料
转载自:https://juejin.cn/post/7406169544089239587