likes
comments
collection
share

React18.2x源码解析:函数组件的加载过程

作者站长头像
站长
· 阅读数 15

在之前的章节我们讲述了FiberTree的创建过程,但是对组件的加载过程这方面的细节没有深入。

本节将深入理解React18.2x 函数组件的具体加载过程。

1,加载阶段

首先准备一个函数组件案例:

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节点加载:

React18.2x源码解析:函数组件的加载过程

执行该Fiber节点的beginWork工作,根据tag类型,进入IndeterminateComponent待定组件的逻辑处理【case IndeterminateComponent】:

每个函数组件的首次加载都是走的IndeterminateComponent分支逻辑,这是因为在创建函数组件Fiber的时候,react没有更新它的tag值,所以它的首次beginWork工作就会进入IndeterminateComponent分支,在mountIndeterminateComponent方法中才会更新它的tag,使函数组件的Fiber在更新阶段执行beginWork时,能够进入正确的FunctionComponent分支。

不了解Fiber创建逻辑的可以查看《React18.2x源码解析(三)reconciler协调流程》的内容。

React18.2x源码解析:函数组件的加载过程

mountIndeterminateComponent

查看mountIndeterminateComponent方法:

// 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,方便函数组件加载的使用:

const props = workInProgress.pendingProps;

然后调用renderWithHooks方法:

value = renderWithHooks();

其实函数组件的加载非常简单,renderWithHooks方法就是函数组件的主要加载逻辑。

这个方法会执行我们定义的函数组件,返回值就是函数中return的内容,也就是jsx内容【处理过后的react-element对象】。

renderWithHooks

查看renderWithHooks方法:

// 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

// 当前渲染中的Fiber节点
currentlyRenderingFiber = workInProgress;

变量currentlyRenderingFiber会在后面的逻辑中被多次用到,这里注意一下它的赋值即可。

接着重置函数组件Fiber的两个属性:

workInProgress.memoizedState = null;
workInProgress.updateQueue = null;

注意: memoizedStateupdateQueue属性是函数组件内容的重点,这两个属性与hooks紧密相连,后面会多次用到。

ReactCurrentDispatcher

然后设置ReactCurrentDispatchercurrent属性值:

ReactCurrentDispatcher.current = (current === null || current.memoizedState === null)
	? HooksDispatcherOnMount
	: HooksDispatcherOnUpdate;

因为当前是首次加载,所以:

ReactCurrentDispatcher.current = HooksDispatcherOnMount

ReactCurrentDispatcher对象是一个全局变量,它是在react源码中的react包定义的:

// packages\react\src\ReactCurrentDispatcher.js

const ReactCurrentDispatcher = {
  current: null,
};
export default ReactCurrentDispatcher;

然后将它包装在一个新的对象中:

// packages\react\src\ReactSharedInternals.js

const ReactSharedInternals = {
  ReactCurrentDispatcher,
  ReactCurrentBatchConfig,
  ReactCurrentOwner,
};
export default ReactSharedInternals;

最后会在react包的入口文件中暴露给外部其他资源包使用:

// packages\react\src\React.js
export {
  ...
  ReactSharedInternals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
}

shared包【通用工具包】会引入这个对象,然后暴露给全局:

// 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工具包来拿到这个对象,所以我们在函数组件加载时才能使用这个对象:

// packages\react-reconciler\src\ReactFiberHooks.new.js

import ReactSharedInternals from 'shared/ReactSharedInternals';
// 拿到ReactCurrentDispatcher对象
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;

知道了ReactCurrentDispatcher对象的由来,我们才能更好地理解它的作用,因为函数组件的每个hook实际就是在调用这个对象中的同名方法,比如useState

// packages\react\src\ReactHooks.js

export function useState(initialState){
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

查看resolveDispatcher方法:

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  // 返回获取到的dispatcher
  return dispatcher;
}

所以这里的dispatcher就是上面的ReactCurrentDispatcher.current对象。

useState(initialState); // 等同于
ReactCurrentDispatcher.current.useState(initialState)

其他的hook也是一样的原理,所以理解ReactCurrentDispatcher对象才能知道hooks的本质。

下面继续回到renderWithHooks方法中。

