React源码03 - React 中的更新

03 - React 中的更新

React 中创建更新的方式:
初次渲染:ReactDOM.render、ReactDOM.hydrate
后续更新:setState、forceUpdate

1. ReactDOM.render()

  • 先创建 ReactRoot 顶点对象
  • 然后创建 FiberRoot 和 RootFiber
  • 创建更新,使应用进入更新调度过程

这个部分,只要了解流程即可,不要陷入各种旁支末节,否则很难再 “return”出来,划不来,先点到为止。

写 JSX 的时候,只是调用了 createElement 创建了 element 树,还需要 render 进一步进行渲染和处理。
ReactDOM 源码在 react-dom/src/client 下面,而 server 对应的是服务端,这里只研究客户端。

const ReactDOM: Object = {    
  render(
    element: React$Element<any>,
    container: DOMContainer,
    callback: ?Function,
  ) {
    return legacyRenderSubtreeIntoContainer(
      null, // 没有父组件
      element,
      container,
      false, // 不调和
      callback,
    );
    },
  
  // hydrate 和 render 唯一区别就是是否会调和 DOM 节点,是否会复用节点,服务端的时候会用到,暂且不表
  hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      true,
      callback,
    );
  },
  // ... 其他方法略
}

渲染子树到 container 中:

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: DOMContainer,
  forceHydrate: boolean,
  callback: ?Function,
) {
    
  let root: Root = (container._reactRootContainer: any); 
  if (!root) {
    // Initial mount 首次挂载时 container 上自然没有绑定过 _reactRootContainer
    // 接着就是根据传入的 container 创建 ReactRoot 并顺便绑定到 container 上,
    // 这个 ReactRoot 对象中的 _internalRoot 是一个 FiberRoot。
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = DOMRenderer.getPublicRootInstance(root._internalRoot);
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    // 首次渲染不需要所谓的批量更新
    DOMRenderer.unbatchedUpdates(() => {
      if (parentComponent != null) {
        root.legacy_renderSubtreeIntoContainer(
          parentComponent,
          children,
          callback,
        );
      } else {
        // 一般来说 parentComponnent 就是 null,所以会走到这里提交更新
        root.render(children, callback); // 具体见后面的代码块
      }
    });
  } else {
    // 下次更新,除了不再放入 DOMRenderer.unbatchedUpdates 回调中执行,其他和首次渲染一样
    // 略
  }
  return DOMRenderer.getPublicRootInstance(root._internalRoot);
}

function legacyCreateRootFromDOMContainer(
  container: DOMContainer,
  forceHydrate: boolean,
): Root {
    
  // 内部通过判断传入的 root 节点是否有子节点来决定是否进行调和。
  // 非服务端的话,不涉及 hydrate 调和,接下来就是清空传入的 root dom 下面的子节点,因为接来下 react 要挂载自己的 dom 到 root 上。
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content. 
  // 清空 container dom 下的子节点
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    while ((rootSibling = container.lastChild)) {
      container.removeChild(rootSibling);
    }
  }

  // Legacy roots are not async by default.
  const isConcurrent = false;
  // 
  return new ReactRoot(container, isConcurrent, shouldHydrate);
}

ReactRoot 是外层包裹,里面的 _internalRoot 才是 FiberRoot:

function ReactRoot(
  container: Container,
  isConcurrent: boolean,
  hydrate: boolean,
) {
  const root = DOMRenderer.createContainer(container, isConcurrent, hydrate);
  this._internalRoot = root;
}

react-reconciler 包下,创建 FiberRoot:

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

在前面的 legacyRenderSubtreeIntoContainer 中的 root.render(children, callback):


ReactRoot.prototype.render = function(
  children: ReactNodeList,
  callback: ?() => mixed,
): Work {
  const root = this._internalRoot;
  const work = new ReactWork();
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    work.then(callback);
  }
  DOMRenderer.updateContainer(children, root, null, work._onCommit);
  return work;
};

DOMRenderer.updateContainer 内部的深层调用。createUpdate() 创建 update 对象,把要更新的 element 添加到 update 上,然后 update 进入更新队列,然后开始调度更新的工作:

function scheduleRootUpdate(
  current: Fiber,
  element: ReactNodeList,
  expirationTime: ExpirationTime,
  callback: ?Function,
) {
   
  const update = createUpdate(expirationTime);
  // Caution: React DevTools currently depends on this property
  // being called "element".
  // 被调用的 element 作为 update 对象的载荷
  update.payload = {element};

  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    warningWithoutStack(
      typeof callback === 'function',
      'render(...): Expected the last optional `callback` argument to be a ' +
        'function. Instead received: %s.',
      callback,
    );
    update.callback = callback;
  }
  // 把 update 加入更新队列
  enqueueUpdate(current, update);
    // 开始调用更新
  scheduleWork(current, expirationTime);
  return expirationTime;
}

