React更新原理
React更新原理
月晕React 框架内部的更新机制原理详解
基本架构
React16之后架构分为三层
在框中的执行过程中,随时可能会被打断,异步的可中断更新(由于更新都是不断在内存中,并不会去执行页面DOM更新,用户是无感的)
- 有其他更高优先级的任务
- 当前帧的没有剩余时间的时候(
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 | const scheduler = { |
MessageChannel的作用
在scheduler.shouldYield()
返回 true
后,Scheduler需要满足一下特点
- 暂停JS执行,将主线程还给浏览器,让浏览器能够去更新UI页面
- 在未来某个时刻继续调度任务,执行上次还没有完成的任务
需要满足这两点就需要调度一个宏任务,因为宏任务是在下次事件循环中执行,不会阻塞本次页面更新,而微任务是在本次页面更新前执行,与同步执行无异,不会让出主线程。
使用MessageChannel的目的就是为了产生宏任务,伪代码如下
1 | const channel = new MessageChannel() |
Reconciler(协调器) – 负责找出变化的组件
React协调器,作用之一是负责DOM的更新,包括接收和处理新DOM,生成VDOM进行diff等
VDOM:虚拟DOM,是JSX描述DOM结构的JS代码抽象表示,本质上是一个对象结构
React会先将我们书写的JSX代码,转化成一个JS对象,在将JS对象转化成真实的DOM,这个JS对象既是所谓的虚拟DOM,如下
1 | const testRender = () => { |
当我们需要创建或更新元素时,React 首先会让这个虚拟 dom 对象进行创建和更改,然后再将虚拟 dom 对象渲染成真实DOM;当我们需要对DOM进行事件监听时,首先对VitrualDom进行事件监听,VitrualDom会代理原生的DOM事件从而做出响应。
为什么存在虚拟DOM?
- 真实DOM渲染成本很高,本质上是用JS的执行速度换区DOM执行成本的操作,一次DOM树的重写计算布局和重绘可能导致页面的更新异常卡顿,通过虚拟DOM和diff算法的结合,减少不必要的多次重新渲染,
- 数据驱动视图,操作简单,具有跨平台的优势
Fiber:本质上是一个对象,包含了VDOM和一些更新工作的字段属性
1 | type Fiber { |
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