React - Fiber原理

浏览器渲染

屏幕刷新率(FPS)

  • 浏览器的正常绘制频率是60次/秒,小于这个值时,用户会感觉到卡顿
  • 绘制一次的称为一帧,平均每帧16.6ms

  • 每个帧的开头包括样式计算、布局和绘制
  • js的执行是单线程,js引擎和页面渲染引擎都占用主线程,GUI渲染和Javascript执行两者是互斥的
  • 如果某个js任务执行时间过长,浏览器会推迟渲染,每帧的绘制时间超过16.6ms,造成页面卡顿
  • requestAnimationFrame回调函数会在绘制之前执行
  • 在绘制之后,如果还有剩余时间,会执行 requestIdleCallback

image.png

requestIdleCallback

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

image.png

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 就会检查现在还剩多少时间,如果没有时间就将控制权让出去

image.png

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}

image.png

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

image.png

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节点遍历的完成顺序一致

image.png

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 });
点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

一篇文章带你了解JavaScript日期

日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用