在 ReactRoot 中会创建 FiberRoot 然后赋值到 this._internalRoot ,this 就是指 ReactRoot 实例。然后顺便把内部创建出来的 ReactRoot 对象绑定到最初传入的 root dom 节点(通常是个div)的 _reactRootContainer 属性上,见下图:

image.png
image.png

在 React 17中 ReactDOM.render() 不再能够用来 hydrate 调和服务端渲染的 container,会被废弃。有此需求应直接使用 ReactDOM.hydrate()

Using ReactDOM.render() to hydrate a server-rendered container is deprecated and will be removed in React 17. Use hydrate() instead.

2. FiberRoot

Fiber 解决了单线程计算量过大时交互、动画卡顿的问题,常说的 “虚拟DOM” 就是指 Fiber 树。

FiberRoot:

  • 整个应用的起点
  • 包含应用挂载的目标节点
  • 记录整个应用更新过程的各种信息

React.createElement() 创建出 ReactElement 节点,组成 element 树, 每一个的 element 也都有对应的 Fiber 节点,组成 Fiber 树。
**FiberRoot **中的一些属性:

  • containerInfo: root 节点,即 ReactDOM.render() 方法接收到的第二个参数。
  • current:记录了当前入口 dom 节点所对应的 Fiber 节点,即 RootFiber(涉及到双缓存/双buff/double-buff 机制)。
  • finishedWork:一次更新渲染过程中完成了的那个更新任务。更新完成之后读取该属性,渲染至 dom 上。
  • nextExpirationTimeToWorkOn:下次更新时要执行的那个任务,react 会遍历 fiber 树,读取每个 fiber 节点上的 ExpirationTIme,在 FiberRoot 上用该属性记录最高优先级的那个任务。
  • expirationTime: 当前更新对应的过期时间。
  • nextScheduledRoot 存在多个 root 挂载点时,会有多个 FiberRoot,而这些 FiberRoot 会组成单向链表,因此该属性就是指向链表中下一个 root 节点的“指针”。这个属性,也体现了,为什么在入口 dom 节点所对应的 Fiber 节点上,还需要一层结构,即 FiberRoot。

3. Fiber

  • 每一个 ReactElement 对应一个 Fiber 对象。
  • Fiber 对象上记录了节点的各种状态,包括 state 和 props。Fiber 更新完成之后,state 和 props 才被更新到 class 组件的 this 上,也为 hooks 的实现提供了根基,因为状态并不是靠 function 函数本身来维持的。
  • 串联整个应用形成树结构。

Fiber 树遍历时根据 child、sibling、return(parent),Fiber 部分属性如下:

  • tag: 标记不同的组件类型
  • elementType: ReactElement.type,也就是我们调用 createElement() 的第一个参数。
  • stateNode:记录组件实例,如 class 组件的实例、原生 dom 实例,而 function 组件没有实例,因此该属性是空。如 state、props 等状态完成更新任务后,react 会通过该属性,更新组件实例。需要强调的是,RootFiber 的 stateNode 属性指向 FiberRoot,和 FiberRoot 上的 current 属性相呼应。
  • penndingProps: 新的 props
  • memorizedProps: 老的 props(上次渲染完成之后的 props)
  • memorizedState:老的 state(上次渲染完成之后的 state)
  • updateQueue: 该 Fiber 对应的组件产生的 update 会存放于该队列(类似于单向链表)中。该过程产出的新的 state 会用来更新 memorizedState。
  • expirationTime: 代表任务在未来的哪个时间点应该被完成,不包括他的子树产生的任务。
  • childExpirationTime: 子树中优先级最高的过期时间,即最先的过期时间,用于快速确定子树中是否有不在等待的变化。
  • alternate: 在 Fiber 树每次更新时,每个 FIber 都会有一个与其对应的 Fiber,称为“current <--> workInProgress”。React 应用的根节点(FiberRoot)通过 current 指针在不同 Fiber 树间进行切换,从而两个 Fiber 树轮流复用(双缓存机制),而不是每次更新都创建新的 Fiber 树。其中很多 workInProgress fiber 的创建可以复用 current Fiber 树对应的节点数据(因为每个 Fiber 节点都有 alternate 指向对应的节点),这个决定是否复用 current Fiber 树对应节点数据的过程就是 Diff 算法。
  • mode: 用来描述当前 Fiber 和其子树的模式(后面会提到)

