React更新原理

React 框架内部的更新机制原理详解

基本架构

React16之后架构分为三层

在框中的执行过程中,随时可能会被打断,异步的可中断更新(由于更新都是不断在内存中,并不会去执行页面DOM更新,用户是无感的)

  1. 有其他更高优先级的任务
  2. 当前帧的没有剩余时间的时候(requestdleCallback)

Scheduler(调度器) – 根据任务的优先级调度

React在浏览器每一帧的时间中,会预留一定时间给JS线程计算更新,当预留时间不够,会将线程控制权交还给浏览器,等待下一帧时间来到,继续被中断的工作

因此需要有一种机制去完成这样一套调度,即React中的**Scheduler(调度器)**,除了在空闲的时触发回调的功能,Scheduler还提供了多种调度优先级供任务设置

组件的更新流程如下

  • React 组件状态更新,向 Scheduler 中存入下一个任务,该任务为 React 更新算法,Scheduler提供pushTask()方法,React 通过该方法存入任务
  • Scheduler 调度该任务,执行 React 更新算法,提供ScheduleTack()方法,用于调度任务。
  • React 在调和阶段更新一个Fiber之后,会询问Scheduler是否需要暂停,如果不需要暂停,则重复该步骤,继续更新下一个Fiber,提供shouldYield(),React 通过该方法决定是否需要暂停执行该任务
  • 如果 Scheduler 表示需要暂停,则 React 将返回一个函数,该函数用于告诉 Scheduler 任务还没有完成。Scheduler 将在未来某时刻调度该任务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const scheduler = {
pushTask() {
// 1. 存入任务
}
scheduleTack() {
// 2. 挑选一个任务并执行
const tack = pickTask()
const hasMoreTask = task()
if(hasMoreTask) {
// 4. 之后调用
}
}
shouldYield() {
// 3. 由调用方调用,调用方判断是否需要暂停
}
}

MessageChannel的作用

scheduler.shouldYield() 返回 true 后,Scheduler需要满足一下特点

  • 暂停JS执行,将主线程还给浏览器,让浏览器能够去更新UI页面
  • 在未来某个时刻继续调度任务,执行上次还没有完成的任务

需要满足这两点就需要调度一个宏任务,因为宏任务是在下次事件循环中执行,不会阻塞本次页面更新,而微任务是在本次页面更新前执行,与同步执行无异,不会让出主线程。

使用MessageChannel的目的就是为了产生宏任务,伪代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = scheduler.scheduleTask
const scheduler = {
scheduleTask() {
// 挑选一个任务并执行
const task = pickTask()
const continuousTask = task()

// 如果当前任务未完成,则在下个宏任务继续执行
if (continuousTask) {
port.postMessage(null)
}
},
}

Reconciler(协调器) – 负责找出变化的组件

React协调器,作用之一是负责DOM的更新,包括接收和处理新DOM,生成VDOM进行diff等

VDOM:虚拟DOM,是JSX描述DOM结构的JS代码抽象表示,本质上是一个对象结构

React会先将我们书写的JSX代码,转化成一个JS对象,在将JS对象转化成真实的DOM,这个JS对象既是所谓的虚拟DOM,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const testRender = () => {
return (
<div class="title">
<span>Hello React</span>
<ul>
<li>苹果</li>
<li>橘子</li>
</ul>
</div>
)
}

// 为方便理解省略一些属性
const VitrualDom = {
type: 'div',
props: { class: 'title' },
children: [
{
type: 'span',
children: 'Hello React'
},
{
type: 'ul',
children: [
{ type: 'li', children: '苹果' },
{ type: 'li', children: '橘子' }
]
}
]
}

当我们需要创建或更新元素时,React 首先会让这个虚拟 dom 对象进行创建和更改,然后再将虚拟 dom 对象渲染成真实DOM;当我们需要对DOM进行事件监听时,首先对VitrualDom进行事件监听,VitrualDom会代理原生的DOM事件从而做出响应。

为什么存在虚拟DOM?

  • 真实DOM渲染成本很高,本质上是用JS的执行速度换区DOM执行成本的操作,一次DOM树的重写计算布局和重绘可能导致页面的更新异常卡顿,通过虚拟DOM和diff算法的结合,减少不必要的多次重新渲染,
  • 数据驱动视图,操作简单,具有跨平台的优势

