浏览器渲染
屏幕刷新率(FPS)
- 浏览器的正常绘制频率是60次/秒,小于这个值时,用户会感觉到卡顿
- 绘制一次的称为一帧,平均每帧16.6ms
帧
- 每个帧的开头包括样式计算、布局和绘制
- js的执行是单线程,js引擎和页面渲染引擎都占用主线程,GUI渲染和Javascript执行两者是互斥的
- 如果某个js任务执行时间过长,浏览器会推迟渲染,每帧的绘制时间超过16.6ms,造成页面卡顿
- requestAnimationFrame回调函数会在绘制之前执行
- 在绘制之后,如果还有剩余时间,会执行 requestIdleCallback

requestIdleCallback
- requestIdleCallback使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
- 正常帧任务完成后没超过16.6ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务
requestIdleCallback在部分低版本浏览器中不支持,React内部是通过MessageChannel来实现的

1<script> 2 function sleep(d) { 3 for (var t = Date.now(); Date.now() - t <= d;); 4 } 5 const works = [ 6 () => { 7 console.log("第1个任务开始"); 8 sleep(20);//sleep(20); 9 console.log("第1个任务结束"); 10 }, 11 () => { 12 console.log("第2个任务开始"); 13 sleep(20);//sleep(20); 14 console.log("第2个任务结束"); 15 }, 16 () => { 17 console.log("第3个任务开始"); 18 sleep(20);//sleep(20); 19 console.log("第3个任务结束"); 20 }, 21 ]; 22 23 requestIdleCallback(workLoop, { timeout: 1000 }); 24 function workLoop(deadline) { 25 console.log('本帧剩余时间', parseInt(deadline.timeRemaining())); 26 while ((deadline.timeRemaining() > 1 || deadline.didTimeout) && works.length > 0) { 27 performUnitOfWork(); 28 } 29 30 if (works.length > 0) { 31 console.log(`只剩下${parseInt(deadline.timeRemaining())}ms,时间片到了等待下次空闲时间的调度`); 32 requestIdleCallback(workLoop); 33 } 34 } 35 function performUnitOfWork() { 36 works.shift()(); 37 } 38 </script>
Fiber 解决了什么问题
React的渲染分为协调和提交两个阶段,协调就是遍历dom树,进行domdiff,收集差异的阶段。提交就是修改真实dom,进行页面绘制的阶段。
Fiber之前的协调
- React 会递归遍历节点,比对VirtualDOM树,找出需要变动的节点,然后同步更新它们。这个过程 React 称为Reconcilation(协调)
- 在Reconcilation期间,React 会一直占用着浏览器资源,一则会导致用户触发的事件得不到响应, 二则会导致掉帧,用户可能会感觉到卡顿
Fiber的协调
- fiber使用链表数据结构,通过浏览器
requestIdleCallbackapi,让协调过程实现了可中断执行,分片完成协调任务 - 在浏览器空闲时去执行协调过程,遍历节点,收集变动的节点,避免了界面卡顿
什么是 React fiber
Fiber是一种调度策略
- 我们可以通过某些调度策略合理分配CPU资源,从而提高用户的响应速度
- 通过Fiber架构,让自己的协调过程变成可被中断。 适时地让出CPU执行权,让浏览器及时地响应用户的交互
Fiber是一个执行单元
- Fiber是一个执行单元,每次执行完一个执行单元, React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去

Fiber是一种数据结构
- React Fiber采用的链表的数据结构
- 内部会将虚拟Dom转化成Fiber节点,通过链表结构将整个dom树表示出来
- Fiber树采用孩子兄弟链表表示法,父节点的
child指向第一个子节点,子节点的sibling指向下一个兄弟节点,子节点的return指向父节点
1let fiberNode = { 2 tag, // fiber类型 Host/Root 3 type, // 虚拟dom的类型 div/span, 如果是类组件,这里是类名 4 props, // 虚拟dom的属性 5 stateNode, // 真实dom,如果是类组件,这里是类的实例 6 // 构建链表的三个指针 7 child, // 指向大儿子 8 sibling, // 指向二弟 9 return, // 指向父节点 10 // 构建Effect List的指针 11 firstEffect, // 第一个有副作用的子节点 12 nextEffect, 13 lastEffect, 14 effectTag, // 副作用类型 插入、更新、删除 15 16}

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

1let A1 = { type: 'div', key: 'A1' }; 2let B1 = { type: 'div', key: 'B1', return: A1 }; 3let B2 = { type: 'div', key: 'B2', return: A1 }; 4let C1 = { type: 'div', key: 'C1', return: B1 }; 5let C2 = { type: 'div', key: 'C2', return: B1 }; 6A1.child = B1; 7B1.sibling = B2; 8B1.child = C1; 9C1.sibling = C2; 10// 根fiber 11const rootFiber = A1; 12 13//下一个工作单元 14let nextUnitOfWork = null; 15//render工作循环 16function workLoop() { 17 while (nextUnitOfWork) { 18 //执行一个任务并返回下一个任务 19 nextUnitOfWork = performUnitOfWork(nextUnitOfWork); 20 } 21 //render阶段结束 22} 23function performUnitOfWork(fiber) { 24 beginWork(fiber); 25 if (fiber.child) {//如果子节点就返回第一个子节点 26 return fiber.child; 27 } 28 while (fiber) {//如果没有子节点说明当前节点已经完成了渲染工作 29 completeUnitOfWork(fiber);//可以结束此fiber的渲染了 30 if (fiber.sibling) {//如果它有弟弟就返回弟弟 31 return fiber.sibling; 32 } 33 fiber = fiber.return;//如果没有弟弟让爸爸完成,然后找叔叔 34 } 35} 36function beginWork(fiber) { 37 console.log('beginWork', fiber.key); 38 //fiber.stateNode = document.createElement(fiber.type); 39} 40function completeUnitOfWork(fiber) { 41 console.log('completeUnitOfWork', fiber.key); 42} 43nextUnitOfWork = rootFiber; 44 45// 请求浏览器分配空闲时间片,执行任务 46requestIdleCallback(workLoop, { timeout: 1000 });
收集Effect List
- 遍历Fiber树将有副作用的fiber节点收集起来,形成一个单向链表
- 遍历完成后通过
commitWork方法,将收集的副作用进行提交,修改真实dom - Effect List的顺序和fiber节点遍历的完成顺序一致

1let container = document.getElementById('root'); 2let C1 = { type: 'div', key: 'C1', props: { id: 'C1', children: [] } }; 3let C2 = { type: 'div', key: 'C2', props: { id: 'C2', children: [] } }; 4let B1 = { type: 'div', key: 'B1', props: { id: 'B1', children: [C1, C2] } }; 5let B2 = { type: 'div', key: 'B2', props: { id: 'B2', children: [] } }; 6let A1 = { type: 'div', key: 'A1', props: { id: 'A1', children: [B1, B2] } }; 7 8let nextUnitOfWork = null; 9let workInProgressRoot = null; 10 11// 1. 浏览器空闲执行 12function workLoop() { 13 let shouldYield = false;//是否要让出时间片或者说控制权 14 while (nextUnitOfWork && !shouldYield) { 15 nextUnitOfWork = performUnitOfWork(nextUnitOfWork);//执行完一个任务后 16 shouldYield = deadline.timeRemaining() < 1;//没有时间的话就要让出控制权 17 } 18 if (!nextUnitOfWork && workInProgressRoot) { 19 console.log('render阶段结束'); 20 commitRoot(); 21 } 22 23 //不管有没有任务,都请求再次调度 每一帧都要执行一次workLoop,检查看有没有要执行的任务 24 requestIdleCallback(workLoop, { timeout: 500 }); 25 26} 27 28// 5. 提交变更 29function commitRoot() { 30 let fiber = workInProgressRoot.firstEffect; 31 while (fiber) { 32 console.log(fiber.key); //C1 C2 B1 B2 A1 33 commitWork(fiber); 34 fiber = fiber.nextEffect; 35 } 36 workInProgressRoot = null; 37} 38 39// 6. 修改真实dom 40function commitWork(currentFiber) { 41 currentFiber.return.stateNode.appendChild(currentFiber.stateNode); 42} 43 44// 2. 执行fiber工作 45function performUnitOfWork(fiber) { 46 beginWork(fiber); 47 if (fiber.child) { 48 return fiber.child; 49 } 50 while (fiber) { 51 // 完成fiber工作 52 completeUnitOfWork(fiber); 53 if (fiber.sibling) { 54 return fiber.sibling; 55 } 56 fiber = fiber.return; 57 } 58} 59 60// 3. 开始工作 61function beginWork(currentFiber) { 62 if (!currentFiber.stateNode) { 63 currentFiber.stateNode = document.createElement(currentFiber.type);//创建真实DOM 64 for (let key in currentFiber.props) {//循环属性赋赋值给真实DOM 65 if (key !== 'children' && key !== 'key') 66 currentFiber.stateNode.setAttribute(key, currentFiber.props[key]); 67 } 68 } 69 let previousFiber; 70 // 创建子fiber 71 currentFiber.props.children.forEach((child, index) => { 72 let childFiber = { 73 tag: 'HOST', 74 type: child.type, 75 key: child.key, 76 props: child.props, 77 return: currentFiber, 78 effectTag: 'PLACEMENT', 79 nextEffect: null 80 } 81 if (index === 0) { 82 currentFiber.child = childFiber; 83 } else { 84 previousFiber.sibling = childFiber; 85 } 86 previousFiber = childFiber; 87 }); 88} 89 90//4. 完成fiber工作,收集effect List 91function completeUnitOfWork(currentFiber) { 92 const returnFiber = currentFiber.return; 93 if (returnFiber) { 94 if (!returnFiber.firstEffect) { 95 returnFiber.firstEffect = currentFiber.firstEffect; 96 } 97 if (currentFiber.lastEffect) { 98 if (returnFiber.lastEffect) { 99 returnFiber.lastEffect.nextEffect = currentFiber.firstEffect; 100 } 101 returnFiber.lastEffect = currentFiber.lastEffect; 102 } 103 104 if (currentFiber.effectTag) { 105 if (returnFiber.lastEffect) { 106 returnFiber.lastEffect.nextEffect = currentFiber; 107 } else { 108 returnFiber.firstEffect = currentFiber; 109 } 110 returnFiber.lastEffect = currentFiber; 111 } 112 } 113} 114console.log(container); 115 116workInProgressRoot = { 117 key: 'ROOT', 118 stateNode: container, 119 props: { children: [A1] } 120}; 121nextUnitOfWork = workInProgressRoot;//从RootFiber开始,到RootFiber结束 122 123// 请求浏览器分配空闲时间片,执行任务 124requestIdleCallback(workLoop, { timeout: 1000 });