// Effect 系列

  • effectTag: SideEffectTag。用来记录 SideEffect。
  • nextEffect: Fiber | null。单链表用来快速查找下一个side effect。
  • firstEffect: Fiber | null。 子树中第一个side effect。
  • lastEffect: Fiber | null。子树中最后一个side effect。

TODO: 补一张 Fiber 树图。

Fiber.tag

export type WorkTag =
  | 0
  | 1
  | 2
  | 3
  | 4
  | 5
  | 6
  | 7
  | 8
  | 9
  | 10
  | 11
  | 12
  | 13
  | 14
  | 15
  | 16
  | 17
  | 18;

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;

4. update 和 updateQueue

Update:

  • 用于记录组件状态的改变
  • 存放于 UpdateQueue 中
  • 多个 Update 可以同时存在(因为放在队列中)
export type Update<State> = {
  // 更新的过期时间
  expirationTime: ExpirationTime,

  // export const UpdateState = 0;
  // export const ReplaceState = 1;
  // export const ForceUpdate = 2;
  // export const CaptureUpdate = 3;
  // 指定更新的类型,值为以上几种
  tag: 0 | 1 | 2 | 3,
  // 更新内容,比如`setState`接收的第一个参数
  payload: any,
  // 对应的回调,`setState`,`render`都有
  callback: (() => mixed) | null,

  // 指向下一个更新
  next: Update<State> | null,
  // 指向下一个`side effect`
  nextEffect: Update<State> | null,
};

export type UpdateQueue<State> = {
  // 每次操作完更新之后的`state`
  baseState: State,

  // 队列中的第一个`Update`
  firstUpdate: Update<State> | null,
  // 队列中的最后一个`Update`
  lastUpdate: Update<State> | null,

  // 第一个捕获类型的`Update`
  firstCapturedUpdate: Update<State> | null,
  // 最后一个捕获类型的`Update`
  lastCapturedUpdate: Update<State> | null,

  // 第一个`side effect`
  firstEffect: Update<State> | null,
  // 最后一个`side effect`
  lastEffect: Update<State> | null,

  // 第一个和最后一个捕获产生的`side effect`
  firstCapturedEffect: Update<State> | null,
  lastCapturedEffect: Update<State> | null,
};

Update:

  • expirationTime: 更新的过期时间。
  • payload:首次渲染 payload 是整个 element 树,而后续如 setState 触发更新,则 payload 是 setState 传入的参数,即 state 对象或者函数。
  • tag: 0 | 1 | 2 | 3 指定更新的类型,值为:UpdateState | ReplaceState | ForceUpdate | CaptureUpdate)
  • callback: 对应的回调, setState 或者 render 都有。
  • next: 指向下一个更新。
  • nextEffect: 指向下一个 side effect。

UpdateQueue:

  • baseState:每次操作完更新之后的 state,作为下次更新 state 时的计算依据。
  • firstUpdate: 更新队列中第一个 Update
  • lastUpdate: 更新队列中最后一个 Update
  • firstCapturedUpdate
  • lastCapturedUpdate
  • firstEffect
  • lastEffect
  • firstCapturedEffect
  • lastCapturedEffect

上面说过 ReatDOM.render() 时创建 Update 并添加到 UpdateQueue 中。enqueueUpdate() (位于 react-reconciler/ReactUpdateQueue.js)用于初始化 Fiber 对象上的 updateQueue,以及如果已经存在时则更新这个队列。在此过程中,保持双 Fiber 的 updateQueue 的首尾 queue 一致。

enqueueUpdate() :

export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
  // Update queues are created lazily.
  const alternate = fiber.alternate;
  let queue1;
  let queue2;
  
 // 创建或更新队列,若两个队列都不存在,则各自创建一个队列;
 // 若其中一个队列存在时,则 clone 出另一个队列,会共享三个属性:baseState、firstUpdate、lastUpdate
 if (alternate === null) {
    // There's only one fiber.
    queue1 = fiber.updateQueue;
    queue2 = null;
    if (queue1 === null) {
      queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
    }
  } else {
    // There are two owners.
    queue1 = fiber.updateQueue;
    queue2 = alternate.updateQueue;
    if (queue1 === null) {
      if (queue2 === null) {
        // Neither fiber has an update queue. Create new ones.
        queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
        queue2 = alternate.updateQueue = createUpdateQueue(
          alternate.memoizedState,
        );
      } else {
        // Only one fiber has an update queue. Clone to create a new one.
        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.
      }
    }
  }
  
  // 2. 调用 appendUpdateToQueue() 将 update 添加到队列链表中
  if (queue2 === null || queue1 === queue2) {
    // There's only a single queue.
    appendUpdateToQueue(queue1, update);
  } else {
    // There are two queues. We need to append the update to both queues,
    // while accounting for the persistent structure of the list — we don't
    // want the same update to be added multiple times.
    if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
      // One of the queues is not empty. We must add the update to both queues.
      appendUpdateToQueue(queue1, update);
      appendUpdateToQueue(queue2, update);
    } else {
      // Both queues are non-empty. The last update is the same in both lists,
      // because of structural sharing. So, only append to one of the lists.
      appendUpdateToQueue(queue1, update);
      // But we still need to update the `lastUpdate` pointer of queue2.
      queue2.lastUpdate = update;
    }
  }
}

