乐趣区

React16源码解析二创建更新

React 源码解析系列文章欢迎阅读:
React16 源码解析 (一)- 图解 Fiber 架构
React16 源码解析(二)- 创建更新
React16 源码解析(三)-ExpirationTime
React16 源码解析(四)-Scheduler
React16 源码解析(五)- 更新流程渲染阶段 1
React16 源码解析(六)- 更新流程渲染阶段 2
React16 源码解析(七)- 更新流程渲染阶段 3
React16 源码解析(八)- 更新流程提交阶段
正在更新中 …

在 React 中创建更新主要有下面三种方式:

1、ReactDOM.render() || hydrate
2、setState
3、forceUpdate

注:
除了上面的还有 react 16.8 引进的 hooks 中的 useState,这个我们后续再讲。
hydrate 是服务端渲染相关的,这块我并不会重点讲解。

一、ReactDOM.render()

调用 legacyRenderSubtreeIntoContainer()

const ReactDOM: Object = {
  // ......
  render(
    element: React$Element<any>,// 传入的 React 组件
    container: DOMContainer,// 挂载的容器节点
    callback: ?Function,// 挂载后的回调函数
  ) {
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      false,
      callback,
    );
  },
  // ......
}

legacyRenderSubtreeIntoContainer

1、root = 创建 ReactRoot
2、调用 root.render()

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,//null
  children: ReactNodeList,// 传入进来需要挂在的 class component
  container: DOMContainer,// 根节点
  forceHydrate: boolean,//false
  callback: ?Function,// 挂载完成后的回调函数
) {
  // ......
  // 是否存在根节点   初次渲染是不存在根节点的
  let root: Root = (container._reactRootContainer: any);
  if (!root) {
    // 1、创建 ReactRoot 赋值给 container._reactRootContainer 和 root(这里发生了很多事,一件很重要很重要的事   生成了 fiber 结构树。。)root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    // ......
  } else {
    // ......
    // 2、调用 root.render()
    root.render(children, callback);
  }
  return DOMRenderer.getPublicRootInstance(root._internalRoot);
}

legacyCreateRootFromDOMContainer

1、清除所有子元素
2、创建 new ReactRoot 节点

function legacyCreateRootFromDOMContainer(
  container: DOMContainer,// 根节点
  forceHydrate: boolean,//false
): Root {
  //  服务端渲染相关 是否合并原先存在的 dom 节点 一般是 false
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // 1、清除所有子元素,通过 container.lastchild 循环来清除 container 的所有内容,因为我们的属于首次渲染,container 里边不包含任何元素
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    while ((rootSibling = container.lastChild)) {
      // ......
      container.removeChild(rootSibling);
    }
  }
  // Legacy roots are not async by default.
  const isConcurrent = false;
  // 2、创建 new ReactRoot 节点
  return new ReactRoot(container, isConcurrent, shouldHydrate);
}

ReactRoot

从 ReactRoot 中,我们把 createContainer 返回值赋给了 实例的_internalRoot,往下看 createContainer

function ReactRoot(
  container: Container,
  isConcurrent: boolean,
  hydrate: boolean,
) {
  // 这里创建了一个 FiberRoot
  const root = DOMRenderer.createContainer(container, isConcurrent, hydrate);
  this._internalRoot = root;
}

createContainer

从 createContainer 看出,createContainer 实际上是直接返回了 createFiberRoot, 而 createFiberRoot 则是通过 createHostRootFiber 函数的返回值 uninitializedFiber,并将其赋值在 root 对象的 current 上,这里需要注意一个点就是,uninitializedFiber 的 stateNode 的值是 root,即他们互相引用。

创建一个 RootFiber -> createHostRootFiber() -> createFiber() -> new FiberNode()

这里创建的这个 RootFiber 里面的绝大部分属性都是初始值 null 或者是 NoWork。所以具体代码我就没有贴出来了。
这里我提下有意义的点:
RootFiber 上的 tag 会被赋值为 HostRoot。这个之后会用来判断节点类型。