函数组件调用
let children = Component(props, secondArg);

调用Component其实就是调用我们定义的函数,也就是说函数组件的加载其实就是执行一次我们定义的函数

React18.2x源码解析:函数组件的加载过程

点击单步执行,就可以进入MyFun函数的执行:

React18.2x源码解析:函数组件的加载过程

所以函数组件的加载就是执行一次函数的内容,理解起来也很简单。最后触发return关键字,这里的jsx内容会在react内部进行处理,生成react-element对象,最后返回值就是创建的react元素对象。

React18.2x源码解析:函数组件的加载过程

return children;

最后返回生成的react-element对象,renderWithHooks方法执行完成。

value = renderWithHooks()

回到mountIndeterminateComponent方法,这里的value就是创建的react元素对象。

然后通过一个if语句来区分类组件和函数组件的逻辑:

if (...) {
	// 类组件的处理
} else {
	// 函数组件的处理
    
    // 更新tag为函数组件类型的值,下个逻辑就可以直接进入函数组件的处理【节点更新的时候】
    workInProgress.tag = FunctionComponent;

    # 处理函数组件FirstChild内容【当前为App组件】
    reconcileChildren(null, workInProgress, value, renderLanes);
      
    return workInProgress.child;
}

这里区分类组件与函数组件,主要是通过render函数:

typeof value.render === 'function'

因为类组件必须存在render函数,所以它创建的组件实例instance会存在render方法,而函数组件则不存在。

只是类组件的加载已经不走这里的逻辑了,具体可以查看《React18.2x源码解析:类组件的加载过程》。

函数组件初始化执行完成后,就会更新函数组件Fiber节点的tag值为正确的类型FunctionComponent【后续逻辑函数组件节点便可以进入Function分支了】。

然后根据新建的valuereact元素对象】创建子Fiber节点,最后返回子节点,函数组件的加载过程就基本完成了。

创建Fiber子节点具体过程可以查看《React18.2x源码解析(三)Reconciler协调流程》。

hooks的加载

本小节将主要讲解函数组件加载过程中:hooks的加载处理。

还是上面的案例,首先查看useState的初始化:

React18.2x源码解析:函数组件的加载过程

export function useState(initialState){
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
  // 等同于
  return ReactCurrentDispatcher.current.useState(initialState)
}

所以我们得先查看当前的ReactCurrentDispatcher对象:

ReactCurrentDispatcher.current = HooksDispatcherOnMount

继续查看HooksDispatcherOnMount对象:

// 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

useState: mountState
mountState

查看mountState方法:

// 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方法:

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

React18.2x源码解析:函数组件的加载过程

workInProgressHook = hook;
currentlyRenderingFiber.memoizedState = workInProgressHook;

将新建hook对象赋值给workInProgressHook,表示为正在处理中的hook对象。

同时也将第一个hook对象赋值给当前函数组件Fiber节点的memoizedState属性。

此时函数组件Fiber节点的memoizedState属性指向为:

React18.2x源码解析:函数组件的加载过程

return workInProgressHook;

最后返回新建的hook对象。

继续回到mountState方法中:

... 
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
  initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
...

hook新建完成之后,判断传入的参数initialState是否为函数,如果为函数则调用此函数,将结果赋值为新的initialState

然后设置hook对象的memoizedStatebaseState属性为初始的数据initialState

React18.2x源码解析:函数组件的加载过程

接着看mountState方法剩下的内容:

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:它是一个函数,作用是根据actionlastRenderedState计算最新的state
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和一个修改函数。

const [count, setCount] = useState(1)

到此,函数组件的第一个hookuseState初始化完成。

这里没有展开dispatchSetState方法,我们放在更新阶段再讲解。

最后再看一下当前函数组件Fiber节点的memoizedState属性内容【第一个hook对象】:

React18.2x源码解析:函数组件的加载过程

下面我们开始第二个hookuseEffect】的初始化:

React18.2x源码解析:函数组件的加载过程

// packages\react\src\ReactHooks.js

export function useEffect(create, deps) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, deps);
  // 等同于
  return ReactCurrentDispatcher.current.useEffect(create, deps)
}
const HooksDispatcherOnMount: Dispatcher = {
	useEffect: mountEffect
}
mountEffect

