跳到主要内容

React 异步渲染

React 异步渲染是指 React 在更新 DOM 时,能够将渲染工作分割成多个小的任务块,并在浏览器的空闲时段内执行这些任务块,而不是一次性完成所有的更新工作。这个特性是通过 React 16 引入的 Fiber 架构来实现的,旨在提高应用的响应性和性能。

渲染流程

[开始] 


[JSX 转换成 React.createElement 调用]


[创建 React 元素]


[调用 ReactDOM.render]


[构建/更新 Fiber 树]──────────────────┐
│ │
│<───[可以被中断和恢复]──>[时间分片]│
│ │
▼ │
[Diffing: 新旧 Fiber 树比较] │
│ │
▼ │
[优先级赋予: 根据更新重要性排序] │
│ │
▼ │
[提交阶段: 应用变更到 DOM] │
│ │
▼ │
[生命周期方法和副作用处理] │
│ │
▼ ┘
[渲染完成,等待用户交互]


[下一个更新触发]


[返回构建/更新 Fiber 树]

  1. 初始化阶段
  • JSX 转换:首先,JSX(React 的标记语法)在构建时被转换为 React.createElement 调用。
  • createElement:这个调用会生成一个所谓的 React 元素(一个轻量级的描述界面的对象)。
  • render:React 元素通过 ReactDOM.render 方法被提交给 React 的渲染器。
  1. 调和(Reconciliation)阶段
  • Fiber 树构建:React 开始构建或更新 Fiber 树。Fiber 是对组件的一种内部表示,每个组件都对应一个 Fiber 节点,这些节点构成了一个树状结构。
  • 任务分割:在 Fiber 架构中,调和过程被设计为可以被中断的任务。这允许 React 将长时间的渲染任务分割成小的单位,根据需要暂停和恢复,以避免阻塞浏览器的主线程。
  • Diff 算法:React 对比新旧两棵 Fiber 树,确定哪些部分需要更新。这个过程被称为 Diffing,通过它 React 可以最小化 DOM 操作,提高性能。
  • 优先级赋予:不同的更新会根据其重要性赋予不同的优先级。React 会优先处理高优先级的更新。
  1. 提交(Commit)阶段
  • DOM 更新:一旦所有必要的 Fiber 节点被处理并且生成了新的树,React 会进入提交阶段。在这个阶段,React 会实际上更新 DOM,应用所有更改。
  • 生命周期方法:在提交阶段,React 会按顺序调用所有相关的生命周期方法,例如 componentDidMount 或 componentDidUpdate。
  • 副作用处理:这个阶段也用于处理诸如数据获取或直接的 DOM 操作这类的副作用。
  1. 渲染完成
  • 用户交互:更新被应用到 DOM 后,用户可以看到最新的界面。此时,应用处于等待交互的状态,直到下一个更新被触发。

React 16+ 的渲染流程

  • scheduler 选择优先级高的任务进入 reconciler
  • reconciler 计算变更内容
  • react-dom 将编程内容渲染到页面

性能瓶颈

  • JS 任务执行时间过长

    • 浏览器刷新频率为60Hz,同时JS线程和渲染线程是互斥的。所以,当JS线程执行任务的时间超过16.6ms,就会导致掉帧、卡顿。解决方案就是利用空闲时间进行更新,不影响渲染进行。
    • 浏览器一帧需要执行大概10ms,所以,留给React只有5ms左右时间
    • 把一个耗时任务切分成一个个小任务,分布在每一帧里的方式叫做时间切片
  • JS和渲染线程互斥原因

    • JS 是可以操作DOM,如果同时执行,导致渲染异常
  • 异步执行,导致原来的执行时间变长(分割成一个个子任务)

一个帧

  • 输入事件(Inuput events)
  • Javascript (Timer)
  • Begin Frame(1.resize2.scroll3.media query change)
  • requestAnimationFrame
  • Layout(布局:1.计算样式2.更新布局)
  • Paint(绘制:1.更新组件2.)
  • idle peroid(空闲阶段)

requestIdleCallback

将在浏览器的空闲时间段内调用排队的函数。函数一般会按照先进先调用的顺序执行,然而,当回调函数指定了超时时间 timeout,则有可能为了在超时前执行函数而打乱执行顺序

https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback

设计理念

  • 快速响应
  • 异步可中断(把一个任务切分成一个个子任务)
  • 增量更新(只更新改动的内容)

不推荐使用的接口

  • componentWillMount
  • componentWillUpdate
  • componentWillReceiveProps

因为现在每个任务都被分割成一个个异步的子任务,因此如果保留,将会导致每次都被调用。

代码演示

// 同步sleep函数
function sleep(delay) {
for(let ts=Date.now();Date.now() - ts >=delay;){}
}
const works = [() => {
console.log('任务1 start')
sleep(0)
console.log('任务1 end')
},() => {
console.log('任务2 start')
sleep(5)
console.log('任务2 end')
},() => {
console.log('任务3 start')
sleep(10)
console.log('任务3 end')
},() => {
console.log('任务4 start')
sleep(20)
console.log('任务4 end')
}]
// 执行空闲任务
requestIdleCallback(workLoop)

function workLoop(deadline) {
// 在一帧内有剩余时间&还有任务待执行
while(deadline.timeRemaining() > 1 && works.length > 0) {
finishWork()
}
// 没有剩余时间&任务未完成
if(works.length > 0) {
requestIdleCallback(workLoop)
}
// 任务都完成,结束
}
// 执行任务
function finishWork() {
if(works.length === 0) return
const work = works.unshift()
work()
}

Fiber 架构

Fiber 是 React 16 中的一个重大更新,它重新实现了 React 的核心算法。Fiber 架构的目标是增强 React 在动画、布局、手势、暂停、中断、重用等方面的能力。Fiber 架构使得 React 能够进行“时间分片”(Time Slicing)和“任务优先级”(Priority Levels)的管理。

时间分片(Time Slicing)

时间分片允许 React 将更新过程分解成多个小任务,并根据浏览器的帧率安排这些任务的执行,而不是连续不断地执行,这样即使在复杂的 UI 更新中,应用也能保持平滑响应。通过这种方式,React 可以在浏览器空闲时执行低优先级的更新,而在需要快速响应时立即执行高优先级的更新。

任务优先级

React Fiber 引入了任务优先级的概念,允许 React 根据更新的紧急程度来调整任务的执行顺序。例如,用户的交互和动画等具有较高优先级,而数据的预取和不可见元素的渲染可以被分配较低的优先级。

React 与 Vue 差异

React 的响应式系统与 Vue 的实现有所不同。React 通过其状态管理和组件生命周期来实现响应式渲染,而不是依赖于代理对象或依赖追踪。当状态(state)或属性(props)发生变化时,React 会重新渲染组件,以此来响应数据的变化。这里是如何结合响应式系统和渲染流程的: