关于react.js:详解React的Transition工作原理原理

0次阅读

共计 9881 个字符,预计需要花费 25 分钟才能阅读完成。

Transition 应用姿态

Transition 是 react18 引入的新概念,用来辨别紧急和非紧急的更新。

  • 紧急的更新,指的是一些间接的用户交互,如输出、点击等;
  • 非紧急的更新,指的是 UI 界面从一个样子过渡到另一个样子;

react 官网的 demo 如下:

import {startTransition} from 'react';

// Urgent: Show what was typed
setInputValue(input);

// Mark any state updates inside as transitions
startTransition(() => {
  // Transition: Show the results
  setSearchQuery(input);
});

有 2 个 API:

  • useTransition:hook,用在 function 组件或其余 hooks 中,能返回 isPending;
  • startTransition:用在不能应用 hooks 的场景,如 class 组件中,相比 useTransition 不能获取 isPending 状态;

2 个 API 还有一个差异:当进行间断疾速输出时,应用 startTransition 是无奈触发相似 throttle 的成果的。

Transition VS throttle、debounce

存在的问题:

  • 达到指定工夫后,更新开始解决,渲染引擎会被长时间阻塞,页面交互会呈现卡顿;
  • throttle 的最佳工夫不易把握,是由开发者设置的工夫。而这个预设的工夫,在不同性能的设施上不肯定能带来最佳的体验;

存在的问题:

  • 会呈现用户输出长时间得不到响应的状况,如上例中尽管输入框中内容始终在变但上面区域内始终不变;
  • 更新操作正式开始当前,渲染引擎依然会被长时间阻塞,依旧会存在页面卡死的状况;

用 transition 机制的成果:

  • 用户能够及时看到输出内容,交互也较晦涩;
  • 用户间断输出时,不会始终得不到响应(最迟 5s 必会开始更新渲染列表);
  • 开始更新渲染后,协调过程是可中断的,不会长时间阻塞渲染引擎(进入浏览器渲染阶段仍然会卡住);

transition 相比前两种计划的劣势:

  • 更新协调过程是可中断的,渲染引擎不会长时间被阻塞,用户能够及时失去响应;
  • 不须要开发人员去做额定的思考,整个优化过程交给 react 和浏览器即可;

transition 实现原理

isPending 实现原理

咱们看到页面首先进入了 pending 状态,而后才显示为 transition 更新后的后果。这里产生了 2 次 react 更新。但咱们只写了一个 setState。

function App() {const [value, setValue] = useState("");
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const newVal = e.target.value;
    startTransition(() => setValue(newVal));
  };

  return (
    <div>
      <input onChange={handleChange} />
      <div className={isPending ? 'loading' : ''}>
      {Array(50000).fill("a").map((item, index) => {return <div key={index}>{value}</div>;
        })
      }
      </div>
    </div>
  );
}

咱们看一下 useTransition 源码:

useTransition(): [boolean, (() => void) => void] {
  currentHookNameInDev = 'useTransition';
  mountHookTypesDev();
  return mountTransition();},

function mountTransition(): [boolean, (callback: () => void, options?: StartTransitionOptions) => void] {const [isPending, setPending] = mountState(false);
  // The `start` method never changes.
  const start = startTransition.bind(null, setPending);
  const hook = mountWorkInProgressHook();
  hook.memoizedState = start;
  return [isPending, start];
}

function startTransition(setPending, callback, options) {const previousPriority = getCurrentUpdatePriority();
  setCurrentUpdatePriority(higherEventPriority(previousPriority, ContinuousEventPriority),
  );

  setPending(true);

  const prevTransition = ReactCurrentBatchConfig.transition;
  ReactCurrentBatchConfig.transition = {};
  ...
  try {setPending(false);
    callback();} finally {setCurrentUpdatePriority(previousPriority);

    ReactCurrentBatchConfig.transition = prevTransition;
    ...
  }
}

当调用 startTransition 时,会先通过 setPending 将 isPending 改为 true,而后再通过 setPending 将 isPending 改为 false,并在 callback 中触发咱们本人定义的更新。
这里有一个奇怪的中央,3 次 setState 并没有合并在一起,而是触发了 2 次 react 更新,setPending(true) 为 1 次,setPending(false) 和 callback() 为第二次。
这是因为

ReactCurrentBatchConfig.transition = {}

这句语句将更新的上下文变更为了 transition。使得 setPending(true) 和 前面的 2 次更新的上下文不同了。

