likes
comments
collection
share

React Hook源码笔记(一):函数组件加载过程React 函数组件加载过程,理解 FiberNode 的 upda

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

博客: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 挂载到 FiberNodememoizedState 中。到这里我们知道 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 对象,最后按顺序一个一个添加到新的 FiberNodememoizedState 属性上。

注意这个组件更新并重新拼接 hook 的过程:

  • 这个过程会更新原有的 hook,将 queue 中的任务取出来执行,并更新状态
  • hook 链的更新过程完全依赖旧有的 hook 链。

这下知道为啥 hook 不能写在条件语句中了。如果组件更新发现 hook 对不上,会直接抛出错误。

小结

整体流程

本篇简单描述了函数组件的加载与更新过程,包含如下操作:

  • renderWithHooks()(核心部分):指定函数组件,创建 FiberNode
  • id 注入:如果存在 id,则插入
  • reconcileChildren():创建子节点

如果是更新节点,则会尝试 bailout 优化组件,满足优化条件,则无需进行最后的两个步骤,直接复用。

钩子节点

renderWithHooks 方法有三个重要逻辑:

  • ReactCurrentDispatcher:获取目前阶段(加载/更新...)的 Dispatcher,执行对应的钩子逻辑
  • Component():执行函数组件,过程中会获取并执行 Dispatcher 的钩子函数
  • finishRenderingHooks():收尾工作,重置一些属性

钩子函数

本篇简单看了一下 setState 钩子的实现,我们只关注所有钩子函数共性的部分:

  • mountWorkInProgressHook:创建一个空的钩子,并将其挂到 FiberNodememoizedState 属性上。
  • updateWorkInProgressHook:复用原有钩子,依此拼接到新 FiberNodememoizedState 属性上,如果是最后一个 hook,则创建新的 hook,复用原来的属性,并指定 next: null

由于 update 阶段复用原有的钩子链表,因此如果条件语句中出现新的钩子,导致钩子链表长度出现问题,就抛错。(如果钩子出现的顺序出了问题,也会出错。)

setState 钩子的逻辑一带而过,下文会详细讲解

重要的属性

FiberNodeHook 上各有几个重要的属性需要记住:

  • FiberNode.memorizedState: 记录 Hook
  • FiberNode.updateQueue: 记录节点需要执行的更新操作,包括监听的事件回调,useState 导致的更新...
  • Hook.memorizedState:记录 Hook 自己的状态
  • Hook.queue: 记录 Hook 的更新队列

其他参考资料

转载自:https://juejin.cn/post/7406169544089239587
评论
请登录