还有这里创建的 FiberRoot 还有一个 containerInfo 置为 ReactDOM.render 第二个参数传入进来的容器节点。这个后续挂载的时候会用到。

export function createContainer(
  containerInfo: Container,
  isConcurrent: boolean,
  hydrate: boolean,
): OpaqueRoot {return createFiberRoot(containerInfo, isConcurrent, hydrate);
}

export function createFiberRoot(
  containerInfo: any,
  isConcurrent: boolean,
  hydrate: boolean,
): FiberRoot {
  // 1、创建了一个 RootFiber
  const uninitializedFiber = createHostRootFiber(isConcurrent);
  // 2、互相引用
  // RootFiber.stateNode --> FiberRoot
  // FiberRoot.current --> RootFiber
  let root;
  root = {
    current: uninitializedFiber,
    containerInfo: containerInfo,
    // ......
  }
  uninitializedFiber.stateNode = root;
  // 3、return 了这个 FiberRoot
  return ((root: any): FiberRoot);
}

FiberRoot

这里牵扯到两种 react 中的数据结构,第一个 FiberRoot,也就是上面 createFiberRoot 函数返回的对象。

type BaseFiberRootProperties = {|
  // root 节点,render 方法接收的第二个参数
  containerInfo: any,
  // 只有在持久更新中会用到,也就是不支持增量更新的平台,react-dom 不会用到
  pendingChildren: any,
  // 当前应用对应的 Fiber 对象,是 Root Fiber
  current: Fiber,

  // 一下的优先级是用来区分
  // 1) 没有提交 (committed) 的任务
  // 2) 没有提交的挂起任务
  // 3) 没有提交的可能被挂起的任务
  // 我们选择不追踪每个单独的阻塞登记,为了兼顾性能
  // The earliest and latest priority levels that are suspended from committing.
  // 最老和新的在提交的时候被挂起的任务
  earliestSuspendedTime: ExpirationTime,
  latestSuspendedTime: ExpirationTime,
  // The earliest and latest priority levels that are not known to be suspended.
  // 最老和最新的不确定是否会挂起的优先级(所有任务进来一开始都是这个状态)earliestPendingTime: ExpirationTime,
  latestPendingTime: ExpirationTime,
  // The latest priority level that was pinged by a resolved promise and can
  // be retried.
  // 最新的通过一个 promise 被 reslove 并且可以重新尝试的优先级
  latestPingedTime: ExpirationTime,

  // 如果有错误被抛出并且没有更多的更新存在,我们尝试在处理错误前同步重新从头渲染
  // 在 `renderRoot` 出现无法处理的错误时会被设置为 `true`
  didError: boolean,

  // 正在等待提交的任务的 `expirationTime`
  pendingCommitExpirationTime: ExpirationTime,
  // 已经完成的任务的 FiberRoot 对象,如果你只有一个 Root,那他永远只可能是这个 Root 对应的 Fiber,或者是 null
  // 在 commit 阶段只会处理这个值对应的任务
  finishedWork: Fiber | null,
  // 在任务被挂起的时候通过 setTimeout 设置的返回内容,用来下一次如果有新的任务挂起时清理还没触发的 timeout
  timeoutHandle: TimeoutHandle | NoTimeout,
  // 顶层 context 对象,只有主动调用 `renderSubtreeIntoContainer` 时才会有用
  context: Object | null,
  pendingContext: Object | null,
  // 用来确定第一次渲染的时候是否需要融合
  +hydrate: boolean,
  // 当前 root 上剩余的过期时间
  // TODO: 提到 renderer 里面区处理
  nextExpirationTimeToWorkOn: ExpirationTime,
  // 当前更新对应的过期时间
  expirationTime: ExpirationTime,
  // List of top-level batches. This list indicates whether a commit should be
  // deferred. Also contains completion callbacks.
  // TODO: Lift this into the renderer
  // 顶层批次(批处理任务?)这个变量指明一个 commit 是否应该被推迟
  // 同时包括完成之后的回调
  // 貌似用在测试的时候?firstBatch: Batch | null,
  // root 之间关联的链表结构
  nextScheduledRoot: FiberRoot | null,
|};

