在之前的章节我们讲述了FiberTree
的创建过程,但是对组件的加载过程这方面的细节没有深入。
本节将深入理解React18.2x 函数组件的具体加载过程。
1,加载阶段
首先准备一个函数组件案例:
js
export default function MyFun(props) {
console.log('MyFun组件运行了')
const [count, setCount] = useState(1)
useEffect(() => {
console.log('useEffect DOM渲染之后执行')
}, [])
useLayoutEffect(() => {
console.log('useLayoutEffect DOM渲染之前执行')
}, [])
function handleClick() {
setCount(2)
setCount(3)
}
return (
<div className='MyFun'>
<div>MyFun组件</div>
<div>state: {count}</div>
<div>name: {props.name}</div>
<button onClick={handleClick}>更新</button>
</div>
)
}
直接跳转到函数组件对应的Fiber
节点加载:
执行该Fiber
节点的beginWork
工作,根据tag
类型,进入IndeterminateComponent
待定组件的逻辑处理【case IndeterminateComponent
】:
每个函数组件的首次加载都是走的
IndeterminateComponent
分支逻辑,这是因为在创建函数组件Fiber
的时候,react没有更新它的tag
值,所以它的首次beginWork
工作就会进入IndeterminateComponent
分支,在mountIndeterminateComponent
方法中才会更新它的tag
,使函数组件的Fiber
在更新阶段执行beginWork
时,能够进入正确的FunctionComponent
分支。不了解
Fiber
创建逻辑的可以查看《React18.2x源码解析(三)reconciler协调流程》的内容。
mountIndeterminateComponent
查看mountIndeterminateComponent
方法:
js
// packages\react-reconciler\src\ReactFiberBeginWork.new.js
function mountIndeterminateComponent(
_current,
workInProgress,
Component,
renderLanes,
) {
// 取出函数组件的props {name: "MyFun"}
const props = workInProgress.pendingProps;
// 存储FirstChild内容
let value;
let hasId;
# 调用函数组件
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderLanes,
);
// 针对类组件和函数组件进行不同的处理【只是类组件现在已经不走这里了】
if (
!disableModulePatternComponents &&
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
// 类组件的处理逻辑
} else {
// Proceed under the assumption that this is a function component
// 函数组件处理
// 更新tag为函数组件类型的值,下个逻辑就可以直接进入函数组件的处理【节点更新的时候】
workInProgress.tag = FunctionComponent;
// 处理函数组件FirstChild内容【当前为App组件】
reconcileChildren(null, workInProgress, value, renderLanes);
return workInProgress.child;
}
}
首先取出当前函数组件FIber
节点上的props
,方便函数组件加载的使用:
js
const props = workInProgress.pendingProps;
然后调用renderWithHooks
方法:
js
value = renderWithHooks();
其实函数组件的加载非常简单,renderWithHooks
方法就是函数组件的主要加载逻辑。
这个方法会执行我们定义的函数组件,返回值就是函数中return
的内容,也就是jsx
内容【处理过后的react-element
对象】。
renderWithHooks
查看renderWithHooks
方法:
js
// packages\react-reconciler\src\ReactFiberHooks.new.js
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;
# 设置为当前渲染中的Fiber
currentlyRenderingFiber = workInProgress;
# 重置函数组件节点的数据
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
// 设置首次加载的dispatcher【重点】
ReactCurrentDispatcher.current =current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// Component为workInProgress.type 如果是函数组件:就是自身函数
# 调用这个函数,即调用组件,循环生成Element对象,
// 将return返回的Jsx内容转换为reactElement对象,最后返回这个对象
let children = Component(props, secondArg);
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
# 返回函数组件的内容【reactElement对象】
return children;
}
首先将当前函数组件节点workInProgress
赋值给全局变量currentlyRenderingFiber
:
js
// 当前渲染中的Fiber节点
currentlyRenderingFiber = workInProgress;
变量currentlyRenderingFiber
会在后面的逻辑中被多次用到,这里注意一下它的赋值即可。
接着重置函数组件Fiber
的两个属性:
js
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
注意: memoizedState
和updateQueue
属性是函数组件内容的重点,这两个属性与hooks
紧密相连,后面会多次用到。
ReactCurrentDispatcher
然后设置ReactCurrentDispatcher
的current
属性值:
js
ReactCurrentDispatcher.current = (current === null || current.memoizedState === null)
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
因为当前是首次加载,所以:
js
ReactCurrentDispatcher.current = HooksDispatcherOnMount
ReactCurrentDispatcher
对象是一个全局变量,它是在react源码中的react
包定义的:
js
// packages\react\src\ReactCurrentDispatcher.js
const ReactCurrentDispatcher = {
current: null,
};
export default ReactCurrentDispatcher;
然后将它包装在一个新的对象中:
js
// packages\react\src\ReactSharedInternals.js
const ReactSharedInternals = {
ReactCurrentDispatcher,
ReactCurrentBatchConfig,
ReactCurrentOwner,
};
export default ReactSharedInternals;
最后会在react
包的入口文件中暴露给外部其他资源包使用:
js
// packages\react\src\React.js
export {
...
ReactSharedInternals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
}
而shared
包【通用工具包】会引入这个对象,然后暴露给全局:
js
// packages\shared\ReactSharedInternals.js
import * as React from 'react';
const ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
export default ReactSharedInternals;
其他资源包就可以通过shared
工具包来拿到这个对象,所以我们在函数组件加载时才能使用这个对象:
js
// packages\react-reconciler\src\ReactFiberHooks.new.js
import ReactSharedInternals from 'shared/ReactSharedInternals';
// 拿到ReactCurrentDispatcher对象
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
知道了ReactCurrentDispatcher
对象的由来,我们才能更好地理解它的作用,因为函数组件的每个hook
实际就是在调用这个对象中的同名方法 ,比如useState
:
js
// packages\react\src\ReactHooks.js
export function useState(initialState){
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
查看resolveDispatcher
方法:
js
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
// 返回获取到的dispatcher
return dispatcher;
}
所以这里的dispatcher
就是上面的ReactCurrentDispatcher.current
对象。
js
useState(initialState); // 等同于
ReactCurrentDispatcher.current.useState(initialState)
其他的hook
也是一样的原理,所以理解ReactCurrentDispatcher
对象才能知道hooks
的本质。
下面继续回到renderWithHooks
方法中。
函数组件调用
js
let children = Component(props, secondArg);
调用Component
其实就是调用我们定义的函数,也就是说函数组件的加载其实就是执行一次我们定义的函数:
点击单步执行,就可以进入MyFun
函数的执行:
所以函数组件的加载就是执行一次函数的内容,理解起来也很简单。最后触发return
关键字,这里的jsx
内容会在react内部进行处理,生成react-element
对象,最后返回值就是创建的react
元素对象。
js
return children;
最后返回生成的react-element
对象,renderWithHooks
方法执行完成。
js
value = renderWithHooks()
回到mountIndeterminateComponent
方法,这里的value
就是创建的react
元素对象。
然后通过一个if
语句来区分类组件和函数组件的逻辑:
js
if (...) {
// 类组件的处理
} else {
// 函数组件的处理
// 更新tag为函数组件类型的值,下个逻辑就可以直接进入函数组件的处理【节点更新的时候】
workInProgress.tag = FunctionComponent;
# 处理函数组件FirstChild内容【当前为App组件】
reconcileChildren(null, workInProgress, value, renderLanes);
return workInProgress.child;
}
这里区分类组件与函数组件,主要是通过render
函数:
js
typeof value.render === 'function'
因为类组件必须存在render
函数,所以它创建的组件实例instance
会存在render
方法,而函数组件则不存在。
只是类组件的加载已经不走这里的逻辑了,具体可以查看《React18.2x源码解析:类组件的加载过程》。
函数组件初始化执行完成后,就会更新函数组件Fiber
节点的tag
值为正确的类型FunctionComponent
【后续逻辑函数组件节点便可以进入Function
分支了】。
然后根据新建的value
【react
元素对象】创建子Fiber
节点,最后返回子节点,函数组件的加载过程就基本完成了。
创建
Fiber
子节点具体过程可以查看《React18.2x源码解析(三)Reconciler协调流程》。
hooks的加载
本小节将主要讲解函数组件加载过程中:hooks
的加载处理。
还是上面的案例,首先查看useState
的初始化:
js
export function useState(initialState){
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
// 等同于
return ReactCurrentDispatcher.current.useState(initialState)
}
所以我们得先查看当前的ReactCurrentDispatcher
对象:
js
ReactCurrentDispatcher.current = HooksDispatcherOnMount
继续查看HooksDispatcherOnMount
对象:
js
// packages\react-reconciler\src\ReactFiberHooks.new.js
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState, // 加载state
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,
unstable_isNewReconciler: enableNewReconciler,
};
可以发现,所有hooks
在加载时都是在调用HooksDispatcherOnMount
对象的同名方法:
这里我们只关注useState
:
js
useState: mountState
mountState
查看mountState
方法:
js
// packages\react-reconciler\src\ReactFiberHooks.new.js
function mountState(initialState) {
// hook加载工作
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = {
pending: null, // 等待处理的update链表
lanes: NoLanes,
dispatch: null, // dispatchSetState方法
lastRenderedReducer: basicStateReducer, // 一个函数,通过action和lastRenderedState计算最新的state
lastRenderedState: initialState, // 上一次的state
};
hook.queue = queue;
const dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue)
return [hook.memoizedState, dispatch];
}
首先调用mountWorkInProgressHook
方法,创建了一个hook
对象。
mountWorkInProgressHook
继续查看mountWorkInProgressHook
方法:
js
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添加到第一个hook的next属性上,形成一个单向链表
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
首先创建一个hook
对象,workInProgressHook
默认为null
,它代表当前正在处理中的hook
对象。
当前useState
为函数组件中的第一个调用的hook
,所以这时workInProgressHook
肯定为null
:
js
workInProgressHook = hook;
currentlyRenderingFiber.memoizedState = workInProgressHook;
将新建hook
对象赋值给workInProgressHook
,表示为正在处理中的hook
对象。
同时也将第一个hook
对象赋值给当前函数组件Fiber
节点的memoizedState
属性。
此时函数组件Fiber
节点的memoizedState
属性指向为:
js
return workInProgressHook;
最后返回新建的hook
对象。
继续回到mountState
方法中:
js
...
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
...
在hook
新建完成之后,判断传入的参数initialState
是否为函数,如果为函数则调用此函数,将结果赋值为新的initialState
。
然后设置hook
对象的memoizedState
和baseState
属性为初始的数据initialState
。
接着看mountState
方法剩下的内容:
js
function mountState(initialState) {
...
const queue = {
pending: null,
lanes: NoLanes,
dispatch: null, // dispatchSetState方法
lastRenderedReducer: basicStateReducer, // 一个函数,通过action和lastRenderedState计算最新的state
lastRenderedState: (initialState: any), // 上一次的state
};
hook.queue = queue; // 设置队列
const dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue)
return [hook.memoizedState, dispatch];
}
创建一个queue
对象,这里要注意两个属性:
lastRenderedReducer
:它是一个函数,作用是根据action
和lastRenderedState
计算最新的state
。
js
function basicStateReducer(state, action) {
// action就是setCount传入的参数,如果为一个函数,则将state传入进行计算,返回新的state
// 如果不是函数,则action就是最新的state
return typeof action === 'function' ? action(state) : action;
}
lastRenderedState
:代表上一次渲染的state
。
然后更新hook
对象的queue
属性,同时设置queue
对象的dispatch
属性为一个修改函数dispatchSetState
,
最后返回一个数组,这就是useState hook
的返回值:一个初始state
和一个修改函数。
js
const [count, setCount] = useState(1)
到此,函数组件的第一个hook
:useState
初始化完成。
这里没有展开
dispatchSetState
方法,我们放在更新阶段再讲解。
最后再看一下当前函数组件Fiber
节点的memoizedState
属性内容【第一个hook
对象】:
下面我们开始第二个hook
【useEffect
】的初始化:
js
// packages\react\src\ReactHooks.js
export function useEffect(create, deps) {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
// 等同于
return ReactCurrentDispatcher.current.useEffect(create, deps)
}
js
const HooksDispatcherOnMount: Dispatcher = {
useEffect: mountEffect
}
mountEffect
查看mountEffect
方法:
js
// packages\react-reconciler\src\ReactFiberHooks.new.js
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
// 进入effect加载
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
}
继续查看mountEffectImpl
方法:
js
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 创建的新的hook对象
const hook = mountWorkInProgressHook();
// 确定当前hook的deps依赖
const nextDeps = deps === undefined ? null : deps;
// 当前渲染中的Fiber节点,即函数组件对应的,打上effect钩子的flags
currentlyRenderingFiber.flags |= fiberFlags;
// 设置hook的memoizedState属性
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
依然是先调用mountWorkInProgressHook
创建一个hook
对象:
js
function mountWorkInProgressHook(): Hook {
...
if (workInProgressHook === null) {
// 第一个hook
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 后面的hook添加到第一个hook的next属性上,形成一个单向链表
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
不过当前workInProgressHook
不为null
,因为此时它还是指向的第一个hook
对象【useState
对应的】:
所以只能进入else
分支:这里要注意连等语句的执行顺序:
js
a = b = 1;
先执行b=1
,然后再把b
的结果赋值给a
。
js
workInProgressHook = workInProgressHook.next = hook;
所以这里是先将第一个hook
对象的next
属性指向新建的hook
。然后再更新workInProgressHook
的值为当前的hook
对象。
此时函数组件Fiber
节点的memoizedState
属性指向为:
回到mountEffectImpl
方法中:
js
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 创建的新的hook对象
const hook = mountWorkInProgressHook();
// 确定当前hook的deps依赖
const nextDeps = deps === undefined ? null : deps;
// 当前渲染中的Fiber节点,即函数组件对应的,打上effect钩子的flags
currentlyRenderingFiber.flags |= fiberFlags;
// 设置hook的memoizedState属性
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
在hook
创建完成之后,确定当前hook
对象的deps
依赖,因为我们传递的依赖为[]
,所以此时deps
为一个空数组。然后更新当前Fiber
节点的flags
标记,最后设置hook对象的memoizedState
属性内容,这里属性的结果为pushEffect
方法调用的返回值,所以我们还得查看pushEffect
方法。
pushEffect
js
function pushEffect(tag, create, destroy, deps) {
// 创建副作用对象
const effect = {
tag,
create, // 回调函数
destroy, // 销毁函数
deps,
// Circular
next: null,
};
// 取出当前函数组件的UpdateQueue
let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
if (componentUpdateQueue === null) {
// 为null时: 创建当前函数组件的UpdateQueue
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
// 第一个effect对象: 它的next属性会执行自己,形成一个单向环状链表
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 第二次加载其他的effect时: 将
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
// 上一个effect的next属性指向新建的effect
lastEffect.next = effect;
// 新建的next属性指向第一个effect
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
首先创建了一个effect
对象,查看它的内容:
create
属性即为我们传入的回调函数。deps
属性是当前useEffect hook
的依赖,为一个空数组。destory
属性为undefined
,它存储的是useEffect hook
返回的清理函数或者说销毁函数,但是它不是在这里赋值的,并且当前我们也没有返回这个函数。
然后取出当前函数组件Fiber
节点的updateQueue
属性内容赋值给变量componentUpdateQueue
。
然后判断componentUpdateQueue
是否为null
:
js
let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
if (componentUpdateQueue === null) {
# 第一effect相关的Hook 加载时,初始化函数组件Fiber的updateQueue属性
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
// 第一个effect对象: 它的next属性会执行自己,形成一个单向环状链表
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
...
}
注意: 前面已经讲解过,函数组件每次一进入renderWithHooks
方法都会重置一些属性:
js
workInProgress.updateQueue = null;
所以当前变量componentUpdateQueue
为null
,然后调用createFunctionComponentUpdateQueue
方法更新它的值。
这里的逻辑本质就是: 在处理第一个effect
相关的hook
时,需要初始化函数组件Fiber
节点的updateQueue
属性。
所以这里我们还需要查看createFunctionComponentUpdateQueue
方法:
js
function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
return {
lastEffect: null,
stores: null,
};
}
这个方法直接返回一个初始的对象,所以当前函数组件Fiber
节点的updateQueue
属性变为:
js
componentUpdateQueue.lastEffect = effect.next = effect;
最后将当前创建的effect
对象的next
属性指向了自身,且同时更新updateQueue.lastEffect
属性为当前effect
对象,由此形成一个单向环状链表。
所以此时函数组件Fiber
节点的updateQueue
属性更新为:
pushEffect
方法最后,返回当前创建的effect
对象:
js
return effect;
再回到mountEffectImpl
方法中:
js
hook.memoizedState = pushEffect()
所以hook
对象的memoizedState
属性值为一个effect
对象。
从这里我们可以发现,虽然每个hook
对象都是相同的属性,但是不同的hook
类型它存储的内容却完全不同。
useState
创建的hook对象,它的memoizedState
属性存储的为数据state
。useEffect
创建的hook对象,它的memoizedState
属性存储的为一个effect
对象。
注意:这里不要将
hook
对象的memoizedState
属性和Fiber
节点的memoizedState
属性搞混了。
到此,函数组件的第二个hook
:useEffect
初始化完成。
下面我们开始第三个hook
【useLayoutEffect
】的初始化:
js
export function useLayoutEffect(
create: () => (() => void) | void, // 回调函数
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useLayoutEffect(create, deps);
// 等同于
return ReactCurrentDispatcher.current.useLayoutEffect(create, deps)
}
js
const HooksDispatcherOnMount: Dispatcher = {
useLayoutEffect: mountLayoutEffect
}
mountLayoutEffect
查看mountLayoutEffect
方法:
js
// packages\react-reconciler\src\ReactFiberHooks.new.js
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
let fiberFlags: Flags = UpdateEffect;
if (enableSuspenseLayoutEffectSemantics) {
fiberFlags |= LayoutStaticEffect;
}
return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}
可以发现useEffect
和useLayoutEffect
共用了同一个加载方法mountEffectImpl
,所以它们会执行同样的逻辑处理。
hook
对象创建和处理,此时函数组件Fiber
节点的memoizedState
属性指向更新为:
effect
对象创建和处理,依然是pushEffect
方法的调用:
js
if (componentUpdateQueue === null) {
...
} else {
// 第二次加载其他的effect时:
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
// 上一个effect的next属性指向新建的effect
lastEffect.next = effect;
// 新建的next属性指向第一个effect
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
当前为第二个effect
相关的hook
处理,所以此时Fiber.updateQueue
【即componentUpdateQueue
】是有值的,进入else
分支处理。
更新Fiber.updateQueue.lastEffect
属性指向为当前新建的effect2
,将effect2
的next
属性指向为之前的effect
对象。
此时函数组件Fiber
节点的updateQueue
属性指向更新为:
到此,函数组件加载阶段的hooks
就处理完成。
commit阶段
前面全部的加载逻辑都是在Fiber Reconciler
协调流程中执行的,即函数组件大部分的加载逻辑都是在reconciler
协调流程中完成的【更新阶段同理】,还有剩下的一部分逻辑在commit
阶段之中处理,这里我们继续讲解。
这里简单介绍一下
commit
阶段的内容,更多处理逻辑可以查看《React18.2x源码解析(四)commit阶段》。
函数组件剩下的加载即是在commit
中关于Effect hook
的副作用回调,即useEffect
和useLayoutEffect
。
commit阶段的逻辑主要分为三个子阶段内容:
- BeforeMutation
- Mutation
- Layout
js
function commitRootImpl() {
// 发起调度处理useEffect回调
scheduleCallback(NormalSchedulerPriority, () => {
// 这个回调就是处理useEffect的
flushPassiveEffects();
});
// 1,BeforeMutation阶段
commitBeforeMutationEffects()
// 2,Mutation阶段,渲染真实DOM加载到页面
commitMutationEffects()
// 3,Layout阶段
commitLayoutEffects()
}
commit
阶段的内容都是同步执行,在进入具体的执行之前,都会先调用scheduleCallback
方法发起一个新的调度,即创建一个新的任务task
,最后会生成一个新的宏任务来异步处理副作用【即执行useEffect
的回调钩子】。
知道了useEffect
的回调处理,我们再查看useLayoutEffect
的回调处理。
Layout阶段
js
function commitLayoutEffectOnFiber(
if ((finishedWork.flags & LayoutMask) !== NoFlags) {
// 根据组件类型
switch (finishedWork.tag) {
// 函数组件的处理
case FunctionComponent: {
// 传入的是layout相关的flag标记
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
}
)
查看commitHookEffectListMount
方法:
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
// 当前函数组件的updateQueue属性,存储的是副作用链表
const updateQueue = finishedWork.updateQueue;
// 取出最后一个effect对象
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
// 获取第一个effect对象
const firstEffect = lastEffect.next;
let effect = firstEffect;
// 开始循环处理
do {
if ((effect.tag & flags) === flags) {
// Mount
const create = effect.create;
// 执行回调函数
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
首先从当前函数组件Fiber
节点取出它的updateQueue
属性内容,在前面我们已经知道了Fiber.updateQueue
存储的是副作用相关的链表,回顾之前的内容:
定义一个lastEffect
变量存储updateQueue.lastEffect
的内容,即最后一个effect
对象。
判断lastEffect
是否为null
,如果lastEffect
为null,代表当前函数组件没有使用过effect
相关的hook
。
当前肯定是有值的,继续向下执行。从lastEffect.next
中取出第一个effect
对象,开始按顺序循环处理副作用。
js
do {
if ((effect.tag & flags) === flags) {
// Mount
const create = effect.create;
// 执行回调函数
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
注意在执行之前有一个条件判断,只有存在effect
相关的flags
标记才会执行对应副作用回调。
而在之前hook
加载是有进行设置的:
js
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags, // HookHasEffect标记就是表示有需要执行副作用
...
}
在函数组件加载阶段时,每个useEffect
和useLayoutEffect
都有打上HookHasEffect
的标记,表示在加载阶段都会默认执行一次。
需要注意的是:之前commitHookEffectListMount
传入的是与Layout
相关的flags
标记。
js
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); // Layout
所以这里只有layout hook
的回调才能执行,第一个effect
对象对应的是useEffect
,不满足判断条件:
js
effect = effect.next;
从当前effect
对象的next
属性取出下一个effect对象,开始第二次循环。
第二个effect
对象对应的是useLayoutEffect
,满足判断条件,执行它的回调函数。
js
const create = effect.create;
// 执行回调函数
effect.destroy = create();
此时控制台就会打印出对应的日志内容,到此hook相关的回调处理完成,函数组件加载逻辑全部执行完成。
总结
函数组件加载阶段:难点在于对hooks
的处理,本案例以三个常见的hook
解析了它们首次加载的逻辑。
2,更新阶段
点击案例的更新按钮,触发一次组件更新,进入函数组件的更新阶段。
这里的setCount
方法就是之前useState hook
加载返回的dispatch
方法:
js
const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue)
return [hook.memoizedState, dispatch];
注意: 下面开始第一个setCount
逻辑【这部分的逻辑可以对比查看类组件的this.setState
基本一致】。
dispatchSetState
查看dispatchSetState
方法:
js
// packages\react-reconciler\src\ReactFiberHooks.new.js
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A, // state 1
) {
// 请求更新优先级
const lane = requestUpdateLane(fiber);
// 创建update更新对象
const update: Update<S, A> = {
lane,
action, // state 1
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
// 调度之前的一个优化策略校验: eagerState
// 快速计算出最新的state,与原来的进行对比,如果没有发生变化,则跳过后续的更新逻辑
const alternate = fiber.alternate;
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
// nothing
}
}
}
// 将更新对象入队
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
// 开启一个新的调度更新任务
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
}
首先一看dispatchSetState
方法的整个结构和类组件的更新方法enqueueSetState
基本相同,还有react应用的初始加载updateContainer
,其实一个react应用的更新场景就只有这三种,而它们的更新逻辑就是以下几个步骤:
- 获取更新优先级
lane
。 - 创建
update
更新对象 。 - 将
update
更新对象添加到目标Fiber
对象的更新队列中。 - 开启一个新的调度更新任务。
关于更新这部分逻辑可以对比查看《React18.2x源码解析:类组件的加载过程》中更新阶段内容和《React18.2x源码解析(一)react应用加载》中
updateContainer
内容。
它们的区别主要是函数组件这里在调度之前有一个eagerState
【急切的state】优化策略校验:
js
// 当前的state,即旧的state
const currentState: S = (queue.lastRenderedState: any);
// 快速计算最新的state
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
// 比较新旧state
if (is(eagerState, currentState)) {
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
这个优化策略的作用是:调用 queue.lastRenderedReducer
方法,通过原来的state
和当前传入的action
参数,快速的计算出最新的state
【即eagerState
】,通过比较新旧state
来判断数据是否变化,如果没有变化则可以跳过后续的更新逻辑,即不会开启新的调度更新任务。当前我们的state
是有变化的,所以不满足优化策略,将继续向下执行更新。
其次是函数组件和类组件的update
更新对象结构不同【其中类组件和应用更新共用同一个update对象结构】。
接下来我们首先看函数组件中update
对象的定义:
js
const update = {
lane,
action, // state数据 1, 也可以是一个函数
hasEagerState: false,
eagerState: null, // 急切的state 根据action计算
next: null, // 指向下一个update对象
};
这里的action
属性存储的就是setCount
的传入参数,也就是新的state
数据。
然后调用enqueueConcurrentHookUpdate
方法,将update
对象添加到队列。
js
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
查看useState
对应的hook
对象的queue
属性内容:
它的lastRenderedState
属性代表上一次的state
,而它的interleaved
和pending
属性目前都为null
。
下面查看enqueueConcurrentHookUpdate
方法:
js
export function enqueueConcurrentHookUpdate<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
lane: Lane,
) {
const interleaved = queue.interleaved;
if (interleaved === null) {
update.next = update;
pushConcurrentUpdateQueue(queue);
} else {
update.next = interleaved.next;
interleaved.next = update;
}
queue.interleaved = update;
}
首先取出queue
的interleaved
属性,如果interleaved
为null
,表示为当前的update1
为第一个入队的更新对象,将此update1
的next
属性指向自身,形成一个单向环状链表。
然后调用了一个pushConcurrentUpdateQueue
方法,这个方法的作用是将queue
备份到一个并发队列concurrentQueues
之中,方便在之后将queue.interleaved
的内容转移到queue.pending
之上。
interleaved
只是一个临时存储update
链表的属性,最终会在更新之前转移到pending
属性之上用于计算。
js
pushConcurrentUpdateQueue(sharedQueue);
最后设置queue.interleaved
为当前的update
对象。
至此,第一个setCount
操作的update1
入队处理完成。
回到dispatchSetState
方法中,这个方法最后会调用scheduleUpdateOnFiber
函数进入更新的调度程序。
在这里你可以发现:无论是函数组件还是类组件的更新,在更新调度方面都是同样的处理逻辑。
click
事件触发的更新任务为同步任务,下面直接快进,来到同步任务的处理:
这里首先会调用scheduleSyncCallback
方法,将处理同步任务的performSyncWorkOnRoot
回调函数添加到同步队列syncQueue
。
然后在支持微任务的环境下:就会使用scheduleMicrotask
方法,这个方法等同于Promise.then
:
js
Promise.then(flushSyncCallbacks)
这里就会将冲刷同步任务队列syncQueue
的flushSyncCallbacks
函数添加到微任务中,然后继续向下执行。
注意: 我们在DOM事件中执行了两次setCount
操作:
js
function handleClick() {
setCount(2)
setCount(3)
}
上面第一次setCount
执行完成,除了处理update
对象之外,调度方面的逻辑主要就是将冲刷同步任务队列的函数添加到了微任务之中,等待异步处理。
但是我们的同步代码还没有执行完成,还有第二个setCount
等待执行:
再次进入dispatchSetState
方法:
第二次调用setCount
还会新建一个update
更新对象【update2
】,依然会执行入队操作。
js
export function enqueueConcurrentHookUpdate<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
lane: Lane,
) {
const interleaved = queue.interleaved;
if (interleaved === null) {
// 第一个update入队
update.next = update;
pushConcurrentUpdateQueue(queue);
} else {
// 其他update入队
update.next = interleaved.next;
interleaved.next = update;
}
queue.interleaved = update;
}
此时update2
非第一个入队的对象,所以就会进入else
分支处理:
- 将当前的
update2
对象的next
属性指向第一个update1
。 - 将第一个
update1
的next
属性指向当前的update2
对象。
最后将queue.interleaved
设置为最新的update2
。
至此,update2
也已经入队完成,此时queue.interleaved
指向的就是最新的update2
。
回到dispatchSetState
方法中,最后还是会调用scheduleUpdateOnFiber
函数进入更新的调度程序。
但是这次在调度时发现新的调度优先级和现存的优先级相同,可以归为同一个任务处理,就不会再重复调度。
最后触发return
关键字,结束本次同步代码的执行。
flushSyncCallbacks
来微任务队列,开始执行flushSyncCallbacks
方法:
可以看出syncQueue
同步任务队列之中只有一个任务,即performSyncWorkOnRoot
函数。
后面的逻辑就直接简单介绍了,方便快速进入到函数组件的更新程序:
js
callback = callback(isSync);
循环syncQueue
队列,从中取出callback
回调函数,然后调用回调函数【performSyncWorkOnRoot
】。
直接进入到performSyncWorkOnRoot
方法中:
js
function performSyncWorkOnRoot(root) {
...
var exitStatus = renderRootSync(root, lanes);
}
调用renderRootSync
方法,开始FiberTree
的创建过程。
在这之前,还有一个处理要注意:
js
function renderRootSync() {
...
prepareFreshStack()
}
function prepareFreshStack() {
...
finishQueueingConcurrentUpdates()
}
在renderRootSync
中会调用一个prepareFreshStack
方法,这个方法主要是确定参数本次创建FiberTree
的hostFiber
根节点,但是这个方法最后调用了finishQueueingConcurrentUpdates
函数,这个函数作用就是循环并发队列concurrentQueues
,将之前存储的queue
对象的更新链表从queue.interleaved
中转移到queue.pending
中,代表此节点有等待处理的更新操作。
interleaved
属性主要是插入时临时存储,现在已经转移到pending
属性中:
下面我们直接快进到函数组件的Fiber
节点处理:
进入beginWork
工作的FunctionComponent
处理分支,开始函数组件的更新:
updateFunctionComponent
查看updateFunctionComponent
方法:
js
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderLanes,
) {
let nextChildren;
// 调用函数组件
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
// 更新优化策略
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// 创建子节点
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
可以看见updateFunctionComponent
方法主要有两个处理:
- 调用
renderWithHooks
【函数组件加载也是调用了这个方法】。 - 判断是否满足优化策略,进行组件的更新优化。
renderWithHooks
首先查看renderWithHooks
方法:
js
// packages\react-reconciler\src\ReactFiberHooks.new.js
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;
// 设置为当前渲染中的Fiber
currentlyRenderingFiber = workInProgress;
// 重置函数组件节点的数据
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
// 设置更新的dispatcher【重点】
ReactCurrentDispatcher.current =current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// Component为workInProgress.type 如果是函数组件:就是自身函数
// 调用这个函数,即调用组件,循环生成Element对象,
// 将return返回的Jsx内容转换为reactElement对象,最后返回这个对象
let children = Component(props, secondArg);
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
# 返回函数组件的内容【reactElement对象】
return children;
}
在更新阶段时:
js
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate
renderWithHooks
方法的重点依然是组件的调用,下面我们继续查看Component()
。
js
let children = Component(props, secondArg);
这里的逻辑依然只是重新调用一遍我们定义的函数,最后返回最新的jsx
内容【即reactElement
对象】。
所以这里我们的重点是查看更新阶段对hooks
的处理。
hooks的更新
首先查看useState
的更新:
js
const HooksDispatcherOnUpdate = {
useState: updateState, // 更新state
}
updateState
查看updateState
方法:
js
function updateState(initialState:) {
return updateReducer(basicStateReducer, initialState);
}
继续查看updateReducer
方法:
js
function updateReducer(reducer, initialArg, init?){
// 更新hook工作
const hook = updateWorkInProgressHook();
... // 省略代码
}
这里我们先省略updateReducer
方法的其他代码,只看它的第一行代码逻辑。
调用了一个updateWorkInProgressHook
方法,返回了一个hook
对象。
updateWorkInProgressHook
查看updateWorkInProgressHook
方法:
js
function updateWorkInProgressHook(): Hook {
// 即将处理的hook
let nextCurrentHook: null | Hook;
// 第一此进入更新时,currentHook为null
if (currentHook === null) {
// 取出当前正在更新的函数组件Fiber的旧节点
const current = currentlyRenderingFiber.alternate;
// 更新阶段,current都是存在的
if (current !== null) {
// 将旧节点的memoizedState 设置为下一个处理的Hook
// 将组件加载时,初始化的hook链表取出,memoizedState指向的是hook1
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 从第二个hook更新开始,会走这里
nextCurrentHook = currentHook.next;
}
// 设置下一个工作中的Hook为null
let nextWorkInProgressHook: null | Hook;
// 组件的第一个Hook更新时,workInProgressHook为null
if (workInProgressHook === null) {
// 将当前函数组件Fiber节点的memoizedState 设置为下一个处理的hook【默认是null】
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
// 如果不是第一个Hook,则取next指向的下一个
nextWorkInProgressHook = workInProgressHook.next;
}
// 下一个不为null, 说明当前hook不是最后一个更新的hook,只有最后一个hook更新时,nextWorkInProgressHook才为null
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
if (nextCurrentHook === null) {
throw new Error('Rendered more hooks than during the previous render.');
}
// 更新currentHook 为第一个hook
currentHook = nextCurrentHook;
// 创建一个新的Hook对象,复用原来的内容
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null, // 但是清空了next指向
};
// 第一个hook更新时,workInProgressHook为null,会进入这里
if (workInProgressHook === null) {
// This is the first hook in the list.
// 更新当前函数的组件的memoizedState为第一个hook对象,同时设置为当前正在工作中的hook
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
// 非第一个Hook,直接添加到上一个hook对象的next属性中
workInProgressHook = workInProgressHook.next = newHook;
}
}
// 返回当前正在工作中的hook
return workInProgressHook;
}
就像函数组件的hook
在加载时都会调用一个mountWorkInProgressHook
方法,生成一个hook
链表。而函数组件的hook在更新时也会调用一个updateWorkInProgressHook
方法,生成对应的hook
链表。
所以updateWorkInProgressHook
方法的作用是:确定当前函数Fiber
节点的memoizedState
属性内容,也就是生成它的hook
链表。它的做法就是从current
节点上取出函数组件加载时生成的hook
链表,按顺序取出原来的hook对象,根据原来的对象信息创建生产新的newHook
对象,最后按顺序一个一个添加到新的Fiber
节点的memoizedState
属性上。
下面我们开始看它的具体执行过程:
首先第一个hook
【useState 】的更新处理:
当前为函数组件第一个hook
的更新,所以currentHook
为null
,从当前函数组件Fiber
的alternate
属性取出旧的节点current
,因为函数组件在加载时,生成hook
链表存储在current.memoizedState
属性上,所以这里需要用到current
节点。
然后判断current
是否为null
,在每个函数组件的更新阶段,它的current
节点必然是存在的,所以这里直接取出current.memoizedState
的内容:即函数组件加载时的第一个hook
对象,也就是上图对应的hook1
,这里将hook1
赋值给nextCurrentHook
。
然后判断workInProgressHook
是否为null
,同理当前为第一个hook
的更新,所以workInProgressHook
为null
:
js
if (workInProgressHook === null) {
// 将当前函数组件Fiber节点的memoizedState 设置为下一个处理的hook【默认是null】
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
// 如果不是第一个Hook,则取next指向的下一个
nextWorkInProgressHook = workInProgressHook.next;
}
这时将当前函数组件Fiber
节点的memoizedState
属性赋值给nextWorkInProgressHook
,很明显当前节点的memoizedState
属性为null
,因为函数组件在每次进入renderWithHooks
方法时,都重置了它的memoizedState
属性。
js
export function renderWithHooks() {
...
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
}
所以此时nextWorkInProgressHook
为null
:
下面判断nextWorkInProgressHook
的值是否为null
,来进行不同的处理,当前它的值为null
,进入else
分支处理:
js
function updateWorkInProgressHook(): Hook {
...
if (nextWorkInProgressHook !== null) {
...
} else {
if (nextCurrentHook === null) {
throw new Error('Rendered more hooks than during the previous render.');
}
// 更新currentHook 为第一个hook
currentHook = nextCurrentHook;
// 创建一个新的Hook对象,复用原来的内容
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null, // 但是清空了next指向
};
// 第一个hook更新时,workInProgressHook为null,会进入这里
if (workInProgressHook === null) {
// 更新当前函数的组件的memoizedState为第一个hook对象,同时设置为当前正在工作中的hook
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// 非第一个Hook,直接添加到上一个hook对象的next属性中
workInProgressHook = workInProgressHook.next = newHook;
}
}
// 返回当前正在工作中的hook
return workInProgressHook;
}
直接更新currentHook
为第一个Hook
对象,然后新建一个hook
对象,将currentHook
的所有内容复制到新的hook对象上,但是清空了next
指向。
注意: 这里是一个重点,如果没有清空next
属性,那更新当前函数组件Fiber
节点的memoizedState
属性,直接拿到第一个hook对象,就可以拿到整个hook
链表,然后后续的hook
更新就不需要再调用updateWorkInProgressHook
方法了。但是函数组件为啥不能如此处理呢?因为react不能保证开发者是一定按照规范来使用的hook
,如果开发者将hook
置于条件语句中,在更新阶段出现了原来hook
链表中不存在的hook
对象,则在渲染时就会发生异常,所以react在函数组件更新时需要主动中断hook
对象的next
属性指向,按原来的链表顺序重新一个一个添加,如果出现了不匹配的hook
对象,就会主动抛出异常,提示用户:
js
if (nextCurrentHook === null) {
// 当前渲染时,比原来出现了更多的hook
throw new Error('Rendered more hooks than during the previous render.');
}
最后将第一个newHook
对象赋值给当前函数组件Fiber
节点的memoizedState
属性,后续其他的newHook
对象则添加到上一个hook的next
属性之上,形成一个新的Hook
链表,这就是updateWorkInProgressHook
方法的作用。
计算state
下面我们再回到updateReducer
方法中:
js
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// 返回新的hook对象
const hook = updateWorkInProgressHook();
const queue = hook.queue;
if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}
queue.lastRenderedReducer = reducer; // 还是basicStateReducer,无变化
const current = currentHook; // 旧的hook对象,加载时useState创建的hook对象
// The last rebase update that is NOT part of the base state.
let baseQueue = current.baseQueue;
// The last pending update that hasn't been processed yet.
// 等待处理的更新链表:默认指向的是最后一个update对象
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// pendingQueue不为null,代表有需要处理的更新对象,然后需要将它们添加到baseQueue
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
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;
# 循环处理update更新对象
do {
// An extra OffscreenLane bit is added to updates that were made to
// a hidden tree, so that we can distinguish them from updates that were
// already there when the tree was hidden.
const updateLane = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;
// Check if this update was made while the tree was hidden. If so, then
// it's not a "base" update and we should disregard the extra base lanes
// that were added to renderLanes when we entered the Offscreen tree.
const shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);
if (shouldSkipUpdate) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Update the remaining priority in the queue.
// TODO: Don't need to accumulate this. Instead, we can remove
// renderLanes from the original lanes.
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else {
// This update does have sufficient priority.
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
// Process this update.
if (update.hasEagerState) {
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
// Mark that the fiber performed work, but only if the new state is
// different from the current state.
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
if (baseQueue === null) {
// `queue.lanes` is used for entangling transitions. We can set it back to
// zero once the queue is empty.
queue.lanes = NoLanes;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
在updateWorkInProgressHook
方法调用完成之后,返回值就是useState
对应的hook
对象:
取出hook
对象的queue
队列,如果queue
为null
,则会抛出错误:
js
if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}
后面的逻辑看似比较多,但其实比较简单,而且和this.setState
计算state
的逻辑基本一致。
它的核心逻辑: 按顺序正向循环update
更新队列,定义一个变量newState
来存储最新的state
,然后根据原来state
和update
对象里面的信息计算最新的数据更新变量newState
,每循环一次就会从update
对象的next
属性取出下一个参与计算的update
,直接到所有的update
处理完成。
当前pendingQueue
结构【单向环状链表】:
在类组件中,会根据pendingQueue
的内容重构生成一个新的单向链表,不再是环状,有明确的结束。
和类组件不同的是,函数组件这里并没有额外处理pendingQueue
,而是直接复制给baseQueue
,从baseQueue.next
取出第一个update
对象【即first
】开始计算state
。
所以函数组件这里的do while
循环多了一个结束的判断条件,就是不能等于first
,不然就会陷入无限循环:
js
do {
...
} while (update !== null && update !== first)
然后就是函数组件计算state
的逻辑:
js
// do while循环中,计算state的核心逻辑
if (update.hasEagerState) {
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
- 如果
eagerState
存在,则直接使用eagerState
的值为新的state
。 - 如果不存在,则调用
reducer
【即basicStateReducer】,根据最新的newState
和当前update
对象的action
重新计算state
。
循环结束,更新hook
对象的memoizedState
属性为最新的newState
:
js
// 存储最新的state
hook.memoizedState = newState;
到此,useState hook
的更新程序执行完成,最后返回结果:
js
// 记忆state
return [hook.memoizedState, dispatch];
同时这里我们也可以明白:函数组件useState hook
能够缓存变量结果的原因,因为它的state
存储在hook
对象的属性之中,并且这个属性可以在函数组件重新渲染过程中得到更新。
下面我们开始第二个hook
【useEffect
】的更新:
js
const HooksDispatcherOnUpdate = {
useEffect: updateEffect, // 更新effect
}
updateEffect
查看updateEffect
方法:
js
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
继续查看updateEffectImpl
方法:
js
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook();
// 取出新的依赖
const nextDeps = deps === undefined ? null : deps;
// 重置销毁方法
let destroy = undefined;
if (currentHook !== null) {
// 原来的pushEffect方法
const prevEffect = currentHook.memoizedState;
// 继承原来的destroy方法
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
首先依然是调用一个updateWorkInProgressHook
方法,前面已经详细讲解了它的作用。所以这里调用此方法后,就会新建一个newHook
对象,添加到第一个hook
对象的next
属性之上,形成一个链表,后续如果还有新的newHook
对象则继续执行同样的逻辑。
此时函数Fiber
节点的memoizedState
属性内容为:
然后定义新的依赖变量nextDeps
,重置destroy
方法。
js
if (currentHook !== null) {
...
}
这里的currentHook
肯定是有值的,它对应的是current
节点上useEffect
创建的hook
对象,这里的逻辑主要是从原来的hook
对象上取出之前的依赖数据deps
,然后和新的依赖判断是否相等:
js
// 判断新旧依赖是否相等
if (areHookInputsEqual(nextDeps, prevDeps)) {
...
}
查看areHookInputsEqual
校验方法:
js
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
// 情况1,无依赖参数,每次渲染都会执行副作用
if (prevDeps === null) {
return false;
}
// 情况2,有至少一项依赖参数,循环判断每个依赖是否相等,任何一个依赖变化则会重新执行副作用
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
// 情况3,即空数组的情况,重新渲染不执行副作用
return true;
}
根据校验逻辑,可以分为以下三种情况:
情况一: 如果prevDeps
为null
,代表没有依赖参数,此时直接返回false
,则函数组件每次渲染之后,都会执行此副作用回调。
情况二: 参数存在且有至少一个依赖项,则循环每个依赖,使用Object.is
判断新旧依赖是否变化,任何一个依赖变化都会返回false
,则本次更新后会执行副作用回调,如果都没有变化,则不会执行副作用回调。
情况三: 即参数为空数组的情况,直接返回true
,组件更新不会执行副作用回调。
当前我们依赖为一个空数组,所以满足第三种情况,直接返回true
。
js
if (currentHook !== null) {
...
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
// 上面校验为true的情况下,这里就不会再执行
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
在依赖校验为true
的情况下,即表示没有变化,此时更新hook.memoizedState
属性:
js
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
最后触发return
关键字,updateEffect
方法执行完成。
注意: 依赖变化和没有变化都会重新设置hook.memoizedState
属性,唯一的区别就是第一个参数不同:
js
HookHasEffect | hookFlags,
在依赖变化时,会打上HookHasEffect
的标记,这个值会存储到effect
对象的tag
属性上,表示此effect
对象有需要执行的副作用回调。hookFlags
表示副作用的类型标记,比如HookPassive
,HookLayout
。所以依赖发生变化的唯一区别就是:打上了HookHasEffect
标记。最终会在commit
阶段中执行回调时,根据effect.tag
的值来判断是否执行回调。
到此,函数组件的第二个hook
:useEffect
更新完成。
下面我们开始第三个hook
【useLayouyEffect
】的更新:
js
const HooksDispatcherOnUpdate = {
useLayoutEffect: updateLayoutEffect, // 更新layout
}
updateLayoutEffect
查看updateLayoutEffect
方法:
js
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}
可以发现useEffect
和useLayoutEffect
共用了同一个更新方法updateEffectImpl
,所以它们会执行同样的逻辑处理。
- 调用
updateWorkInProgressHook
方法:创建新hook
对象,此时函数组件Fiber
节点的memoizedState
属性指向更新为:
- 判断
deps
依赖是否变化,如果变化则为对应的effect
对象打上HookHasEffect
的标记。
到此,函数组件更新阶段的hooks
就处理完成。
总结
函数组件更新阶段主要有这两个重点逻辑:
- 根据
updateQueue
更新队列,循环计算state
,最后将最新的state
数据存储到Fiber.memoizedState
属性上并返回。 - 更新
Effect
类hook
时,判断依赖是否变化打上HookHasEffect
标记,最后会在commit
阶段中根据effect.tag
值来决定本次更新是否执行回调。