React15的调度策略
JavaScript是单线程运行的。在浏览器环境中,他需要负责页面的Js解析和执行、绘制、事件处理、静态资源加载和处理。而且只能一个任务一个任务的执行,如果其中某个任务耗时很长,那后面的任务则执行不了,在浏览器端则会呈现卡死的状态。
React15的渲染和diff会递归比对VirtualDOM树,找出有增删改的节点,然后同步更新他们,整个过程是一气呵成的。那么如果页面节点数量非常庞大,React会一直霸占着浏览器资源,一则会导致用户触发的事件得不到响应,二则会导致掉帧,用户会感知到这些卡顿。
所以针对上述痛点,我们期望将找出有增删改的节点,然后同步更新他们这个过程分解成两个独立的部分,或者通过某种方式能让整个过程可中断可恢复的执行,类似于多任务操作系统的单处理器调度。
浏览器任务调度策略和渲染流程
帧
那么一个帧包含什么呢?
- 一帧平均是16.66ms(1s/60),主要分为以下几个部分:脚本执行 - 样式计算 - 布局 - 重绘 - 合成
- js引擎和页面渲染引擎是在同一个渲染线程,两者是互斥的。当某个任务执行时间过长,浏览器会推迟渲染
requestAnimationFrame
requestAnimationFrame的callback会在浏览器每次绘制前执行,执行的时机:浏览器刷新下一帧渲染周期的起点上
requestIdleCallback
requestIdleCallback使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响关键事件,如动画和输入响应。当正常帧任务完成不超过16ms,此时就会执行requestIdleCallback中注册的任务
requestAnimationCallback的回调会在每一帧开始时确定执行,属于高优先级任务;requestIdleCallback回调属于低优先级任务,有富余时间时才执行
当浏览器很长没有任务执行时,requestIdleCallback的时间会延长,最长是50ms
Fiber
什么是Fiber
Fiber是一个执行单元
Fiber是一个执行单元,每次执行完一个执行单元后,react会检查还剩多少时间,如果没有时间就将控制权交还浏览器;然后继续进行下一帧的渲染。
下图为React结合空闲阶段的调度过程
这是一种合作式调度,需要程序和浏览器互相信任。浏览器作为领导者,会分配执行时间片(即requestIdleCallback)给程序去选择调用,程序需要按照约定在这个时间内执行完毕,并将控制权交还浏览器。
Fiber也是一种数据结构
React中使用链表将Virtual DOM链接起来,每一个虚拟节点表示一个Fiber。child指向下个节点,sibling指向同级节点return指向父级节点
Fiber小结
- 我们可以通过某些调度策略合理分配CPU资源,从而提高用户的响应速度
- 通过Fiber架构,让自己的Reconciliation过程变得可被中断,适时地让出CPU执行权,可以让浏览器及时地响应用户的交互
任务循环
将fiber作为一个执行单元,每次执行完一个执行单元, React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去。
function workLoop (deadline) {
// 这一帧渲染还有空闲时间 || 没超时 && 还存在一个执行单元
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork) // 执行当前执行单元 并返回下一个执行单元
}
if (!nextUnitOfWork) { // 不存在执行单元时,此阶段完成
console.log('render end !')
} else {
requestIdleCallback(workLoop, { timeout: 1000 }) // 如果时间不够,在再次等待浏览器的空闲时间执行任务循环
}
}
requestIdleCallback(workLoop, {timeout: 500});
// 从根节点开始,遍历执行单元。
function performUnitOfWork (fiber) {
beginWork(fiber) // 开始
if (fiber.child) {
return fiber.child
}
while (fiber) {
completeUnitOfWork(fiber) // 结束
if (fiber.sibling) {
return fiber.sibling
}
fiber = fiber.return
}
}
Fiber执行阶段
每次渲染有两个阶段:
- Reconciliation协调/render阶段:可以认为是Diff阶段,这个阶段可以被中断,这个阶段会找出所有节点变更,例如节点增删改等等,这些变更在React中称为Effect(副作用)。
- Commit提交阶段:将上一个阶段计算出来的需要处理的副作用一次性执行。这个阶段不能中断,必须同步一次性执行完。
Reconciliation阶段
-
从顶点开始遍历
-
如果有son,则进行遍历;如果没有,此节点遍历完成
-
如果sibling,则进行遍历;如果没有,返回父节点标识完成父节点遍历;如果有叔叔,遍历叔叔
-
没有父节点,遍历结束
-
son--> sibling -->uncle
-
beginWork
1)创建真实dom;没有真实节点时,创建真实dom+添加属性;2)遍历子fiber树,将所有节点fiber化
function beginWork(currentFiber) {
console.log('开始' + currentFiber.key)
if (!currentFiber.stateNode) {
currentFiber.stateNode = document.createElement(currentFiber.type);//创建真实DOM
for (let key in currentFiber.props) {//循环属性赋值给真实DOM
if (key !== 'children' && key !== 'key')
currentFiber.stateNode.setAttribute(key, currentFiber.props[key]);
}
}
let previousFiber;
currentFiber.props.children.forEach((child, index) => {
let childFiber = {
tag: 'HOST',
type: child.type,
key: child.key,
props: child.props,
return: currentFiber,
effectTag: 'PLACEMENT',
nextEffect: null // 下一个有副作用的节点
}
if (index === 0) {
currentFiber.child = childFiber; // a. child指向子fiber
} else {
previousFiber.sibling = childFiber; // c. 上一个fiber的sibling指向新的fiber
}
previousFiber = childFiber; // b. 变成上一个fiber
});
}
-
completeUnitOfWork
当前fiber结束,构建effectList副作用链。firstEffect执行第一个有副作用的子节点,lastEffect指向最后一个有副作用的子节点;中间通过nextEffect链接
function completeUnitOfWork(currentFiber) {
console.log('结束' + currentFiber.key)
const returnFiber = currentFiber.return;
if (returnFiber) {
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = currentFiber.firstEffect;
}
if (currentFiber.lastEffect) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
}
returnFiber.lastEffect = currentFiber.lastEffect;
}
if (currentFiber.effectTag) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber;
} else {
returnFiber.firstEffect = currentFiber;
}
returnFiber.lastEffect = currentFiber;
}
}
}
Commit阶段
在performUnitOfWork过程中,通过beginWork将所有节点fiber化。经过completeUnitOfWork,形成effectList链表,即将所有真实DOM、虚拟DOM、Fiber结合,其副作用(增删改)形成一个链表结构。接下来,需要将其渲染到页面。
function workLoop (deadline) {
// ...
if (!nextUnitOfWork) {
commitRoot()
} else {
requestIdleCallback(workLoop, { timeout: 1000 })
}
}
function commitRoot() {
let fiber = workInProgressRoot.firstEffect
while (fiber) { // 从firstEffect开始,通过nextEffect找到每个有副作用的fiber节点,根据副作用类型递归进行dom增删改
commitWork(fiber)
fiber = fiber.nextEffect
}
workInProgressRoot = null
}
function commitWork(currentFiber) { // 根据副作用类型递归进行dom增删改
...
}
Render阶段
全局定义当前页面对应的fiber,即currentRoot;内存中构建的fiber,即workInProgressRoot;下一个工作单元,即nextUnitOfWork
初次render
render阶段,从根节点开始进行调度渲染。将rootFiber赋值给workInProgressRoot后,进一步赋值给nextUnitOfWork。当浏览器有时间时执行。
let currentRoot = null; //当前页面对应的根Fiber
let workInProgressRoot = null; //正在渲染中的根Fiber
let nextUnitOfWork = null; // 下一个工作单元
function render(element, container) {
let rootFiber = {
tag: TAG_ROOT,
stateNode: container,
props: { children: [element] }
}
scheduleRoot(rootFiber);
}
function scheduleRoot(rootFiber) {
if (rootFiber) {
workInProgressRoot = rootFiber; //把当前树设置为nextUnitOfWork开始进行调度
} else {
...
}
nextUnitOfWork = workInProgressRoot;
}
更新render
当通过useReducer更新状态时,会主动调用scheduleRoot方法进行调度渲染。
回顾下useReducer的实现:
function useReducer(reducer, initialValue) {
let newHook = workInProgressFiber.alternate.hooks[hookIndex];
if (newHook) {
newHook.state = newHook.updateQueue.forceUpdate(newHook.state); // 链表结构进行状态更新
} else {
newHook = {
state: initialValue,
updateQueue: new UpdateQueue()
};
}
const dispatch = action => {
newHook.updateQueue.enqueueUpdate(
new Update(reducer ? reducer(newHook.state, action) : action)
);
scheduleRoot(); // 调度渲染
}
workInProgressFiber.hooks[hookIndex++] = newHook;
return [newHook.state, dispatch];
}
在更新时,通过双缓存机制实现fiber树的复用
双缓存机制
-
在进行每次页面绘制前,会先清除上一次绘制的内容。如果当前绘制的计算量比较大,会导致上一次清除到当前内容绘制完成的时间间隙较长,出现白屏闪烁。为了解决这个问题,会在内存中保存当前帧的绘制,等绘制完毕替换上一帧内容,避免画面闪烁。这种在内存中构建并直接替换的技术叫做双缓存。
-
React 根据双缓冲的机制维护了两棵树:
- 一棵是 Fiber 树用于渲染页面;
- 一棵是 WorkInProgress Fiber 树,用于在内存中构建,方便在构建完成时直接替换用于渲染页面的 Fiber 树。
let currentRoot = null; //当前的根Fiber
let workInProgressRoot = null; //正在渲染中的根Fiber
let nextUnitOfWork = null; //下一个工作单元
function scheduleRoot(rootFiber) {
if (rootFiber) {
workInProgressRoot = rootFiber; // 1. 初次render时,把当前树设置为nextUnitOfWork开始进行调度
} else {
if (currentRoot.alternate) { // 3. 再一次数据更新时,为了fiber树的复用,将workInProgressRoot指向currentRoot.alternate,即第一次渲染时的fiber树
workInProgressRoot = currentRoot.alternate;
workInProgressRoot.alternate = currentRoot;
} else { // 2. 第一次数据更新时,没有传rootFiber,此时,将currentRoot复制给workInProgressRoot,并将workInProgressRoot指向currentRoot即上次fiber树
workInProgressRoot = {
...currentRoot,
alternate: currentRoot
}
}
}
nextUnitOfWork = workInProgressRoot;
}
function commitRoot() {
...
currentRoot = workInProgressRoot;
workInProgressRoot = null;
}