Fiber

这里就是 createHostRootFiber 函数返回的 fiber 对象。注意这里其实每一个节点都对应一个 fiber 对象,不是 Root 专有的哦。

// Fiber 对应一个组件需要被处理或者已经处理了,一个组件可以有一个或者多个 Fiber
type Fiber = {|
  // 标记不同的组件类型
  // export const FunctionComponent = 0;
  // export const ClassComponent = 1;
  // export const IndeterminateComponent = 2; // Before we know whether it is function or class
  // export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
  // export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
  // export const HostComponent = 5;
  // export const HostText = 6;
  // export const Fragment = 7;
  // export const Mode = 8;
  // export const ContextConsumer = 9;
  // export const ContextProvider = 10;
  // export const ForwardRef = 11;
  // export const Profiler = 12;
  // export const SuspenseComponent = 13;
  // export const MemoComponent = 14;
  // export const SimpleMemoComponent = 15;
  // export const LazyComponent = 16;
  // export const IncompleteClassComponent = 17;
  tag: WorkTag,

  // ReactElement 里面的 key
  key: null | string,

  // ReactElement.type,标签类型,也就是我们调用 `createElement` 的第一个参数
  elementType: any,

  // The resolved function/class/ associated with this fiber.
  // 异步组件 resolved 之后返回的内容,一般是 `function` 或者 `class`
  type: any,

  // The local state associated with this fiber.
  // 跟当前 Fiber 相关本地状态(比如浏览器环境就是 DOM 节点)stateNode: any,

  // 指向他在 Fiber 节点树中的 `parent`,用来在处理完这个节点之后向上返回
  return: Fiber | null,

  // 单链表树结构
  // 指向自己的第一个子节点
  child: Fiber | null,
  // 指向自己的兄弟结构
  // 兄弟节点的 return 指向同一个父节点
  sibling: Fiber | null,
  index: number,

  // ref 属性
  ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,

  // 新的变动带来的新的 props
  pendingProps: any, 
  // 上一次渲染完成之后的 props
  memoizedProps: any,

  // 该 Fiber 对应的组件产生的 Update 会存放在这个队列里面
  updateQueue: UpdateQueue<any> | null,

  // 上一次渲染的时候的 state
  memoizedState: any,

  // 一个列表,存放这个 Fiber 依赖的 context
  firstContextDependency: ContextDependency<mixed> | null,

  // 用来描述当前 Fiber 和他子树的 `Bitfield`
  // 共存的模式表示这个子树是否默认是异步渲染的
  // Fiber 被创建的时候他会继承父 Fiber
  // 其他的标识也可以在创建的时候被设置
  // 但是在创建之后不应该再被修改,特别是他的子 Fiber 创建之前
  mode: TypeOfMode,

  // Effect
  // 用来记录 Side Effect
  // Don't change these two values. They're used by React Dev Tools.
  // export const NoEffect = /*              */ 0b00000000000;
  // export const PerformedWork = /*         */ 0b00000000001;

  // You can change the rest (and add more).
  // export const Placement = /*             */ 0b00000000010;
  // export const Update = /*                */ 0b00000000100;
  // export const PlacementAndUpdate = /*    */ 0b00000000110;
  // export const Deletion = /*              */ 0b00000001000;
  // export const ContentReset = /*          */ 0b00000010000;
  // export const Callback = /*              */ 0b00000100000;
  // export const DidCapture = /*            */ 0b00001000000;
  // export const Ref = /*                   */ 0b00010000000;
  // export const Snapshot = /*              */ 0b00100000000;

  // Update & Callback & Ref & Snapshot
  // export const LifecycleEffectMask = /*   */ 0b00110100100;

  // Union of all host effects
  // export const HostEffectMask = /*        */ 0b00111111111;

  // export const Incomplete = /*            */ 0b01000000000;
  // export const ShouldCapture = /*         */ 0b10000000000;
  effectTag: SideEffectTag,

  // 单链表用来快速查找下一个 side effect
  nextEffect: Fiber | null,

  // 子树中第一个 side effect
  firstEffect: Fiber | null,
  // 子树中最后一个 side effect
  lastEffect: Fiber | null,

  // 代表任务在未来的哪个时间点应该被完成
  // 不包括他的子树产生的任务
  expirationTime: ExpirationTime,

  // 快速确定子树中是否有不在等待的变化
  childExpirationTime: ExpirationTime,

  // 在 Fiber 树更新的过程中,每个 Fiber 都会有一个跟其对应的 Fiber
  // 我们称他为 `current <==> workInProgress`
  // 在渲染完成之后他们会交换位置
  alternate: Fiber | null,

  // 下面是调试相关的,收集每个 Fiber 和子树渲染时间的

  actualDuration?: number,

  // If the Fiber is currently active in the "render" phase,
  // This marks the time at which the work began.
  // This field is only set when the enableProfilerTimer flag is enabled.
  actualStartTime?: number,

  // Duration of the most recent render time for this Fiber.
  // This value is not updated when we bailout for memoization purposes.
  // This field is only set when the enableProfilerTimer flag is enabled.
  selfBaseDuration?: number,

  // Sum of base times for all descedents of this Fiber.
  // This value bubbles up during the "complete" phase.
  // This field is only set when the enableProfilerTimer flag is enabled.
  treeBaseDuration?: number,

  // Conceptual aliases
  // workInProgress : Fiber ->  alternate The alternate used for reuse happens
  // to be the same as work in progress.
  // __DEV__ only
  _debugID?: number,
  _debugSource?: Source | null,
  _debugOwner?: Fiber | null,
  _debugIsCurrentlyTiming?: boolean,
|};

root.render()

经过上面的步骤,创建好了 ReactRoot。初始化完成了。下面开始 root.render。
我们回到 legacyRenderSubtreeIntoContainer 函数,前面一堆讲解的是调用 legacyCreateRootFromDOMContainer 方法我们得到了一个 ReactRoot 对象。reactRoot 的原型上面我们找到了 render 方法:

ReactRoot.prototype.render = function(
  children: ReactNodeList,
  callback: ?() => mixed,): Work {
  // 这个就是我们上面创建的 FiberRoot 对象
  const root = this._internalRoot;
  // ......
  DOMRenderer.updateContainer(children, root, null, work._onCommit);
  return work;
};

updateContainer

这个函数里面使用了 currentTime 和 expirationTime, currentTime 是用来计算 expirationTime 的,expirationTime 代表着优先级,这个留在后续分析。后续紧接着调用了 updateContainerAtExpirationTime。

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  // 这个 current 就是 FiberRoot 对应的 RootFiber
  const current = container.current;
  const currentTime = requestCurrentTime();
  const expirationTime = computeExpirationForFiber(currentTime, current);
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    callback,
  );
}

注:这个函数在 ReactFiberReconciler.js 里面。

updateContainerAtExpirationTime

这里将 current(即 Fiber 实例)提取出来,并作为参数传入调用 scheduleRootUpdate

export function updateContainerAtExpirationTime(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  expirationTime: ExpirationTime,
  callback: ?Function,
) {
  const current = container.current;
  // ......
  return scheduleRootUpdate(current, element, expirationTime, callback);
}

scheduleRootUpdate

这个函数主要执行了两个操作:
1、创建更新 createUpdate 并放到更新队列 enqueueUpdate,创建更新的具体细节稍后再讲哈。因为待会我们会发现其他地方也用到了。
2、个是执行 sheculeWork 函数,进入 React 异步渲染的核心:React Scheduler,这个我后续文章详细讲解。

