浏览器渲染
屏幕刷新率(FPS)
- 浏览器的正常绘制频率是60次/秒,小于这个值时,用户会感觉到卡顿
- 绘制一次的称为一帧,平均每帧16.6ms
帧
- 每个帧的开头包括样式计算、布局和绘制
- js的执行是单线程,js引擎和页面渲染引擎都占用主线程,GUI渲染和Javascript执行两者是互斥的
- 如果某个js任务执行时间过长,浏览器会推迟渲染,每帧的绘制时间超过16.6ms,造成页面卡顿
- requestAnimationFrame回调函数会在绘制之前执行
- 在绘制之后,如果还有剩余时间,会执行 requestIdleCallback
requestIdleCallback
- requestIdleCallback使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
- 正常帧任务完成后没超过16.6ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务
requestIdleCallback
在部分低版本浏览器中不支持,React内部是通过MessageChannel
来实现的
<script>
function sleep(d) {
for (var t = Date.now(); Date.now() - t <= d;);
}
const works = [
() => {
console.log("第1个任务开始");
sleep(20);//sleep(20);
console.log("第1个任务结束");
},
() => {
console.log("第2个任务开始");
sleep(20);//sleep(20);
console.log("第2个任务结束");
},
() => {
console.log("第3个任务开始");
sleep(20);//sleep(20);
console.log("第3个任务结束");
},
];
requestIdleCallback(workLoop, { timeout: 1000 });
function workLoop(deadline) {
console.log('本帧剩余时间', parseInt(deadline.timeRemaining()));
while ((deadline.timeRemaining() > 1 || deadline.didTimeout) && works.length > 0) {
performUnitOfWork();
}
if (works.length > 0) {
console.log(`只剩下${parseInt(deadline.timeRemaining())}ms,时间片到了等待下次空闲时间的调度`);
requestIdleCallback(workLoop);
}
}
function performUnitOfWork() {
works.shift()();
}
</script>
Fiber 解决了什么问题
React的渲染分为协调和提交两个阶段,协调就是遍历dom树,进行domdiff,收集差异的阶段。提交就是修改真实dom,进行页面绘制的阶段。
Fiber之前的协调
- React 会递归遍历节点,比对VirtualDOM树,找出需要变动的节点,然后同步更新它们。这个过程 React 称为Reconcilation(协调)
- 在Reconcilation期间,React 会一直占用着浏览器资源,一则会导致用户触发的事件得不到响应, 二则会导致掉帧,用户可能会感觉到卡顿
Fiber的协调
- fiber使用链表数据结构,通过浏览器
requestIdleCallback
api,让协调过程实现了可中断执行,分片完成协调任务 - 在浏览器空闲时去执行协调过程,遍历节点,收集变动的节点,避免了界面卡顿
什么是 React fiber
Fiber是一种调度策略
- 我们可以通过某些调度策略合理分配CPU资源,从而提高用户的响应速度
- 通过Fiber架构,让自己的协调过程变成可被中断。 适时地让出CPU执行权,让浏览器及时地响应用户的交互
Fiber是一个执行单元
- Fiber是一个执行单元,每次执行完一个执行单元, React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去
Fiber是一种数据结构
- React Fiber采用的链表的数据结构
- 内部会将虚拟Dom转化成Fiber节点,通过链表结构将整个dom树表示出来
- Fiber树采用孩子兄弟链表表示法,父节点的
child
指向第一个子节点,子节点的sibling
指向下一个兄弟节点,子节点的return
指向父节点
let fiberNode = {
tag, // fiber类型 Host/Root
type, // 虚拟dom的类型 div/span, 如果是类组件,这里是类名
props, // 虚拟dom的属性
stateNode, // 真实dom,如果是类组件,这里是类的实例
// 构建链表的三个指针
child, // 指向大儿子
sibling, // 指向二弟
return, // 指向父节点
// 构建Effect List的指针
firstEffect, // 第一个有副作用的子节点
nextEffect,
lastEffect,
effectTag, // 副作用类型 插入、更新、删除
}
React fiber的调度顺序
react执行阶段
- 每次渲染有两个阶段:Reconciliation(协调阶段)和Commit(提交阶段)
- 协调阶段: 可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等, 这些变更React 称之为副作用(Effect)
- 提交阶段: 将上一个阶段收集的需要处理的副作用(Effects)一次性执行,将修改应用到真实Dom节点。这个阶段必须同步执行,不能被打断
构建Fiber树
- 通过babel打包编译,将JSX元素转化为React.createElement()方法
- render阶段,创建虚拟Dom节点,通过虚拟Dom创建fiber节点,构建Fiber链表
Fiber树的遍历和完成顺序
- 从顶点开始遍历
- 如果有第一个儿子,先遍历第一个儿子
- 如果没有第一个儿子,标志着此节点遍历完成
- 如果有弟弟遍历弟弟
- 如果有没有下一个弟弟,返回父节点标识完成父节点遍历,如果有叔叔遍历叔叔
- 没有父节点遍历结束
- 遍历顺序:A1 B1 C1 C2 B2
- 完成顺序:C1 C2 B1 B2 A1
let A1 = { type: 'div', key: 'A1' };
let B1 = { type: 'div', key: 'B1', return: A1 };
let B2 = { type: 'div', key: 'B2', return: A1 };
let C1 = { type: 'div', key: 'C1', return: B1 };
let C2 = { type: 'div', key: 'C2', return: B1 };
A1.child = B1;
B1.sibling = B2;
B1.child = C1;
C1.sibling = C2;
// 根fiber
const rootFiber = A1;
//下一个工作单元
let nextUnitOfWork = null;
//render工作循环
function workLoop() {
while (nextUnitOfWork) {
//执行一个任务并返回下一个任务
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
//render阶段结束
}
function performUnitOfWork(fiber) {
beginWork(fiber);
if (fiber.child) {//如果子节点就返回第一个子节点
return fiber.child;
}
while (fiber) {//如果没有子节点说明当前节点已经完成了渲染工作
completeUnitOfWork(fiber);//可以结束此fiber的渲染了
if (fiber.sibling) {//如果它有弟弟就返回弟弟
return fiber.sibling;
}
fiber = fiber.return;//如果没有弟弟让爸爸完成,然后找叔叔
}
}
function beginWork(fiber) {
console.log('beginWork', fiber.key);
//fiber.stateNode = document.createElement(fiber.type);
}
function completeUnitOfWork(fiber) {
console.log('completeUnitOfWork', fiber.key);
}
nextUnitOfWork = rootFiber;
// 请求浏览器分配空闲时间片,执行任务
requestIdleCallback(workLoop, { timeout: 1000 });
收集Effect List
- 遍历Fiber树将有副作用的fiber节点收集起来,形成一个单向链表
- 遍历完成后通过
commitWork
方法,将收集的副作用进行提交,修改真实dom - Effect List的顺序和fiber节点遍历的完成顺序一致
let container = document.getElementById('root');
let C1 = { type: 'div', key: 'C1', props: { id: 'C1', children: [] } };
let C2 = { type: 'div', key: 'C2', props: { id: 'C2', children: [] } };
let B1 = { type: 'div', key: 'B1', props: { id: 'B1', children: [C1, C2] } };
let B2 = { type: 'div', key: 'B2', props: { id: 'B2', children: [] } };
let A1 = { type: 'div', key: 'A1', props: { id: 'A1', children: [B1, B2] } };
let nextUnitOfWork = null;
let workInProgressRoot = null;
// 1. 浏览器空闲执行
function workLoop() {
let shouldYield = false;//是否要让出时间片或者说控制权
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);//执行完一个任务后
shouldYield = deadline.timeRemaining() < 1;//没有时间的话就要让出控制权
}
if (!nextUnitOfWork && workInProgressRoot) {
console.log('render阶段结束');
commitRoot();
}
//不管有没有任务,都请求再次调度 每一帧都要执行一次workLoop,检查看有没有要执行的任务
requestIdleCallback(workLoop, { timeout: 500 });
}
// 5. 提交变更
function commitRoot() {
let fiber = workInProgressRoot.firstEffect;
while (fiber) {
console.log(fiber.key); //C1 C2 B1 B2 A1
commitWork(fiber);
fiber = fiber.nextEffect;
}
workInProgressRoot = null;
}
// 6. 修改真实dom
function commitWork(currentFiber) {
currentFiber.return.stateNode.appendChild(currentFiber.stateNode);
}
// 2. 执行fiber工作
function performUnitOfWork(fiber) {
beginWork(fiber);
if (fiber.child) {
return fiber.child;
}
while (fiber) {
// 完成fiber工作
completeUnitOfWork(fiber);
if (fiber.sibling) {
return fiber.sibling;
}
fiber = fiber.return;
}
}
// 3. 开始工作
function beginWork(currentFiber) {
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;
// 创建子fiber
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;
} else {
previousFiber.sibling = childFiber;
}
previousFiber = childFiber;
});
}
//4. 完成fiber工作,收集effect List
function completeUnitOfWork(currentFiber) {
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;
}
}
}
console.log(container);
workInProgressRoot = {
key: 'ROOT',
stateNode: container,
props: { children: [A1] }
};
nextUnitOfWork = workInProgressRoot;//从RootFiber开始,到RootFiber结束
// 请求浏览器分配空闲时间片,执行任务
requestIdleCallback(workLoop, { timeout: 1000 });