Fiber:本质上是一个对象,包含了VDOM和一些更新工作的字段属性

1
2
3
4
5
6
7
8
9
10
11
type Fiber {
tag: WorkTag
stateNode: dom节点
return: father-node
child: son-node
sibling: brother-node
pendingProps: new-props
memoizedProps: pre-props
updateQueue: fiber对应的组件产生的update放入这个队列中
memoizedState: pre-state
}

React 16之后会将虚拟DOM进行转化,最终会转化成Fiber结构,每个节点都是Fiber对象,通过return、child、sibling属性连接起来

在React16之前:

Stack Reconcilation:每次更新需要对比新旧虚拟DOM树操作,找出更新的内容(Patch),通过打补丁的方式更新DOM树,当对比的节点非常多的时候会一直占用浏览器的资源,导致用户响应得不到更新,同步且不可中断,感觉明显卡顿

vdom –> 页面:通过递归将vdom渲染到页面上(递归过程包含diff和提交)

  • 在页面元素多时,递归调用栈层级过深,耗能大
  • 在频繁刷新时,生成多个递归任务,且递归不可中断,JS是单线程,会出现JS执行时间过长的现象,JS和GUI线程是互斥的,运行在浏览器的渲染主线程上面,JS未完成的时候,GUI线程阻塞,导致页面失去响应
  • 当JS递归耗时过长时,GUI的渲染时机也会被推迟,导致渲染间隔变长,帧率降低,对用户来说就是卡顿

在React16之后:

Fiber Reconcilation:使用时间分片方案,将一次性的任务拆成一个个的异步任务,在浏览器的空闲时间执行

vdom –> fiber –> 页面:引入Fiber概念,通过vdom生成fiber再进行页面渲染(将diff和提交划分为两个阶段–协调阶段和提交阶段)

  • 支持增量渲染:以前的渲染是全量渲染,即一次性将整个页面进行更新,而现在可以根据浏览器是否有空闲时间实现部分更新(空闲就继续更新,忙就中断更新,下次空闲从标记处继续更新)
  • 支持暂停、终止以及恢复渲染任务:浏览器空闲就执行渲染任务,否则就暂停渲染,将控制权让回浏览器;下次浏览器空闲时,还可以从暂停中断处恢复渲染
  • 任务优先级:优先级高的先执行,比如事件交互响应,页面渲染等,像网络请求之类的被延后执行
  • 支持并发处理:并发情况下,始终处理最高优先级,灵活调整处理顺序,保证重要的任务都会在允许的最快时间内响应

Fiber架构是什么完成的?

  • 增量渲染、暂停、终止、恢复
    使用Fiber这种类似链表的结构,每个节点可以被理解为一个执行单元,对其进行遍历(深度、先序)使用循环,每次遍历完一个节点(执行完一个执行单元),都会判断是否需要中断(时间分片),如果是那么就可以中断循环并保存当前位置,下次又能很好的从断点恢复循环,为增量渲染提供了必要条件

  • 实现中断和恢复,浏览器提供了requestAnimationFrame 和 requestIdleCallback,react中使用MessageChannel+requestAnimationFrame 模拟了requestCallback

    • requestAnimationFrame:在Fiber中使用,是浏览器提供的绘制动画的API,要求浏览器在下次重绘之前调用指定的回调函数更新动画
    • requestIdleCallback:在一帧中,说明有多余的空闲时间,此时就会执行requestIdleCallback里注册的任务
  • 任务优先级、支持并发处理:当处理fiber的过程是一个循环并且可以被中断时,就可以实现按优先级进行调度,当有优先级更高的任务时,中断处理fiber的过程,优先处理高优任务

Fiber执行阶段

  • 协调阶段(diff):可被终止,找出节点变更或增删属性变化,每次diff完由fiber决定是否交出控制权
  • 提交阶段(commit):将vdom的所有变化更新到fiber上并使用effectTag保存变更需求(副作用),接下去react会将fiber节点中的effect处理成一条effect链,一次性将effect提交到页面上,由于此过程是用户感知的,并且需要处理一些异步请求、useEffect等,所以不可中断,必须同步执行。

Render(渲染器) – 负责将变化的组件渲染到页面上

在每次更新发生时,Renderer 接到 Reconciler通知,将变化的组件渲染在当前宿主环境。不同平台有不同的Renderer,浏览器环境渲染,比如ReactDOM,ReactNative