为什么更新的上下文变动会影响 setState 的合并呢,上面简略开展讲一讲 setState 时 react 在干什么。参考 React 实战视频解说:进入学习

WorkLoop

一次 react 更新,主外围的过程是 fiber tree 的协调(reconcile),协调的作用是找到 fiber tree 中发生变化的 fiber node,最小水平地对页面的 dom tree 构造进行调整。

在进行协调时,react 提供了两种模式:Legacy mode – 同步阻塞模式和 Concurrent mode – 并行模式。
这两种模式,区别在于 fiber tree 的协调过程是否可中断。Legacy mode,协调过程不可中断;Concurrent mode,协调过程可中断。
Legacy mode:

Concurrent mode:

Concurrent mode 的意义在于:

  • 协调不会长时间阻塞浏览器渲染;
  • 高优先级更新能够中断低优先级更新,优先渲染;

react 的调度机制是 workLoop 机制。伪代码实现如下:

let taskQueue = [];   // 工作列表
let shouldTimeEnd = 5ms;   // 一个工夫片定义为 5ms
let channel = new MessageChannel();  // 创立一个 MessageChannel 实例

function workLoop() {let beginTime = performance.now();  // 记录开始工夫
    while(true) { // 循环解决 taskQueue 中的工作
        let currentTime = performance.now();  // 记录下一个工作开始时的工夫
        if (currentTime - beginTime >= shouldTimeEnd) break;  // 工夫片曾经到期,结束任务解决
        processTask();  // 工夫片没有到期,持续解决工作}
    if (taskQueue.length) { // 工夫片到期,通过调用 postMessage,申请下一个工夫片
        channel.port2.postMessage(null);
    }
}

channel.port1.onmessage = workLoop;  // 在下一个工夫片内持续解决工作
workLoop();

workLoop 有 2 种,Legacy 模式下,是 workLoopSync;Concurrent 模式下,是 workLoopConcurrent。workLoopSync 中每个工作都要实现后才会开释主过程,workLoopConcurrent 中每个工作在工夫片耗尽后会开释主过程期待下一个工夫片继续执行工作。

workLoopSync 对应 Legacy 模式。如果是在 event、setTimeout、network request 的 callback 中触发更新,那么协调时会启动 workLoopSync。在协调过程中,须要对 fiber tree 做深度优先遍历,解决每一个 fiber node。workLoopSync 开始工作当前,要等到 stack 中收集的所有 fiber node 都处理完毕当前,才会完结工作,也就是 fiber tree 的协调过程不可中断。

workLoopConcurrent 对应 Concurrent 模式。如果更新与 Suspense、useTransition、OffScreen 相干,那么协调时会启动 workLoopConcurrent。workLoopConcurrent 开始工作当前,每次协调 fiber node 时,都会判断以后工夫片是否到期。如果工夫片到期,会进行以后 workLoopConcurrent、workLoop,让出主线程,而后申请下一个工夫片持续协调。

相干源码如下:

function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {performUnitOfWork(workInProgress);
  }
}

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {performUnitOfWork(workInProgress);
  }
}

工作优先级

react 有 3 套优先级机制:

  • React 事件优先级
  • Scheduler 优先级
  • Lane 优先级

React 事件优先级如下:

// 离散事件优先级,例如:点击事件,input 输出等触发的更新工作,优先级最高
export const DiscreteEventPriority: EventPriority = SyncLane;
// 间断事件优先级,例如:滚动事件,拖动事件等,间断触发的事件
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
// 默认事件优先级,例如:setTimeout 触发的更新工作
export const DefaultEventPriority: EventPriority = DefaultLane;
// 闲置事件优先级,优先级最低
export const IdleEventPriority: EventPriority = IdleLane;

react 在外部定义了 5 种类型的调度(Scheduler)优先级:

  • ImmediatePriority, 间接优先级,对应用户的 click、input、focus 等操作;
  • UserBlockingPriority,用户阻塞优先级,对应用户的 mouseMove、scroll 等操作;
  • NormalPriority,一般优先级,对应网络申请、useTransition 等操作;
  • LowPriority,低优先级(未找到利用场景);
  • IdlePriority,闲暇优先级,如 OffScreen;

5 种优先级的程序为: ImmediatePriority > UserBlockingPriority > NormalPriority > LowPriority > IdlePriority。

react 外部定义了 31 条 lane,lane 能够了解为每个工作所处的赛道。用二进制示意,按优先级从低到高顺次为:

lane 对应的位数越小,优先级最高。如 SyncLane 为 1,优先级最高;OffscreenLane 为 31,优先级最低。

react 先将 lane 的优先级转换为 React 事件的优先级,而后再依据 React 事件的优先级转换为 Scheduler 的优先级。

当通过 startTransition 的形式触发更新时,更新对应的优先级等级为 NormalPriority。而在 NormalPriority 之上,还存在 ImmediatePriority、UserBlockingPriority 这两种级别更高的更新。通常,高优先级的更新会优先级解决,这就使得只管 transition 更新先触发,但并不会在第一工夫解决,而是处于 pending – 期待状态。只有没有比 transition 更新优先级更高的更新存在时,它才会被解决。

Concurrent 模式下,如果在低优先级更新的协调过程中,有高优先级更新进来,那么高优先级更新会中断低优先级更新的协调过程。

每次拿到新的工夫片当前,workLoopConcurrent 都会判断本次协调对应的优先级和上一次工夫片到期中断的协调的优先级是否一样。如果一样,阐明没有更高优先级的更新产生,能够持续上次未实现的协调;如果不一样,阐明有更高优先级的更新进来,此时要清空之前已开始的协调过程,从根节点开始从新协调 。等高优先级更新解决实现当前, 再次从根节点开始解决低优先级更新

setState 机制

调用 setState,并不会立刻更新组件 state。state 的更新,其实是产生在 fiber tree 的协调过程中,这个过程如下:

  1. 调用 setState
  2. 生成 update 对象:调用 setState 时传入的 new state 会存储在 update 对象的 payload 属性上
  3. 将 update 对象收集到 组件的 Fiber node 外部的 updateQueue 中
  4. 为更新创立 task:新建的 task 会增加到 taskQueue 堆顶
  5. workLoop 解决 task
  6. 协调 fiber tree
  7. 协调组件 fiber node
  8. 生成 new state:遍历 updateQueue 中所有的 update 对象,读取 payload 属性
  9. 执行组件 render
  10. fiber tree 协调实现
  11. 浏览器渲染

下面 useTransition 的例子中,间断 3 次 setState,会生成 3 个 update 对象 – update1(setPending(true)),update2(setPending(false)),update3(callback 里的 setState 调用)。这三个 update 对象会依照创立的先后顺序顺次增加到 updateQueue 中。

update 对象构造:

export function createUpdate(eventTime: number, lane: Lane): Update<*> {
  const update: Update<*> = {
    eventTime,
    lane, // 这里为 update 绑定了优先级

    tag: UpdateState,
    payload: null,
    callback: null,

    next: null,
  };
  return update;
}

因为创立 update 对象的上下文不雷同,导致 update 对象解决的机会不雷同。第一次协调时,解决 update1;第二次协调时,解决 update2 和 update3。之所以这样,是因为不同的上下文,为 update 对象绑定了的不同的 lane。

lane 决定了 update 对象的解决机会。

所以如上,update1 被调配的 lane 为 InputContinuousLane,而 update2、update3 被调配的 lane 为 TransitionLane。为每个 update 生成 lane 的源码如下:

export function requestUpdateLane(fiber: Fiber): Lane {
  ...
  const isTransition = requestCurrentTransition() !== NoTransition;
  if (isTransition) {if (currentEventTransitionLane === NoLane) {
      // All transitions within the same event are assigned the same lane.
      currentEventTransitionLane = claimNextTransitionLane();}
    return currentEventTransitionLane;
  }
  ...
}

export function requestCurrentTransition(): Transition | null {return ReactCurrentBatchConfig.transition;}

至此,曾经能够看到,update2 和 update3 被调配了较低的优先级,因而 3 次 setState 被离开成了 2 次更新。

理解了下面的原理,就能够来答复这几个问题了:

useTransition 为何能体现出 debounce 成果

高优先级更新会中断低优先级更新,优先解决。

startTransition 办法执行过程中,setPending(true) 触发的更新优先级较高,而 setPending(false)、callback 触发的更新优先级较低。当 callback 触发的更新进入协调阶段当前,因为协调过程可中断,并且用户始终在输出导致始终触发 setPending(true),使得 callback 触发的更新始终被中断,直到用户进行输出当前能力被残缺解决。

useTransition 为何能体现出 throttle 成果

如果你始终输出,最多 5s,长列表必然会渲染,和 防抖 – throttle 成果一样。
这是因为为了避免低优先级更新始终被高优先级更新中断而得不到解决,react 为每种类型的更新定义了最迟必须解决工夫 – timeout。如果在 timeout 工夫内更新未被解决,那么更新的优先级就会被晋升到最高 – ImmediatePriority,优先解决。