function scheduleRootUpdate(
  current: Fiber,
  element: ReactNodeList,
  expirationTime: ExpirationTime,
  callback: ?Function,
) {
  // ......
  // 1、创建一个 update 对象
  const update = createUpdate(expirationTime);
  update.payload = {element};

  // ......
  // 2、将刚创建的 update 对象入队到 fiber.updateQueue 队列中
  enqueueUpdate(current, update);

  // 3、开始进入 React 异步渲染的核心:React Scheduler
  scheduleWork(current, expirationTime);
  return expirationTime;
}

图解

以上的过程我画了张图:

二、setState

虽然我还没有讲解到 class component 的渲染过程,但是这个不影响我现在要讨论的内容~
如下我们调用 this.setState 方法的时候,调用了 this.updater.enqueueSetState

Component.prototype.setState = function(partialState, callback) {this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

先不管 this.updater 什么时候被赋值的,直接看到 ReactFiberClassComponent.js 中的 enqueueSetState,这就是我们调用 setState 执行的 enqueueSetState 方法。

const classComponentUpdater = {
  // ......
  enqueueSetState(inst, payload, callback) {
    // inst 就是我们调用 this.setState 的 this,也就是 classComponent 实例
    // 获取到当前实例上的 fiber
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    // 计算当前 fiber 的到期时间(优先级)const expirationTime = computeExpirationForFiber(currentTime, fiber);

    // 创建更新一个更新 update
    const update = createUpdate(expirationTime);

    //payload 是 setState 传进来的要更新的对象
    update.payload = payload;

    //callback 就是 setState({},()=>{})的回调函数
    if (callback !== undefined && callback !== null) {if (__DEV__) {warnOnInvalidCallback(callback, 'setState');
      }
      update.callback = callback;
    }

    // 把更新放到队列 UpdateQueue
    enqueueUpdate(fiber, update);

    // 开始进入 React 异步渲染的核心:React Scheduler
    scheduleWork(fiber, expirationTime);
  },
  // ......
}

看到上面的代码,是不是发现和上面 ReactDOM.render 中 scheduleRootUpdate 非常的相似。其实他们就是同一个更新原理呢~

三、forceUpdate

废话不多说,先上代码。也是在 ReactFiberClassComponent.js 中 classComponentUpdater 对象中。

const classComponentUpdater = {
  // ......
  enqueueForceUpdate(inst, callback) {const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);

    // 与 setState 不同的地方
    // 默认是 0 更新,需要改成 2 强制更新
    update.tag = ForceUpdate;

    if (callback !== undefined && callback !== null) {if (__DEV__) {warnOnInvalidCallback(callback, 'forceUpdate');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  // ......
}

看到代码的我们很开心,简直就是 enqueueSetState 的孪生兄弟。我就不详说啦。

到这里我们总结一下上面三种更新的流程:
(1)获取节点对应的 fiber 对象
(2)计算 currentTime
(3)根据(1)fiber 和(2)currentTime 计算 fiber 对象的 expirationTime
(4)根据(3)expirationTime 创建 update 对象
(5)将 setState 中要更新的对象赋值到(4)update.payload,ReactDOM.render 是{element}
(6)将 callback 赋值到(4)update.callback
(7)update 入队 updateQueue
(8)进行任务调度

四、update 对象

上面三种创建更新的方式中都创建了一个叫 update 的对象。那这个对象里面到底是什么呢?充满好奇的我们点开 createUpdate 函数瞧瞧:

export function createUpdate(expirationTime: ExpirationTime): Update<*> {
  return {
    // 过期时间
    expirationTime: expirationTime,

    // export const UpdateState = 0;
    // export const ReplaceState = 1;
    // export const ForceUpdate = 2;
    // export const CaptureUpdate = 3;
    // 指定更新的类型,值为以上几种
    // 提下 CaptureUpdate,在 React16 后有一个 ErrorBoundaries 功能
    // 即在渲染过程中报错了,可以选择新的渲染状态(提示有错误的状态),来更新页面
    // 0 更新 1 替换 2 强制更新 3 捕获性的更新
    tag: UpdateState,

    // 更新内容,比如 `setState` 接收的第一个参数
    // 第一次渲染 ReactDOM.render 接收的是 payload = {element};
    payload: null,

    // 更新完成后对应的回调,`setState`,`render` 都有
    callback: null,

    // 指向下一个更新
    next: null,

    // 指向下一个 `side effect`,这块内容后续讲解
    nextEffect: null,
  };
}

就是返回了个简单的对象。对象每个属性的解释我都写在上面了。

五、UpdateQueue

UpdateQueue 是一个单向链表,用来存放 update。每个 update 用 next 连接。它的结构如下:

// 创建更新队列
export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
  const queue: UpdateQueue<State> = {
    // 应用更新后的 state
    // 每次的更新都是在这个 baseState 基础上进行更新
    baseState,
    // 队列中的第一个 update
    firstUpdate: null,
    // 队列中的最后一个 update
    lastUpdate: null,
    // 队列中第一个捕获类型的 update
    firstCapturedUpdate: null,
    // 队列中最后一个捕获类型的 update
    lastCapturedUpdate: null,
    // 第一个 side effect
    firstEffect: null,
    // 最后一个 side effect
    lastEffect: null,
    // 第一个和最后一个捕获产生的 `side effect`
    firstCapturedEffect: null,
    lastCapturedEffect: null,
  };
  return queue;
}

六、enqueueUpdate

创建了 update 对象之后,紧接着调用了 enqueueUpdate,把 update 对象放到队列 enqueueUpdate。同时保证 current 和 workInProgress 的 updateQueue 是一致的,即 fiber.updateQueue 和 fiber.alternate.updateQueue 保持一致。

export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
  // 保证 current 和 workInProgress 的 updateQueue 是一致的
  // alternate 即 workInProgress
  const alternate = fiber.alternate;
  // current 的队列
  let queue1;
  // alternate 的队列
  let queue2;
  // 如果 alternate 为空
  if (alternate === null) {
    // There's only one fiber.
    queue1 = fiber.updateQueue;
    queue2 = null;
    // 如果 queue1 仍为空,则初始化更新队列
    if (queue1 === null) {queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
    }
  } else {
    // 如果 alternate 不为空,则取各自的更新队列
    queue1 = fiber.updateQueue;
    queue2 = alternate.updateQueue;
    if (queue1 === null) {if (queue2 === null) {
        // 初始化
        queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
        queue2 = alternate.updateQueue = createUpdateQueue(alternate.memoizedState,);
      } else {
        // 如果 queue2 存在但 queue1 不存在的话,则根据 queue2 复制 queue1
        queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
      }
    } else {if (queue2 === null) {
        // Only one fiber has an update queue. Clone to create a new one.
        queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
      } else {// Both owners have an update queue.}
    }
  }
  if (queue2 === null || queue1 === queue2) {
    // 将 update 放入 queue1 中
    appendUpdateToQueue(queue1, update);
  } else {
    // 两个队列共享的是用一个 update
    // 如果两个都是空队列,则添加 update
    if (queue1.lastUpdate === null || queue2.lastUpdate === null) {appendUpdateToQueue(queue1, update);
      appendUpdateToQueue(queue2, update);
    } else {
      // 如果两个都不是空队列,由于两个结构共享,所以只在 queue1 加入 update
      // 在 queue2 中,将 lastUpdate 指向 update
      appendUpdateToQueue(queue1, update);
      queue2.lastUpdate = update;
    }
  }

总结上面过程:
(1)queue1 取的是 fiber.updateQueue;

queue2 取的是 alternate.updateQueue

(2)如果两者均为 null,则调用 createUpdateQueue()获取初始队列
(3)如果两者之一为 null,则调用 cloneUpdateQueue() 从对方中获取队列
(4)如果两者均不为 null,则将 update 作为 lastUpdate

注:两个队列共享的是同一个 update。

七、scheduleWork

上面三种更新最后都调用了 scheduleWork(fiber, expirationTime)进入 React 异步渲染的核心:React Scheduler。后续文章详细讲解。

任世界纷繁复杂, 仍旧保持可爱。
我是小柚子小仙女。文章如有不妥,欢迎指正~

退出移动版