查看mountEffect方法:

// 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方法:

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对象:

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对应的】:

React18.2x源码解析:函数组件的加载过程

所以只能进入else分支:这里要注意连等语句的执行顺序:

a = b = 1;

先执行b=1,然后再把b的结果赋值给a

workInProgressHook = workInProgressHook.next = hook;

所以这里是先将第一个hook对象的next属性指向新建的hook。然后再更新workInProgressHook的值为当前的hook对象。

此时函数组件Fiber节点的memoizedState属性指向为:

React18.2x源码解析:函数组件的加载过程

回到mountEffectImpl方法中:

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
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对象,查看它的内容:

React18.2x源码解析:函数组件的加载过程

  • create属性即为我们传入的回调函数。
  • deps属性是当前useEffect hook的依赖,为一个空数组。
  • destory属性为undefined,它存储的是useEffect hook返回的清理函数或者说销毁函数,但是它不是在这里赋值的,并且当前我们也没有返回这个函数。

然后取出当前函数组件Fiber节点的updateQueue属性内容赋值给变量componentUpdateQueue

然后判断componentUpdateQueue是否为null

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方法都会重置一些属性:

workInProgress.updateQueue = null;

所以当前变量componentUpdateQueuenull,然后调用createFunctionComponentUpdateQueue方法更新它的值。

这里的逻辑本质就是: 在处理第一个effect相关的hook时,需要初始化函数组件Fiber节点的updateQueue属性。

所以这里我们还需要查看createFunctionComponentUpdateQueue方法:

function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
  return {
    lastEffect: null,
    stores: null,
  };
}

这个方法直接返回一个初始的对象,所以当前函数组件Fiber节点的updateQueue属性变为:

React18.2x源码解析:函数组件的加载过程

componentUpdateQueue.lastEffect = effect.next = effect;

最后将当前创建的effect对象的next属性指向了自身,且同时更新updateQueue.lastEffect属性为当前effect对象,由此形成一个单向环状链表。

所以此时函数组件Fiber节点的updateQueue属性更新为:

React18.2x源码解析:函数组件的加载过程

React18.2x源码解析:函数组件的加载过程

pushEffect方法最后,返回当前创建的effect对象:

return effect;

再回到mountEffectImpl方法中:

hook.memoizedState = pushEffect()

所以hook对象的memoizedState属性值为一个effect对象。

从这里我们可以发现,虽然每个hook对象都是相同的属性,但是不同的hook类型它存储的内容却完全不同。

  • useState创建的hook对象,它的memoizedState属性存储的为数据state
  • useEffect创建的hook对象,它的memoizedState属性存储的为一个effect对象。

注意:这里不要将hook对象的memoizedState属性和Fiber节点的memoizedState属性搞混了。

到此,函数组件的第二个hookuseEffect初始化完成。

下面我们开始第三个hookuseLayoutEffect】的初始化:

React18.2x源码解析:函数组件的加载过程

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)
}
const HooksDispatcherOnMount: Dispatcher = {
	useLayoutEffect: mountLayoutEffect
}
mountLayoutEffect

查看mountLayoutEffect方法:

// 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);
}

可以发现useEffectuseLayoutEffect共用了同一个加载方法mountEffectImpl,所以它们会执行同样的逻辑处理。

  • hook对象创建和处理,此时函数组件Fiber节点的memoizedState属性指向更新为:

React18.2x源码解析:函数组件的加载过程

  • effect对象创建和处理,依然是pushEffect方法的调用:
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,将effect2next属性指向为之前的effect对象。

此时函数组件Fiber节点的updateQueue属性指向更新为:

React18.2x源码解析:函数组件的加载过程

到此,函数组件加载阶段的hooks就处理完成。

commit阶段

前面全部的加载逻辑都是在Fiber Reconciler协调流程中执行的,即函数组件大部分的加载逻辑都是在reconciler协调流程中完成的【更新阶段同理】,还有剩下的一部分逻辑在commit阶段之中处理,这里我们继续讲解。