appendUpdateToQueue() ,UpdateQueue 显然是一个基于链表的队列,看情况更新首尾指针即可:

function appendUpdateToQueue<State>(
  queue: UpdateQueue<State>,
  update: Update<State>,
) {
  // Append the update to the end of the list.
  if (queue.lastUpdate === null) {
    // Queue is empty
    queue.firstUpdate = queue.lastUpdate = update;
  } else {
    queue.lastUpdate.next = update;
    queue.lastUpdate = update;
  }
}

cloneUpdateQueue :

function cloneUpdateQueue<State>(
  currentQueue: UpdateQueue<State>,
): UpdateQueue<State> {
  const queue: UpdateQueue<State> = {
    // 克隆队列时这三个属性是共享的
    baseState: currentQueue.baseState,
    firstUpdate: currentQueue.firstUpdate,
    lastUpdate: currentQueue.lastUpdate,

    // TODO: With resuming, if we bail out and resuse the child tree, we should
    // keep these effects.
    firstCapturedUpdate: null,
    lastCapturedUpdate: null,

    firstEffect: null,
    lastEffect: null,

    firstCapturedEffect: null,
    lastCapturedEffect: null,
  };
  return queue;
}

5. ExpirationTime

尤其对于异步任务来说,过期时间是某个更新任务告诉 react 在过期时间未到之前,自己可以被打断。但如果过期时间已经到了,而更新任务依旧未得到执行,则会被强制执行。

  • currentTime:简单理解当前时间距 JS 加载完成时的时间
    • 在一次渲染中产生的更新需要使用相同的时间
    • 一次批处理的更新应该得到相同的时间
    • 挂起任务用于记录的时候应该相同
  • expirationTime:过期时间

react-reconciler/ReactFiberReconciler.js
updateContainer() :

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  const current = container.current;
  // 获取 currentTime
  const currentTime = requestCurrentTime();
  // 根据 currentTime 计算过期时间(其实并不是直接计算,而是先调用)
  const expirationTime = computeExpirationForFiber(currentTime, current);
  // 然后就是上面刚刚说过的创建 update 和 updateQueue 的过程
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    callback,
  );
}

requestCurrentTime() :
同一事件中的两个更新计划应被处理为同时发生,即使它们的时钟时间必然有先有后。因为 expirationTime 决定了如何处理批量更新,所以这里出于性能考虑,在同一事件中,类似优先级的更新任务会得到相同的 currentTime,从而后面计算出相同的 expirationTime,这些任务在某一时刻同时更新,避免短期内多次频繁更新崩溃:

function requestCurrentTime() {
  // requestCurrentTime is called by the scheduler to compute an expiration
  // time.
  //
  // Expiration times are computed by adding to the current time (the start
  // time). However, if two updates are scheduled within the same event, we
  // should treat their start times as simultaneous, even if the actual clock
  // time has advanced between the first and second call.

  // In other words, because expiration times determine how updates are batched,
  // we want all updates of like priority that occur within the same event to
  // receive the same expiration time. Otherwise we get tearing.
  //
  // We keep track of two separate times: the current "renderer" time and the
  // current "scheduler" time. The renderer time can be updated whenever; it
  // only exists to minimize the calls performance.now.
  //
  // But the scheduler time can only be updated if there's no pending work, or
  // if we know for certain that we're not in the middle of an event.

  if (isRendering) {
    // We're already rendering. Return the most recently read time.
    return currentSchedulerTime;
  }
  // Check if there's pending work.
  findHighestPriorityRoot();
  if (
    nextFlushedExpirationTime === NoWork ||
    nextFlushedExpirationTime === Never
  ) {
    // If there's no pending work, or if the pending work is offscreen, we can
    // read the current time without risk of tearing.
    recomputeCurrentRendererTime();
    currentSchedulerTime = currentRendererTime;
    return currentSchedulerTime;
  }
  // There's already pending work. We might be in the middle of a browser
  // event. If we were to read the current time, it could cause multiple updates
  // within the same event to receive different expiration times, leading to
  // tearing. Return the last read time. During the next idle callback, the
  // time will be updated.
  return currentSchedulerTime;
}

computeExpirationForFiber() 方法更多信息下一小节再说,其中涉及到的 expirationTime 计算过程:

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';

export type ExpirationTime = number;

export const NoWork = 0;
export const Sync = 1;
export const Never = MAX_SIGNED_31_BIT_INT;

const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = 2;

// 1 unit of expiration time represents 10ms.
export function msToExpirationTime(ms: number): ExpirationTime {
  // Always add an offset so that we don't clash with the magic number for NoWork.
  return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET;
}

export function expirationTimeToMs(expirationTime: ExpirationTime): number {
  return (expirationTime - MAGIC_NUMBER_OFFSET) * UNIT_SIZE;
}

function ceiling(num: number, precision: number): number {
  return (((num / precision) | 0) + 1) * precision;
}

function computeExpirationBucket(
  currentTime,
  expirationInMs,
  bucketSizeMs,
): ExpirationTime {
  return (
    MAGIC_NUMBER_OFFSET +
    ceiling(
      currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,
      bucketSizeMs / UNIT_SIZE,
    )
  );
}

export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;

export function computeAsyncExpiration(
  currentTime: ExpirationTime,
): ExpirationTime {
  return computeExpirationBucket(
    currentTime,
    LOW_PRIORITY_EXPIRATION,
    LOW_PRIORITY_BATCH_SIZE,
  );
}

// We intentionally set a higher expiration time for interactive updates in
// dev than in production.
//
// If the main thread is being blocked so long that you hit the expiration,
// it's a problem that could be solved with better scheduling.
//
// People will be more likely to notice this and fix it with the long
// expiration time in development.
//
// In production we opt for better UX at the risk of masking scheduling
// problems, by expiring fast.
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;

export function computeInteractiveExpiration(currentTime: ExpirationTime) {
  return computeExpirationBucket(
    currentTime,
    HIGH_PRIORITY_EXPIRATION,
    HIGH_PRIORITY_BATCH_SIZE,
  );
}

过期时间 = 当前时间 + 延迟
延迟的时间长度如下(ms):

export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150; // 高优先级任务的过期时间基础偏移量
export const HIGH_PRIORITY_BATCH_SIZE = 100;

export const LOW_PRIORITY_EXPIRATION = 5000; // 高优先级任务的过期时间基础偏移量
export const LOW_PRIORITY_BATCH_SIZE = 250;

最终计算出的 expirationTime 的精度是 10ms(高优先级) 或者 25ms(低优先级),即 expirationTime 会是 10 或者 25 的整数倍。

bucketSIzeMs / UNIT_SIZE 精度在这里的意义
如果在一个操作内多次调用了 setState,即便前后调用的时间差距可能很小,但毫秒级别还是有差距,那么计算出的 expirationTime 也就不一样,任务优先级也就不一样,导致 react 更新多次,导致整个应用性能下降。
而有了 精度/粒度 的控制,使得非常详尽的两次更新,即使具有微小的 currentTime 差异,也会得到相同的 expirationTime,从而到时候在一次更新中一起完成(批量更新)。

currentTime 和 expirationTime 在各自计算过程中,为了性能都在**保证在一个批量更新中产生的同类型的更新,应具有相同的过期时间。 **否则全部用当前时间加上固定的延迟作为未来的过期时间就用不着计算这么麻烦了。

6. 不同的 ExpirationTime

  • NoWork: 代表没有更新
  • Sync: 代表同步执行,不会被调度也不会被打断
  • async: 异步模式下计算出来的过期时间,一个时间戳,会被调度,同时还可能被打断
export const NoWork = 0;
export const Sync = 1;
export const Never = MAX_SIGNED_31_BIT_INT;

上一小节中提到,得到 currentTime 后,会调用 computeExpirationForFiber() 然后返回 expirationTime,还涉及到过期时间的复杂的计算公式,但有些过期时间的计算其实不需要调用计算公式:
比如后面提到的 flushSync 中把 expirationContext 改为 Sync,直接进入下面第一个条件判断,最终得到的过期时间直接就是 Sync,也就是 0ms:

function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  let expirationTime;
  if (expirationContext !== NoWork) {
    // An explicit expiration context was set;
    expirationTime = expirationContext;
  } else if (isWorking) {
    if (isCommitting) {
      // Updates that occur during the commit phase should have sync priority
      // by default.
      expirationTime = Sync;
    } else {
      // Updates during the render phase should expire at the same time as
      // the work that is being rendered.
      expirationTime = nextRenderExpirationTime;
    }
  } else {
    // No explicit expiration context was set, and we're not currently
    // performing work. Calculate a new expiration time.
    if (fiber.mode & ConcurrentMode) {
      if (isBatchingInteractiveUpdates) {
        // This is an interactive update
        expirationTime = computeInteractiveExpiration(currentTime);
      } else {
        // This is an async update
        expirationTime = computeAsyncExpiration(currentTime);
      }
      // If we're in the middle of rendering a tree, do not update at the same
      // expiration time that is already rendering.
      if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
        expirationTime += 1;
      }
    } else {
      // This is a sync update
      expirationTime = Sync;
    }
  }
  if (isBatchingInteractiveUpdates) {
    // This is an interactive update. Keep track of the lowest pending
    // interactive expiration time. This allows us to synchronously flush
    // all interactive updates when needed.
    if (expirationTime > lowestPriorityPendingInteractiveExpirationTime) {
      lowestPriorityPendingInteractiveExpirationTime = expirationTime;
    }
  }
  return expirationTime;
}
  • 通过外部来强制某一个更新必须使用哪一种 expirationTime(指定 expirationContext):

比如使用 ReactDOM.flushSync() (该方法实际存在于 react-reconciler/ReactFiberScheduler.js 中) 可以指定 expirationTime 为 1ms,意味着同步更新:

import { flushSync } from 'react-dom';
// ...
    handleClick = () => {
    flushSync(() => {
      this.setState({ text: '666' });
    });
  };
// ...
let expirationContext: ExpirationTime = NoWork;

// ...

function flushSync<A, R>(fn: (a: A) => R, a: A): R {
  const previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;
  try {
    return syncUpdates(fn, a);
  } finally {
    isBatchingUpdates = previousIsBatchingUpdates;
    performSyncWork();
  }
}

function syncUpdates<A, B, C0, D, R>(
  fn: (A, B, C0, D) => R,
  a: A,
  b: B,
  c: C0,
  d: D,
): R {
  const previousExpirationContext = expirationContext;
  expirationContext = Sync; // 设置为 1
  try {
    return fn(a, b, c, d); // 传入 flushSync 的回调函数在这里被执行
  } finally {
    expirationContext = previousExpirationContext; // 把 expirationContext 恢复成 NoWork
  }
}
  • isWorking/isCommitting,即有任务更新的时候:

同样也不需要什么计算公式。具体留待后面涉及更新的时候再说。

  • 处于 ConcurrentMode 模式下才需要异步更新,即需要用到计算公式:

对于大部分的 react 事件系统产生的更新,这里的 isBatchingInteractiveUpdates 会是 true ,也就是高优先级的任务,过期时间会更短。

  if (fiber.mode & ConcurrentMode) {
        // 大部分的 react 事件产生的更新中 isBatchingInteractiveUpdates 会是 true ,
      // 也就是高优先级的任务,过期时间会更短。
      if (isBatchingInteractiveUpdates) {
        expirationTime = computeInteractiveExpiration(currentTime);
      } else {
        expirationTime = computeAsyncExpiration(currentTime);
      }
      // 正在渲染树时,新加入的更新的过期时间+1 以遍不会和当前更新一起更新。后续讲更新时再细说
      if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
        expirationTime += 1;
      }
    } else {
      expirationTime = Sync;
    }

至于 fiber.mode & ConcurrentMode 这种按位操作的表达式,其实就是使用位运算进行属性的读写
使用一个若干位的二进制数表达(存储)若干个布尔属性,设置属性使用按位异或 ^ ,查询属性使用按位与 &
Fiber 上的 mode:

export type TypeOfMode = number;

export const NoContext = 0b000;
export const ConcurrentMode = 0b001;
export const StrictMode = 0b010;
export const ProfileMode = 0b100;

7. setState 和 forceUpdate

在 react 中能合理产生更新的方式,同时也是 react 推崇的方式有以下几种:

  • ReactDOM.render() 首次渲染
  • setState(class 组件)
  • forceUpdate (class 组件)这个其实也很少使用
  • useState (函数式组件中的 hooks)

ReactDOM.render 创建的更新是放在 RootFiber 上面,是整体的初始化渲染。
是针对setState 和 forceUpdate 是为节点的 Fiber 创建更新,是针对某一个 class component 而言。

和之前的 ReactDOM.render() 内部会调用的 updateContaine() 方法很像,在 enqueueSetState()enqueueForceUpdate() 中:

  • 拿到 fiber,得到 currentTime,一起作为 computeExpirationForFibe() 的参数算出 expirationTime。
  • 然后创建 update 对象,然后 enqueueUpdate() 时看情况创建或更新队列,然后进行调度。