transition 更新的优先级为 NormalPriority,timeout 为 5000ms 即 5s。如果超过 5s,transition 更新还因为始终被高优先级更新中断而没有解决,它的优先级就会被晋升为 ImmediatePriority,优先解决。这样就实现了 throttle 的成果。

useTransition 和 startTransition 区别

用户间断输出时,应用 useTransition 会呈现 debounce 的成果,而间接应用 startTransition 则不会。

因为 startTransition 的源码:

function startTransition(scope) {
    var prevTransition = ReactCurrentBatchConfig.transition;
    ReactCurrentBatchConfig.transition = 1;  // 批改更新上下文
    try {scope();   // 触发更新
    } finally {...}
}

比照 useTransition 的 startTransition,咱们会发现 startTransition 中少了 setPending(true) 的过程。

应用 useTransition 时,transition 更新会始终被间断的 setPending(true) 中断,每次中断时都会被重置为未开始状态,导致 transition 更新只有在用户进行输出 (或者超过 5s) 时能力失去无效解决,也就呈现了相似 debounce 的成果。

而间接应用 startTransition 时,只管协调过程会每隔 5ms 中断一次,但因为没有 setPending(true) 的中断,间断的输出并不会重置 transition 更新。当 transition 更新完结协调时,自然而然地就会开始浏览器渲染过程,不会呈现相似 debounce 的成果。

Transition API 由来

React 如何优化性能

React,它自身的思路是纯 JS 写法,这种形式非常灵活,然而,这也使它在编译时很难做太多的事件,像下面这样的编译时优化是很难实现的。所以,咱们能够看到 React 几个大版本的的优化次要都在运行时。
进行运行时优化,关注的次要问题就是 CPU 和 IO。

  • 首先,就是 CPU 的问题,支流浏览器的刷新频率个别是 60Hz,也就是每秒刷新 60 次,大略 16.6ms 浏览器刷新一次。因为 GUI 渲染线程和 JS 线程是互斥的,所以 JS 脚本执行和浏览器布局、绘制不能同时执行。在这 16.6ms 的工夫里,浏览器既须要实现 JS 的执行,也须要实现款式的重排和重绘,如果 JS 执行的工夫过长,超出了 16.6ms,这次刷新就没有工夫执行款式布局和款式绘制了,于是在页面上就会体现为卡顿。
  • IO 的问题就比拟好了解了,很多组件须要期待一些网络提早,那么怎么样能力在网络提早存在的状况下,缩小用户对网络提早的感知呢?就是 react 须要解决的问题。
    React 引入 fiber 机制,可中断协调阶段,就是在 CPU 角度优化运行时性能。而 Suspense API 则是 IO 角度的优化,让新内容替换成旧内容的过程不闪屏,内容切换更晦涩。

Transition API 退场

Suspense 的作用,次要是 react 优化切换内容成果。而 Transition API 的最后提出,是为了配合 Suspense API 进行 IO 角度的优化。

useTransition 的前身是 withSuspenseConfig。Sebmarkbage 在 19 年五月份提的一个 PR 中引进了它。在 19 年 11 月更名为 useTransition。

Transition Hook 的作用是通知 React,提早更新 State 也没关系。

初版的 useTransition 的实现源码如下:

function updateTransition(config: SuspenseConfig | void | null,): [(() => void) => void, boolean] {const [isPending, setPending] = updateState(false); // 相当于 useState
  const startTransition = updateCallback(             // 相当于 useCallback
    callback => {setPending(true); // 设置 pending 为 true
      // 以低优先级调度执行
      Scheduler.unstable_next(() => {
        // ⚛️ 设置 suspenseConfig
        const previousConfig = ReactCurrentBatchConfig.suspense;
        ReactCurrentBatchConfig.suspense = config === undefined ? null : config;
        try {
          // 还原 pending
          setPending(false);

          // 执行你的回调
          callback();} finally {
          // ⚛️ 还原 suspenseConfig
          ReactCurrentBatchConfig.suspense = previousConfig;
        }
      });
    },
    [config, isPending],
  );
  return [startTransition, isPending];
}

划重点,尽管跟当初的版本有一些差异,但次要的思维仍然是:以较低的优先级运行后 2 次 setState。

一路以来,次要的批改蕴含:在做兼容数据流状态库如 redux,批改优先级的实现计划。

正文完
 0