这里简单介绍一下commit阶段的内容,更多处理逻辑可以查看《React18.2x源码解析(四)commit阶段》。

函数组件剩下的加载即是在commit中关于Effect hook的副作用回调,即useEffectuseLayoutEffect

commit阶段的逻辑主要分为三个子阶段内容:

  • BeforeMutation
  • Mutation
  • Layout
function commitRootImpl() {
  // 发起调度处理useEffect回调
  scheduleCallback(NormalSchedulerPriority, () => {
	// 这个回调就是处理useEffect的
	flushPassiveEffects();
  });
  
  // 1,BeforeMutation阶段
  commitBeforeMutationEffects()
  // 2,Mutation阶段,渲染真实DOM加载到页面
  commitMutationEffects()
  // 3,Layout阶段
  commitLayoutEffects()
}

commit阶段的内容都是同步执行,在进入具体的执行之前,都会先调用scheduleCallback方法发起一个新的调度,即创建一个新的任务task,最后会生成一个新的宏任务来异步处理副作用【即执行useEffect的回调钩子】。

知道了useEffect的回调处理,我们再查看useLayoutEffect的回调处理。

Layout阶段

function commitLayoutEffectOnFiber(
  if ((finishedWork.flags & LayoutMask) !== NoFlags) {
    // 根据组件类型
    switch (finishedWork.tag) {
      // 函数组件的处理
      case FunctionComponent: {
          // 传入的是layout相关的flag标记
          commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
      }
)

查看commitHookEffectListMount方法:

// 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存储的是副作用相关的链表,回顾之前的内容:

React18.2x源码解析:函数组件的加载过程

定义一个lastEffect变量存储updateQueue.lastEffect的内容,即最后一个effect对象。

判断lastEffect是否为null,如果lastEffect为null,代表当前函数组件没有使用过effect相关的hook

当前肯定是有值的,继续向下执行。从lastEffect.next中取出第一个effect对象,开始按顺序循环处理副作用。

do {
  if ((effect.tag & flags) === flags) {
	// Mount
	const create = effect.create;
	// 执行回调函数
	effect.destroy = create();
  }
  effect = effect.next;
} while (effect !== firstEffect);

注意在执行之前有一个条件判断,只有存在effect相关的flags标记才会执行对应副作用回调。

而在之前hook加载是有进行设置的:

hook.memoizedState = pushEffect(
  HookHasEffect | hookFlags, // HookHasEffect标记就是表示有需要执行副作用
  ...
}

在函数组件加载阶段时,每个useEffectuseLayoutEffect都有打上HookHasEffect的标记,表示在加载阶段都会默认执行一次。

需要注意的是:之前commitHookEffectListMount传入的是与Layout相关的flags标记。

commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); // Layout

所以这里只有layout hook的回调才能执行,第一个effect对象对应的是useEffect,不满足判断条件:

effect = effect.next;

从当前effect对象的next属性取出下一个effect对象,开始第二次循环。

第二个effect对象对应的是useLayoutEffect,满足判断条件,执行它的回调函数。

const create = effect.create;
// 执行回调函数
effect.destroy = create();

React18.2x源码解析:函数组件的加载过程

此时控制台就会打印出对应的日志内容,到此hook相关的回调处理完成,函数组件加载逻辑全部执行完成。

总结

函数组件加载阶段:难点在于对hooks的处理,本案例以三个常见的hook解析了它们首次加载的逻辑。

2,更新阶段

点击案例的更新按钮,触发一次组件更新,进入函数组件的更新阶段。

React18.2x源码解析:函数组件的加载过程

这里的setCount方法就是之前useState hook加载返回的dispatch方法:

  const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue)
  return [hook.memoizedState, dispatch];

注意: 下面开始第一个setCount逻辑【这部分的逻辑可以对比查看类组件的this.setState基本一致】。

dispatchSetState

查看dispatchSetState方法:

// 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】优化策略校验:

// 当前的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对象的定义:

const update = {
  lane,
  action, // state数据 1, 也可以是一个函数
  hasEagerState: false,
  eagerState: null, // 急切的state 根据action计算
  next: null, // 指向下一个update对象
};

这里的action属性存储的就是setCount的传入参数,也就是新的state数据。

React18.2x源码解析:函数组件的加载过程

然后调用enqueueConcurrentHookUpdate方法,将update对象添加到队列。

const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);

查看useState对应的hook对象的queue属性内容:

React18.2x源码解析:函数组件的加载过程

它的lastRenderedState属性代表上一次的state,而它的interleavedpending属性目前都为null

下面查看enqueueConcurrentHookUpdate方法:

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;
}

首先取出queueinterleaved属性,如果interleavednull,表示为当前的update1为第一个入队的更新对象,将此update1next属性指向自身,形成一个单向环状链表。

React18.2x源码解析:函数组件的加载过程

React18.2x源码解析:函数组件的加载过程

然后调用了一个pushConcurrentUpdateQueue方法,这个方法的作用是将queue备份到一个并发队列concurrentQueues之中,方便在之后将queue.interleaved的内容转移到queue.pending之上。

interleaved只是一个临时存储update链表的属性,最终会在更新之前转移到pending属性之上用于计算。

pushConcurrentUpdateQueue(sharedQueue);

最后设置queue.interleaved为当前的update对象。

至此,第一个setCount操作的update1入队处理完成。

回到dispatchSetState方法中,这个方法最后会调用scheduleUpdateOnFiber函数进入更新的调度程序。

在这里你可以发现:无论是函数组件还是类组件的更新,在更新调度方面都是同样的处理逻辑。

click事件触发的更新任务为同步任务,下面直接快进,来到同步任务的处理:

React18.2x源码解析:函数组件的加载过程

这里首先会调用scheduleSyncCallback方法,将处理同步任务的performSyncWorkOnRoot回调函数添加到同步队列syncQueue

然后在支持微任务的环境下:就会使用scheduleMicrotask方法,这个方法等同于Promise.then

Promise.then(flushSyncCallbacks)

这里就会将冲刷同步任务队列syncQueueflushSyncCallbacks函数添加到微任务中,然后继续向下执行。

注意: 我们在DOM事件中执行了两次setCount操作:

function handleClick() {
  setCount(2)
  setCount(3)
}

上面第一次setCount执行完成,除了处理update对象之外,调度方面的逻辑主要就是将冲刷同步任务队列的函数添加到了微任务之中,等待异步处理。

但是我们的同步代码还没有执行完成,还有第二个setCount等待执行:

React18.2x源码解析:函数组件的加载过程

再次进入dispatchSetState方法:

React18.2x源码解析:函数组件的加载过程

第二次调用setCount还会新建一个update更新对象【update2】,依然会执行入队操作。

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
  • 将第一个update1next属性指向当前的update2对象。

最后将queue.interleaved设置为最新的update2

React18.2x源码解析:函数组件的加载过程

React18.2x源码解析:函数组件的加载过程

至此,update2也已经入队完成,此时queue.interleaved指向的就是最新的update2

回到dispatchSetState方法中,最后还是会调用scheduleUpdateOnFiber函数进入更新的调度程序。

React18.2x源码解析:函数组件的加载过程

但是这次在调度时发现新的调度优先级和现存的优先级相同,可以归为同一个任务处理,就不会再重复调度。

最后触发return关键字,结束本次同步代码的执行。

flushSyncCallbacks

来微任务队列,开始执行flushSyncCallbacks方法:

React18.2x源码解析:函数组件的加载过程

可以看出syncQueue同步任务队列之中只有一个任务,即performSyncWorkOnRoot函数。

后面的逻辑就直接简单介绍了,方便快速进入到函数组件的更新程序:

callback = callback(isSync);

循环syncQueue队列,从中取出callback回调函数,然后调用回调函数【performSyncWorkOnRoot】。

直接进入到performSyncWorkOnRoot方法中:

function performSyncWorkOnRoot(root) {
  ...
  var exitStatus = renderRootSync(root, lanes);
}

调用renderRootSync方法,开始FiberTree的创建过程。

在这之前,还有一个处理要注意:

function renderRootSync() {
    ...
	prepareFreshStack()
}
function prepareFreshStack() {
	...
	finishQueueingConcurrentUpdates()
}