如之前说的,update 对象上的 payload 载荷在 ReactDOM.render 时是 element 树,而在 setState 或 forceUpdate 时是传入的新的 state 对象(可能是局部的 state 对象)。

forceUpdate 和 setState 进行 enqueue 时唯一不同点在于 forceUpdate 所创建的 update 对象上的 tag 会是 ForceUpdate 而不是默认的 UpdateState

react-reconciler/src/ReactFiberClassComponent.js 中:

const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueReplaceState(inst, payload, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);
    update.tag = ReplaceState;
    update.payload = payload;

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

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueForceUpdate(inst, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);
    update.tag = ForceUpdate;

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

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

可见,在 react 中创建更新的过程基本一样,。而更多的技术细节会在整体的 Scheduler 调度方面。

ReactDOM.render/setState/forceUpdate 最终都会创建 update 对象,挂载 payload 载荷,并添加到各自 Fiber 节点上的 updateQueue 中,然后即将进入下一环节,开始 scheduleWork 即调度工作。
下一篇就来分析创建更新队列之后,react 如何进行统一调度。



喜欢的朋友记得点赞、收藏、关注哦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/898961.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Qt | http获取网页文件(小项目)

点击上方"蓝字"关注我们 ctrl+r 运行 URL可以自己替换一个试一试 【源码获取】 链接:https://pan.baidu.com/s/1QzHKZPXjkpx2p5TWUS_acA?pwd=5xsd 提取码:5xsd 01、QProgressDialog >>> QProgressDialog 是 Qt 框架中的一个类,主要用于显示一个进…

Java使用dom4j生成kml(xml)文件遇到No such namespace prefix: xxx is in scope on:问题解决

介绍addAttribute和addNamepsace: addAttribute 方法 addAttribute 方法用于给XML元素添加属性。属性&#xff08;Attributes&#xff09;是元素的修饰符&#xff0c;提供了关于元素的额外信息&#xff0c;并且位于元素的开始标签中。属性通常用于指定元素的行为或样式&#…

Golang | Leetcode Golang题解之第497题非重叠矩形中的随机点

题目&#xff1a; 题解&#xff1a; type Solution struct {rects [][]intsum []int }func Constructor(rects [][]int) Solution {sum : make([]int, len(rects)1)for i, r : range rects {a, b, x, y : r[0], r[1], r[2], r[3]sum[i1] sum[i] (x-a1)*(y-b1)}return Sol…

ReactOS系统中搜索给定长度的空间地址区间中的二叉树

搜索给定长度的空间地址区间 //搜索给定长度的空间地址区间 MmFindGap MmFindGapTopDown PVOID NTAPI MmFindGap(PMADDRESS_SPACE AddressSpace,ULONG_PTR Length,ULONG_PTR Granularity,BOOLEAN TopDown );PMADDRESS_SPACE AddressSpace,//该进程用户空间 ULONG_PTR Length,…

JavaScript入门中-流程控制语句

本文转载自&#xff1a;https://fangcaicoding.cn/article/52 大家好&#xff01;我是方才&#xff0c;目前是8人后端研发团队的负责人&#xff0c;拥有6年后端经验&3年团队管理经验&#xff0c;截止目前面试过近200位候选人&#xff0c;主导过单表上10亿、累计上100亿数据…

echart改变最后一个节点的图标

需求 在折线图的最后一个节点增加一个gif动图表示增长 一、静态图的使用 采用symbol属性进行设置&#xff0c;结果就是只能展示静态图 无法插入gif series: [{data: [150, 230, 224, 218, 135, 147, {value:200,symbol:image://https://ylxstatic.storage.ylingxin.com/va…

PostgreSQL数据库查看shared buffer配置

哈喽&#xff0c;大家好&#xff0c;我是木头左&#xff01; PostgreSQL是一个功能强大的开源关系型数据库管理系统&#xff0c;广泛应用于各种规模的应用程序。在PostgreSQL中&#xff0c;shared buffer是一个重要的性能调优参数&#xff0c;它直接影响到数据库查询的性能。本…

【信息论基础第六讲】离散无记忆信源等长编码包括典型序列和等长信源编码定理

一、信源编码的数学模型 我们知道信源的输出是消息序列&#xff0c;对于信源进行编码就是用码字集来表示消息集&#xff0c;也就是要进行从消息集到码字集的映射。 根据码字的特征我们又将其分为D元码&#xff0c;等长码&#xff0c;不等长码&#xff0c;唯一可译码。 我们通过…

