一、fiber机理
1、浏览器卡顿原因
- 浏览器每秒刷新60次
- 如果浏览器每秒绘制的帧数大于60,那相当于浏览器刷新一次,页面就会发生变化,说明不卡顿
- 但是如果每秒绘制的帧数小于60,那有可能浏览器连续刷新两次,页面一直处于某一帧,说明页面卡顿了
js的执行,如果占用空闲时间太长(超过了空闲时间),那就导致一直不能绘制,导致每秒绘制的帧数减少,造成卡顿
2、如何解决
- 我们知道每秒绘制打于60帧,页面就不会卡顿;
- 也就每帧的时间只要低于有1s/60也就是16.66ms,页面就流畅;
- 由于每一帧只要保持16.66ms页面就达到流畅,所以浏览器执行帧的周期也是16.66ms, 看下图
浏览器每一帧开始都是优先执行顺序是:
(1)input事件
(2)定时器(js setTimeout/setInterval)
(3)滚动、媒体查询、resize
(4)requestAnimationFrame
(5)layout(布局)
(6)paint(绘制)
(7)idle(空闲时间才执行JS)
因为js是在idle阶段执行,如果在这个阶段js占用时间过长,那就会造成页面一直卡在这一帧,无法进行下一次渲染
react 15 之前
渲染是通过遍历虚拟dom,不断创建dom,并append到父节点,如果dom节点很多,且中间不能停止,将耗费较长时间,容易造成页面卡顿
/**
* react 15之前
* 问题:js是单线程, UI渲染和js执行是互斥的
* dom层级特别深
*/
export function render(element: any, parentDom: any) {
const dom = document.createElement(element.type);
Object.keys(element.props)
.filter((key: any) => key !== 'children')
.forEach((key: any) => {
dom[key] = element.props[key];
});
if (Array.isArray(element.props.children)) {
element.props.children.forEach((ele: any) => {
render(ele, dom);
});
}
parentDom.appendChild(dom);
}
3、fiber
fiber可以将分割执行单元,暂停执行,等到空闲再执行
, fiber利用requestIdleCallback
回调函数,只在浏览器空闲的时候去执行dom操作,如果超过空闲时间,就将控制权交回浏览器去进行渲染操作
另外fiber有自己的协调机制
- 在V16版本之前 协调机制 是 Stack reconciler, V16版本发布Fiber 架构后是 Fiber reconciler。
原来的React更新任务是采用递归形式,那么现在如果任务想中断, 在递归中是很难处理,
所以React改成了大循环模式,修改了生命周期也是因为任务可中断
。React Fiber 每个工作单元运行时有6种优先级:
- synchronous 与之前的Stack reconciler操作一样,同步执行
- task 在next tick之前执行
- animation 下一帧之前执行
- high 在不久的将来立即执行
- low 稍微延迟(100-200ms)执行也没关系
offscreen 下一次render时或scroll时才执行
4、
requestAnimationFrame
使用requestAnimationFrame:每帧都会执行,在页面不阻塞情况下时间间隔为16.66ms
// dom <div style="background:blue;width:0;height: 20px;"></div> <button>开始</button> <script> let div = document.querySelector('div'); let button = document.querySelector('button'); let startTime = 0; function progress(raftime) { console.log('raftime:',raftime); div.style.width = div.offsetWidth + 1 + 'px'; div.innerHTML = Math.floor(div.offsetWidth/3) + '%'; if (div.offsetWidth < 300) { console.log(Date.now() - startTime + 'ms'); // 打印progress被调用的时间点 requestAnimationFrame(progress); // 时间差16ms 可以传参DOMHighResTimeStamp表示执行的时间点=performance.now() // progress() // 时间差只有1ms } } button.onclick = function () { div.style.width = 0; startTime = Date.now(); // 浏览器会在每一帧渲染前执行这个方法 requestAnimationFrame(progress); // 宏任务 // progress() }; </script>
直接调用progress和requestAnimationFrame(progress)效果对比:
5、requestIdleCallback
requestIdleCallback:协调处理,只有在浏览器空闲的时候才会执行这样防止了js占用阻塞渲染
以下栗子:处理耗时的多个任务function sleep(duration) { let start = Date.now(); while (duration + start > Date.now()) { } } const works = [ () => { console.log('A1开始'); sleep(20); console.log('A1结束'); }, () => { console.log('A2开始'); sleep(20); console.log('A2结束'); }, () => { console.log('A3开始'); sleep(10); console.log('A3结束'); } ]; function performUnitOfWork() { works.shift()(); } function workLoop(deadline) { // 本帧的剩余时间 console.log(deadline); console.log(deadline.timeRemaining()); // 如果有剩余时间,或者已经过期了就执行deadline.didTimeout while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && works.length > 0) { performUnitOfWork(); } if (works.length > 0) { console.log(`剩余:${deadline.timeRemaining()}, 等待下次requestIdleCallback`); requestIdleCallback(workLoop, {timeout: 1000}); // 可以传参 如过过了1000还没执行,不管忙不忙都要执行 } } // 空闲时间执行 只有chrome支持 react 用MessageChannel模拟了requestIdleCallback 并且将回调延迟到绘制结束执行 requestIdleCallback(workLoop, {timeout: 1000});// 宏任务
6、fiber中的requestIdleCallback
由于requestIdleCallback回调函数只有chrome浏览器支持,react中试通过
MessageChannel
模拟requestIdleCallback的// messageChannel let channel = new MessageChannel(); let port1 = channel.port1; let port2 = channel.port2; let activeFrameTime = 1000 / 60; let frameDeadline; // 这一帧的截止时间 let pendingCallback; let timeRemaining = () => frameDeadline - performance.now(); // 会在绘制之后执行,此时可以看是否空闲 port2.onmessage = function (e) { console.log('port2收到port1的数据:', e.data); let currentTime = performance.now(); let didTimeout = currentTime > frameDeadline; if (didTimeout || timeRemaining() >0) { !!pendingCallback && pendingCallback({didTimeout,timeRemaining}) } }; window.requestIdleCallback = (callback, option) => { // performance.timing.navigationStart + performance.now() = Date.now() requestAnimationFrame((rafTime) => { // 每一帧的开始时间+16.6=每一帧的截止时间 frameDeadline = rafTime + activeFrameTime; pendingCallback = callback; // 发消息后相当于添加了一个宏任务 port1.postMessage('hello'); }); };
二、Fiber如何渲染
1、大框架
let container = document.getElementById('root'); // 根节点fiber let inProgressFiberRoot = { stateNode: container, props: {children: [element]} // child, // sibling, // return }; // fiber任务单元 let nextUnitOfWork = inProgressFiberRoot; function workLoop(deadline) { while (nextUnitOfWork && deadline.timeRemaining() > 0) { // 这一步构建fiber链条 nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } if (!nextUnitOfWork) { // 链条结束就开始渲染 commitRoot(); } } // 渲染 function commitRoot() { let currentFiber = inProgressFiberRoot.firstEffect; while (currentFiber) { if (currentFiber.effectTag === 'PLACEMENT') { currentFiber.return.stateNode.appendChild(currentFiber.stateNode); } currentFiber = currentFiber.nextEffect; } inProgressFiberRoot = null; } requestIdleCallback(workLoop);
2、performUnitOfWork如何构建链条
2.1 首先要创建真实dom,创建fiber树,如下图
- 目的就是根据虚拟dom创建真实的dom,并挂载到stateNode中
- 方式就是爸爸找儿子,儿子找弟弟,弟弟找叔叔的步骤,把所有节点遍历到
代码实现:
function beginWork(inProcessFiber) { console.log('干活了:', inProcessFiber.props.id); // 创建了真实dom并挂载到stateNode if (!inProcessFiber.stateNode) { inProcessFiber.stateNode = document.createElement(inProcessFiber.type); for (let key in inProcessFiber.props) { if (key !== 'children') { inProcessFiber.stateNode[key] = inProcessFiber.props[key]; } } } let previousFilber; Array.isArray(inProcessFiber.props.children) && inProcessFiber.props.children.forEach((child, index) => { let childFiber = { type: child.type, props: child.props, return: inProcessFiber, // 字节点的return当然是当前节点了 effectTag: 'PLACEMENT', // 表示这个节点将要被插入 nextEffect: null // 下一个有副作用的节点 }; if (index === 0) { // 先儿子 inProcessFiber.child = childFiber; } else { // 再兄弟 previousFilber.sibling = childFiber; } // 上一个节点,供下次用 previousFilber = childFiber; }); }
2.2 上一节只是创建了第一层子元素的dom,这一节要通过儿子找兄弟,兄弟找叔叔的方式继续遍历
fiber数是尊巡深度优先策略,先遍历儿子,然后弟弟,然后叔叔(叔叔是通过return到爸爸,让爸爸找兄弟)
function performUnitOfWork(inProcessFiber) {
// 1创建真实dom, 2创建fiber树
beginWork(inProcessFiber); //
if (inProcessFiber.child) {
// 有儿子找儿子
return inProcessFiber.child;
}
while (inProcessFiber) {
// 没有儿子,就完成了
completeInProcessFiber(inProcessFiber);
// 继续遍历弟弟
if (inProcessFiber.sibling) {
return inProcessFiber.sibling;
}
// 没有弟弟 就先找到父亲,让父亲帮助找叔叔
inProcessFiber = inProcessFiber.return;
}
}
2.3 当自己没儿子的时候,自己就完成了,就可以构建副作用链条了
function completeInProcessFiber(inProcessFiber) {
console.log('完成了:', inProcessFiber.props.id);
// 构建副作用链条 effectTag不为null的节点
let returnFiber = inProcessFiber.return;
if (returnFiber) {
if (returnFiber.firstEffect) {
returnFiber.firstEffect = inProcessFiber.firstEffect;
}
if (inProcessFiber.lastEffect) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = inProcessFiber.firstEffect;
}
returnFiber.lastEffect = inProcessFiber.lastEffect;
}
// 挂自己
if (inProcessFiber.effectTag) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = inProcessFiber;
} else {
returnFiber.firstEffect = inProcessFiber;
}
returnFiber.lastEffect = inProcessFiber;
}
}
}
参考
系列
重学react——slot
重学react——state和生命周期
重学react——redux
重学react——hooks以及原理
重学react——context/reducer
重学react——router
重学react——高阶组件
build your own react
React——fiber
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。