renderRootSync中会调用一个prepareFreshStack方法,这个方法主要是确定参数本次创建FiberTreehostFiber根节点,但是这个方法最后调用了finishQueueingConcurrentUpdates函数,这个函数作用就是循环并发队列concurrentQueues,将之前存储的queue对象的更新链表从queue.interleaved中转移到queue.pending中,代表此节点有等待处理的更新操作。

interleaved属性主要是插入时临时存储,现在已经转移到pending属性中:

React18.2x源码解析:函数组件的加载过程

React18.2x源码解析:函数组件的加载过程

下面我们直接快进到函数组件的Fiber节点处理:

React18.2x源码解析:函数组件的加载过程

进入beginWork工作的FunctionComponent处理分支,开始函数组件的更新:

React18.2x源码解析:函数组件的加载过程

updateFunctionComponent

查看updateFunctionComponent方法:

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方法:

// 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;
}

在更新阶段时:

ReactCurrentDispatcher.current = HooksDispatcherOnUpdate

renderWithHooks方法的重点依然是组件的调用,下面我们继续查看Component()

let children = Component(props, secondArg);

这里的逻辑依然只是重新调用一遍我们定义的函数,最后返回最新的jsx内容【即reactElement对象】。

所以这里我们的重点是查看更新阶段对hooks的处理。

hooks的更新

首先查看useState的更新:

React18.2x源码解析:函数组件的加载过程

const HooksDispatcherOnUpdate = {
	useState: updateState, // 更新state
}
updateState

查看updateState方法:

function updateState(initialState:) {
  return updateReducer(basicStateReducer, initialState);
}

继续查看updateReducer方法:

function updateReducer(reducer, initialArg, init?){
  // 更新hook工作
  const hook = updateWorkInProgressHook();
 
  ... // 省略代码

}

这里我们先省略updateReducer方法的其他代码,只看它的第一行代码逻辑。

调用了一个updateWorkInProgressHook方法,返回了一个hook对象。

updateWorkInProgressHook

查看updateWorkInProgressHook方法:

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属性上。

React18.2x源码解析:函数组件的加载过程

下面我们开始看它的具体执行过程:

首先第一个hook【useState 】的更新处理:

React18.2x源码解析:函数组件的加载过程

当前为函数组件第一个hook的更新,所以currentHooknull,从当前函数组件Fiberalternate属性取出旧的节点current,因为函数组件在加载时,生成hook链表存储在current.memoizedState属性上,所以这里需要用到current节点。

然后判断current是否为null,在每个函数组件的更新阶段,它的current节点必然是存在的,所以这里直接取出current.memoizedState的内容:即函数组件加载时的第一个hook对象,也就是上图对应的hook1,这里将hook1赋值给nextCurrentHook

React18.2x源码解析:函数组件的加载过程

然后判断workInProgressHook是否为null,同理当前为第一个hook的更新,所以workInProgressHooknull:

  if (workInProgressHook === null) {
    // 将当前函数组件Fiber节点的memoizedState 设置为下一个处理的hook【默认是null】
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    // 如果不是第一个Hook,则取next指向的下一个
    nextWorkInProgressHook = workInProgressHook.next;
  }

这时将当前函数组件Fiber节点的memoizedState属性赋值给nextWorkInProgressHook,很明显当前节点的memoizedState属性为null,因为函数组件在每次进入renderWithHooks方法时,都重置了它的memoizedState属性。

export function renderWithHooks() {
  ...
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
}

所以此时nextWorkInProgressHooknull

React18.2x源码解析:函数组件的加载过程

下面判断nextWorkInProgressHook的值是否为null,来进行不同的处理,当前它的值为null,进入else分支处理:

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对象,就会主动抛出异常,提示用户:

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方法中:

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对象:

React18.2x源码解析:函数组件的加载过程

取出hook对象的queue队列,如果queuenull,则会抛出错误:

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,然后根据原来stateupdate对象里面的信息计算最新的数据更新变量newState,每循环一次就会从update对象的next属性取出下一个参与计算的update,直接到所有的update处理完成。

当前pendingQueue结构【单向环状链表】:

React18.2x源码解析:函数组件的加载过程

在类组件中,会根据pendingQueue的内容重构生成一个新的单向链表,不再是环状,有明确的结束。

React18.2x源码解析:函数组件的加载过程

和类组件不同的是,函数组件这里并没有额外处理pendingQueue,而是直接复制给baseQueue,从baseQueue.next取出第一个update对象【即first】开始计算state

所以函数组件这里的do while循环多了一个结束的判断条件,就是不能等于first,不然就会陷入无限循环:

do {
 ...
} while (update !== null && update !== first)

然后就是函数组件计算state的逻辑:

// 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

// 存储最新的state
hook.memoizedState = newState;

到此,useState hook的更新程序执行完成,最后返回结果:

// 记忆state
return [hook.memoizedState, dispatch];

同时这里我们也可以明白:函数组件useState hook能够缓存变量结果的原因,因为它的state存储在hook对象的属性之中,并且这个属性可以在函数组件重新渲染过程中得到更新。

下面我们开始第二个hookuseEffect】的更新:

React18.2x源码解析:函数组件的加载过程

const HooksDispatcherOnUpdate = {
	useEffect: updateEffect, // 更新effect
}
updateEffect

查看updateEffect方法:

function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}

继续查看updateEffectImpl方法:

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属性内容为:

React18.2x源码解析:函数组件的加载过程

然后定义新的依赖变量nextDeps,重置destroy方法。

if (currentHook !== null) {
	...
}

这里的currentHook肯定是有值的,它对应的是current节点上useEffect创建的hook对象,这里的逻辑主要是从原来的hook对象上取出之前的依赖数据deps,然后和新的依赖判断是否相等:

// 判断新旧依赖是否相等
if (areHookInputsEqual(nextDeps, prevDeps)) {
  ...
}

查看areHookInputsEqual校验方法:

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;
}

根据校验逻辑,可以分为以下三种情况:

情况一: 如果prevDepsnull,代表没有依赖参数,此时直接返回false,则函数组件每次渲染之后,都会执行此副作用回调。

情况二: 参数存在且有至少一个依赖项,则循环每个依赖,使用Object.is判断新旧依赖是否变化,任何一个依赖变化都会返回false,则本次更新后会执行副作用回调,如果都没有变化,则不会执行副作用回调。

情况三: 即参数为空数组的情况,直接返回true,组件更新不会执行副作用回调。

当前我们依赖为一个空数组,所以满足第三种情况,直接返回true

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属性:

hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);

最后触发return关键字,updateEffect方法执行完成。

注意: 依赖变化和没有变化都会重新设置hook.memoizedState属性,唯一的区别就是第一个参数不同:

HookHasEffect | hookFlags,

在依赖变化时,会打上HookHasEffect的标记,这个值会存储到effect对象的tag属性上,表示此effect对象有需要执行的副作用回调。hookFlags表示副作用的类型标记,比如HookPassiveHookLayout。所以依赖发生变化的唯一区别就是:打上了HookHasEffect标记。最终会在commit阶段中执行回调时,根据effect.tag的值来判断是否执行回调。

到此,函数组件的第二个hookuseEffect更新完成。

下面我们开始第三个hookuseLayouyEffect】的更新:

const HooksDispatcherOnUpdate = {
	useLayoutEffect: updateLayoutEffect, // 更新layout
}
updateLayoutEffect

查看updateLayoutEffect方法:

function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}

可以发现useEffectuseLayoutEffect共用了同一个更新方法updateEffectImpl,所以它们会执行同样的逻辑处理。

  • 调用updateWorkInProgressHook方法:创建新hook对象,此时函数组件Fiber节点的memoizedState属性指向更新为:

React18.2x源码解析:函数组件的加载过程

  • 判断deps依赖是否变化,如果变化则为对应的effect对象打上HookHasEffect的标记。

到此,函数组件更新阶段的hooks就处理完成。

总结

函数组件更新阶段主要有这两个重点逻辑:

  • 根据updateQueue更新队列,循环计算state,最后将最新的state数据存储到Fiber.memoizedState属性上并返回。
  • 更新Effecthook时,判断依赖是否变化打上HookHasEffect标记,最后会在commit阶段中根据effect.tag值来决定本次更新是否执行回调。