使用Yolov10和Ollama增强OCR

1. 训练自定义 Yolov10 数据集 利用物体检测增强 OCR 的第一步是在数据集上训练自定义 YOLO 模型。YOLO&#xff08;只看一遍&#xff09;是一种功能强大的实时对象检测模型&#xff0c;它将图像划分为网格&#xff0c;使其能够在一次前向传递中识别多个对象。这种方法非常适合…

Redis遇到Hash冲突怎么办?

这是小伙伴之前遇到的一个面试题&#xff0c;感觉也是一个经典八股&#xff0c;和大伙分享下。 一 什么是 Hash 冲突 Hash 冲突&#xff0c;也称为 Hash 碰撞&#xff0c;是指不同的关键字通过 Hash 函数计算得到了相同的 Hash 地址。 Hash 冲突在 Hash 表中是不可避免的&am…

eNSP网络基本配置

1.配置设备名称 网络上一般不会配属一台设备&#xff0c;管理员需要对这些设备进行统一管理。在进行设备调试的时候&#xff0c;首要任务是配置设备名称&#xff0c;设备名称用来唯一标识一台设备。 例如通过以下操作将设备名称设置为testA &#xff1f; //可以查看用户视图…

AnaTraf | 提升网络性能:深入解析网络关键指标监控、TCP重传与TCP握手时间

AnaTraf 网络性能监控系统NPM | 全流量回溯分析 | 网络故障排除工具 在当今的数字化时代&#xff0c;网络的稳定性和性能对企业的运营效率至关重要。无论是内部通信、应用程序的运行&#xff0c;还是对外提供服务&#xff0c;网络都发挥着关键作用。对于网络工程师或IT运维人员…

雨情教务排课系统源码

PC端的雨情教务排课系统&#xff0c;是一款集功能性、实用性与便捷性于一体的教育管理工具。它全面支持班级设置功能&#xff0c;能够轻松管理不同年级、不同专业的班级信息&#xff0c;为后续的排课工作奠定坚实基础。在课程设置方面&#xff0c;系统允许管理者根据教学计划&a…

安装OpenResty

OpenResty OpenResty 是一个基于 Nginx的高性能 Web 平台&#xff0c;用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。具备下列特点&#xff1a; 具备Nginx的完整功能 基于Lua语言进行扩展&#xff0c;集成了大量精良的 Lua 库、第三方模块…

Java最全面试题->Java基础面试题->JavaWeb面试题->Git/SVN面试题

文章目录 Git/SVN面试题Git和SVN有什么区别&#xff1f;SVN优缺点&#xff1f;Git优缺点&#xff1f;说一下Git创建分支的步骤&#xff1f;说一下Git合并的两种方法以及区别&#xff1f;Git如何查看文件的提交历史和分支的提交历史?什么是 git stash&#xff1f;什么是git sta…

uniapp修改input中placeholder样式

Uniapp官方提供了两种修改的属性方法&#xff0c;但经过测试&#xff0c;只有 placeholder-class 属性能够生效 <input placeholder"请输入手机验证码" placeholder-class"input-placeholder"/><!-- css --> <style lang"scss" s…

KBPC1010-ASEMI新能源专用方桥KBPC1010

编辑&#xff1a;ll KBPC1010-ASEMI新能源专用方桥KBPC1010 型号&#xff1a;KBPC1010 品牌&#xff1a;ASEMI 封装&#xff1a;KBPC-4 安装方式&#xff1a;直插 批号&#xff1a;2024 现货&#xff1a;50000 正向电流&#xff08;Id&#xff09;&#xff1a;10A 反向…

芯海休眠唤醒

这个电路要钱&#xff0c;降本需要去掉这个电路&#xff0c;让软件完全实现开关机的功能。 1、当按键按下的时候&#xff0c;K1下面接地&#xff0c;R12下面为低电平&#xff0c;同时BAT在左边供电&#xff0c;为高电平&#xff0c;Q2MOS管导通&#xff0c;给板子供电。 2、当…

【and design ProTable组件rowClassName属性进行判断修改行样式】

代码解析 注&#xff1a;行改变基于css效果 【导入css文件】 效果展示 代码块 自己导入cssrowClassName{(record)>{return record.jibie"高"?"row-style":""}}

Linux_进程终止_进程等待_进程替换

进程终止 不知道大家想过没有&#xff0c;我们写的main()函数的返回值是返回给谁的呢&#xff1f;其实是返回给父进程或者系统的。 int main() {std::cout << "hello" << std::endl;return 10; }运行该代码&#xff0c;输入hello&#xff0c;没问题&am…