React15和React16 调度机制
react15核心思想
维护一个虚拟dom树,当数据变化时(setState),会在will生命周期中合并更新队列自动更新虚拟DOM,得到一个新树,然后在updatecomponent阶段diff新老虚拟DOM树,找到有变化的部分,得到一个change(patch),将这个patch加入队列,最终批量更新这些path到DOM中,而且,React 并不是计算出一个差异就去执 行一次 Patch,而是计算出全部差异并放入差异队列后,再一次性地去执行 Patch 方法完成真实 DOM 的更新。简单说就是:diff + patch。
react主要可分为两个阶段
调度阶段 (Reconciler): 用新数据生成一颗新树,遍历虚拟dom,diff新老virtual dom树,搜集具体的UI差异,找到需要更新的元素,放到更新队列中。
渲染阶段(Renderer): 遍历更新队列,通过调用宿主环境的API,实际更新渲染对应元素。宿主环境,比如dom,native等。
缺陷
Fiber之前的Reconciler阶段采用的是Stack Reconciler, 其自顶向下遍历vdom tree, 递归组件执行任务,过程无法中断。
假设有一个层级很复杂的组件,在顶层组件内执行setState, 那么调用栈可能会很长。由于调用栈过长,中间可能还有一些复杂操作,这些任务无法中断,就导致主线程被长时间阻塞。由于浏览器里渲染和js执行共一个主线程,在对响应要求高的场景,比如手势,动画等,就容易造成卡顿,延迟等现象,从而影响用户体验。
React16 Fiber
卡顿原因
人眼不能分辨超过每秒30帧的画面~~
当一秒刷30帧以上时,肉眼就就觉得是连续动画无卡顿,但要求均匀刷新,所以每33ms就要刷新动画。但是,也许33ms执行完动画或者用户交互还剩下点时间,那么就轮到了requestidlework。
window里也有requestIdleWork:window.requestIdleCallback()方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。你可以在空闲回调函数中调用requestIdleCallback(),以便在下一次通过事件循环之前调度另一个回调。
这就是Fiber的主要目的啦。
总的来讲,通常,客户端线程执行任务时会以帧的形式划分,大部分设备控制在30-60帧是不会影响用户体验;在两个执行帧之间,主线程通常会有一小段空闲时间,
requestIdleCallback可以在这个空闲期(Idle Period)调用空闲期回调(Idle Callback),执行一些任务
- 低优先级任务由
requestIdleCallback处理;- 高优先级任务,如动画相关的由
requestAnimationFrame处理;requestIdleCallback可以在多个空闲期调用空闲期回调,执行任务;requestIdleCallback方法提供deadline,即任务执行限制时间,以切分任务,避免长时间执行,阻塞UI渲染而导致掉帧;
React16的两个阶段
Render阶段
内容:调度、重新计算新的state和props,重新计算dom树和fiber树。
在这个过程,每个节点都是独立的,每个节点在更新完成后都可以跳出这个更新的循环,然后根据不同的更新模式,可以分片的进行更新。从而使react把更多的优先级给浏览器,及时进行用户反馈并进行连续动画展示,从而解决卡顿问题。
Commit阶段
render阶段结束的时候在root上设置一个对象叫finishwork的对象,即rootfiber,包含一个从firsteffect到lasteffect一个链。effect链式render阶段计算出来的一些更新,且尽可能少的对node进行变更,在commit阶段就会根据这个effect链进行不同的effect更新。中间不可以像render phase那样被打断。
暂时有点朦胧,可以看下面的总概览介绍。
React Fiber总概览
首先上一下Fiber的总框图:更正:addRootToScheduler->scheduleWorkToRoot
(图片来自jokcy ,react.jokcy.me/book/featur…)
Fiber的源码过于庞大,所以本文只是对总流程的一个概述。
建立requestwork部分:
* 1 ReactDOM.render、setState、forceUpdate都会引起createUpdate,从而触发scheduleWork,然后scheduleWorkToRoot:创建root节点并计算expirationtime,返回root。然后通过判断是否正处在render阶段,或者root是不是前后不同来判断是否要继续执行还是return。如果是render阶段,或者root前后相同那就停止调度,直接return。
tips:同步的过期时间最大,过期时间越大.
**优先级**
* 2 然后进行requestwork操作,在本阶段里会先执行addRootToSchedule函数,是将root节点加入到schedule里。接下来会判断expirationtime,是进行同步任务还是异步任务。如果是同步任务会执行performsyncwork,是不需要进行存在异步callback队列里等待浏览器执行完任务后空闲时刷新的。如果是异步work就需要执行一系列函数(这里暂时不管),将work加入到callbacklist队列。
schedule阶段
可以主要参考文章。
其实schedule阶段就是要实现前面提到的window.requestIdleCallback,因为很多浏览器兼容性不支持,所以react Fiber使用了polyfill的方式进行模拟requestIdleCallback。
先看两张 amazing 的图: 首先是 React 16 之前版本的
在之前的版本里面,若 React 要开始更新的时候,就会处于深度调用的状态,程序会一直处理更新,而不会接受处理外部的输入。如果更新的层级多而深则会导致更新时间长的问题。到了 React 16 fiber 的阶段呢,如下所示;
看看 react 中设计的 requestIdleCallback ployfill。最主要的就是idleTick和animationTick,其实就是通过postMessage和addeventlistener来进行链接。
-
第一段代码中,利用浏览器都兼容的requestAnimationFrame来对requestIdleCallback进行替代。而第26行的animationTick其实就是requestAnimationFrame的回调函数。
这个函数干了啥呢,就是
-
1)更新 frameDeadline
-
2)isAnimationFrameScheduled = ture
-
3)window.postMessage(messageKey, '*')。
-
-
看第二段代码里idleTick,里面window.addEventListener('message', idleTick, false)。idleTick里主要干了是啥呢?
-
1)判断是否还有空余时间进行其他的react异步任务,如果有则执行优先级最高的异步任务
-
2)callbacklist里是否有过期任务,如果有就直接执行。
-
3)每执行完一个小任务,就判断是否还有空余时间,如果没有就执行20-26行代码继续调用requestAnimationFrame把主线程还给浏览器。
-
以上也就是最上面的流程框图里右上角的蓝框:async schedule work和下面要讲的perform阶段的大体流程
const localRequestAnimationFrame = requestAnimationFrame;
// 链表头部与尾部
let headOfPendingCallbacksLinkedList = null;
let tailOfPendingCallbacksLinkedList = null;
// frameDeadlineObject 为传入callback的参数 deadline
const frameDeadlineObject = {
didTimeout: false,
timeRemaining() {
// 通过 frameDeadline 来判断,该帧剩余时间
const remaining = frameDeadline - now();
return remaining > 0 ? remaining : 0;
},
};
// export 对外函数,也就是 requestIdleCallback ployfill
scheduleWork = function(callback, options) {
const timeoutTime = now() + options.timeout;
const scheduledCallbackConfig: CallbackConfigType = {
scheduledCallback: callback,
timeoutTime,
prev: null,
next: null,
};
// 省略将scheduledCallbackConfig插入到链表里面过程
if (!isAnimationFrameScheduled) {
isAnimationFrameScheduled = true;
localRequestAnimationFrame(animationTick);
}
}
// requestAnimationFrame 调用函数
const animationTick = function(rafTime) {
isAnimationFrameScheduled = false;
// 更新 frameDeadline
frameDeadline = rafTime + activeFrameTime;
if (!isIdleScheduled) {
isIdleScheduled = true;
window.postMessage(messageKey, '*');
}
}
// 省略消息监听处理部分
// 执行 callback,与传参 deadline
const callUnsafely = function(callbackConfig, arg) {
const callback = callbackConfig.scheduledCallback;
callback(arg);
// 总是会删除调用过的 callbackConfig
cancelScheduledWork(callbackConfig);
}
cancelScheduledWork = function(callbackConfig) {
// 在链表中删除对应节点,并维护好pre以及next关系
}
// messageKey 为特点字符串
const idleTick = function(event) {
if (event.source !== window || event.data !== messageKey) {
return;
}
isIdleScheduled = false;
callTimedOutCallbacks();
let currentTime = now();
// 空闲时间判断
while (
frameDeadline - currentTime > 0 &&
headOfPendingCallbacksLinkedList !== null
) {
const latestCallbackConfig = headOfPendingCallbacksLinkedList;
frameDeadlineObject.didTimeout = false;
callUnsafely(latestCallbackConfig, frameDeadlineObject);
currentTime = now();
}
// 继续下一个节点,调用requestAnimationFrame
if (
!isAnimationFrameScheduled &&
headOfPendingCallbacksLinkedList !== null
) {
isAnimationFrameScheduled = true;
localRequestAnimationFrame(animationTick);
}
}
window.addEventListener('message', idleTick, false);
// 如果设置了 timeoutTime 的话,自然是无脑执行到底的,而不会把时间让渡予下一帧
const callTimedOutCallbacks = function() {
const currentTime = now();
const timedOutCallbacks = [];
let currentCallbackConfig = headOfPendingCallbacksLinkedList;
while (currentCallbackConfig !== null) {
if (timeoutTime !== -1 && timeoutTime <= currentTime) {
timedOutCallbacks.push(currentCallbackConfig);
}
}
// 存在 timeoutTime 的事件,并且发生超时了,那就执行,不考虑帧的问题了
if (timedOutCallbacks.length > 0) {
frameDeadlineObject.didTimeout = true;
for (let i = 0, len = timedOutCallbacks.length; i < len; i++) {
callUnsafely(timedOutCallbacks[i], frameDeadlineObject);
}
}
}
preform阶段
简单来说就是:如果是同步任务,没有deadline,立即执行同步任务。如果有deadline,就执行异步任务,没执行完一个异步任务就判断是否还有空余时间,如果没有了就回去再执行schedulecallbackwithexpirationtime把任务放到callbacklist里等待下一个33ms的空余时间来执行。
commit阶段
上面的框图呢其实就只是render阶段的,只是在执行过程中更新state和props,js逻辑运算、虚拟节点树进行更新,但是并没有真正的将效果反馈在页面上。
render阶段时,会有一个current fiber tree也就是上面一直说的在schedulework一开始就会创建的root对象,同时也会根据current fiber tree和更新state和props构建workinprogress(其实也是个fiber tree),然后在构建过程中会对有变化的节点进行effect tag标签,这个effect对象就挂在Root(即fiber tree)上,如下图。同时,会有一个effect 单链表,这个单链表已经在render阶段做好了计算,能够尽可能的少更新节点(毕竟commit阶段不能中途停下)。在commit阶段呢,就是根据root上的effect 链表进行节点更新并渲染的。
假设这里有个react页面,是点击button 会对item里的state进行平方并渲染。
第一次 render 的时候会生成下图所示的 Fiber Tree:
因为我需要对 Item 里面的数值做平方运算,于是我点击了 Button,react 根据之前生成的 Fiber Tree 开始构建workInProgress Tree。在构建的过程中,以一个 fiber 节点为单位自顶向下对比,如果发现根节点没有发生改变,根据其 child 指针,把 List 节点复制到 workinprogress Tree 中。 每处理完一个 fiber 节点,react 都会检查当前时间片是否够用,如果发现当前时间片不够用了,就是会标记下一个要处理的任务优先级,根据优先级来决定下一个时间片要处理什么任务。
在平方运算这一过程中,react 通过依次对比 fiber 节点发现 List,Item2,Item3 发生了变化,就会在对应生成的 workInProgress Tree 中打一个 Tag,并且推送到 effect list 中。
当调度阶段也就是render阶段结束后,根节点的 effect list 里记录了包括 DOM change 在内的所有 side effect,在第二阶段(commit)执行更新操作,这样一个流程就算结束了。
但是commit的具体流程本文就不讲了,因为React16最重要的还是render阶段的粒度分化与根据优先级执行异步work。况且我也没仔细看。
有兴趣的同学可以继续看下:Lin Clark去年 react conf 中的演讲。