https://mp.weixin.qq.com/s/uYd72aUb9wvUcjICFLREgg

Fiber之前的react

看一个例子

  1. // index.js
  2. import React from 'react';
  3. import ReactDOM from 'react-dom';
  4. import render from './vdom'
  5. let element= (
  6. <div id="A1">
  7. <div id="B1">
  8. <div id="C1"></div>
  9. <div id="C2"></div>
  10. </div>
  11. <div id="B2"></div>
  12. </div>
  13. )
  14. render(element, document.getElementById('root'))
  15. // ReactDOM.render(
  16. // element,
  17. // document.getElementById('root')
  18. // );

以上jsx被babel编译之后就是如下结构:

  1. // 经过babel编译后的虚拟dom
  2. let element = {
  3. "type": "div",
  4. "props":{
  5. "id": "A1",
  6. "children": [
  7. {
  8. "type": "div",
  9. "props": {
  10. "id": "B1",
  11. "children":[
  12. {
  13. "type": "div",
  14. "props": {
  15. "id": "C1"
  16. }
  17. },
  18. {
  19. "type": "div",
  20. "props": {
  21. "id": "C2"
  22. }
  23. }
  24. ]
  25. }
  26. },
  27. {
  28. "type": "div",
  29. "props": {
  30. "id": "B2"
  31. }
  32. }
  33. ]
  34. }
  35. }

虚拟dom经过render 方法调用将其渲染到页面上

  1. function render(element, parentDom){
  2. // 创建DOM 元素
  3. let dom = document.createElement(element.type); // div
  4. // 处理属性
  5. Object.keys(element.props).filter( key => key !== 'children').forEach(key => {
  6. dom[key] = element.props[key]; // dom.id = 'A1'
  7. })
  8. //递归处理子元素
  9. if(Array.isArray(element.props.children)){
  10. // 把子虚拟dom变成真实dom插入父dom里
  11. element.props.children.forEach(child => render(child, dom))
  12. }
  13. parentDom.appendChild(dom)
  14. }
  15. export default render;

image.png
可以看到,react 对子元素的处理是递归调用render 方法渲染,但是 js 是单线程,UI 渲染和 js 执行互斥的,如果节点特别多,层级特别深,递归调用就不会停,调用栈会特别深,会阻塞浏览器的主进程。用户操作得不到响应,造成页面卡顿。

image.png
每一帧处理完优先级较高的任务后,还有空闲时间就去执行用户代码,也就是 render 方法。

什么是Fiber

React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。
其中每个任务更新单元为React Element对应的Fiber节点。

  • fiber是一个调度算法,是为了解决如果有很多组件需要更新的时候,diff一口气执行完,造成主线程阻塞的问题,fiber会对diff策略进行更细粒度的控制,如果有涉及到用户输入这种高优先级的任务的时候,暂时停止diff,优先执行该任务,等到该任务结束后,再进行diff,而后commit渲染页面,
  • 通过调度策略,合理分配CPU资源,从而提高用户的响应速度。
  • 让自己的协调任务变成可被中断的,适时地让出CPU执行权,可以让浏览器响应用户交互。

    Fiber是一个执行单元

    以前的渲染是递归不能暂停,现在可以将一个渲染任务拆成一个个小任务单元,在浏览器空闲时间执行,
    image.png
    react 请求调度,每一帧浏览器首先处理优先级较高的任务,如果处理完还有空闲时间,就执行react 任务。

    Fiber 是一种数据结构

    React 目前做法是使用链表,每个虚拟节点内部表示为一个Fiber
    image.png
    每个父节点只指向它的大儿子,二儿子由他的兄弟节点指向,每个子节点都 return 到父节点

    requestAnimationFrame

    window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
    image.png
    image.png
    每一帧的时间大概都是 16ms。

requestIdleCallback

window.requestIdleCallback()方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

image.png

image.png

image.png

MessageChannel

image.png

image.png

Fiber执行阶段

每次渲染有两个阶段:Reconciliation(协调render 阶段)和Commit(提交阶段)

  • 协调阶段:可以认为是Diff阶段(但其实还包括创建fiber树、创建dom等),这个阶段可以被中断,这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等,这些变更React称之为副作用(Effect)
  • 提交阶段:将上一个阶段计算出来的需要处理的副作用一次性执行了。这个阶段必须同步执行,不能被打断。

    render 阶段

render阶段的结果是生成一个部分节点标记了side effects(把不能在 render 阶段完成的一些 work 称之为副作用)的fiber节点树,side effects描述了在下一个commit阶段需要完成的工作。
render阶段可以异步执行。 React可以根据可用时间来处理一个或多个fiber节点,然后停止已完成的工作,并让出调度权来处理某些事件。然后它从它停止的地方继续。但有时候,它可能需要丢弃完成的工作并再次从头。由于在render阶段执行的工作不会导致任何用户可见的更改(如DOM更新),因此这些暂停是不会有问题的。相反,在接下来的commit阶段始终是同步的,这是因为在此阶段执行的工作,将会生成用户可见的变化,例如, DOM更新,这就是React需要一次完成它们的原因。

  • 从顶点开始遍历
  • 如果有第一个儿子,先遍历第一个儿子
  • 如果没有第一个儿子,标志着此节点遍历完成
  • 如果有弟弟就遍历弟弟
  • 如果没有下一个弟弟,返回父节点标识完成父节点遍历,如果有叔叔节点遍历叔叔节点
  • 没有父节点遍历结束
  • 先儿子,后弟弟,再叔叔,辈分越小越优先
  • 什么时候一个节点遍历完成?没有子节点,或者所有子节点都遍历完成
  • 没父节点了就表示全部遍历完成了。

开始顺序
image.png

完成顺序
image.png

  1. import {element} from './vdom'
  2. let container = document.getElementById('root')
  3. const PLACEMENT = 'PLACEMENT';
  4. // 根
  5. let workingInProgressRoot = {
  6. stateNode: container, // 此fiber 对应的DOM节点
  7. props:{ // fiber 属性,
  8. children: [element] // 虚拟dom
  9. },
  10. // child, // 儿子
  11. // return, // 完成指向父亲
  12. // sibling // 兄弟
  13. };
  14. // 下一个工作单元
  15. let nextUnitOfWork = workingInProgressRoot;
  16. function workLoop(deadline){
  17. // 存在下一个执行单元就去执行,并返回下下一个执行单元
  18. while(nextUnitOfWork && deadline.timeRemaining()>0){
  19. nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  20. }
  21. // 如果没有下一个工作单元了就要提交了
  22. if(!nextUnitOfWork){
  23. commitRoot()
  24. }
  25. }
  26. // 提交插入
  27. function commitRoot(){
  28. let currentFiber = workingInProgressRoot.firstEffect; // C1
  29. while(currentFiber){
  30. console.log('commitRoot', commitRoot.props.id)
  31. if(currentFiber.effectTag === 'PLACEMENT'){
  32. currentFiber.return.stateNode.appendChild(currentFiber.stateNode)
  33. }
  34. currentFiber = currentFiber.nextEffect;
  35. }
  36. workingInProgressRoot = null;
  37. }
  38. /**
  39. * beginWork 1.创建此fiber 的真实DOM,通过虚拟dom创建fiber树结构
  40. * @param {*} workingInProgressFiber 正在执行的工作单元
  41. */
  42. function performUnitOfWork(workingInProgressFiber){
  43. beginWork(workingInProgressFiber)
  44. // 有儿子下一个工作单元就处理儿子
  45. if(workingInProgressFiber.child){
  46. return workingInProgressFiber.child
  47. }
  48. while(workingInProgressFiber){
  49. // 如果没有儿子,当前节点结完成了
  50. completeUnitOfWork(workingInProgressFiber);
  51. // 没有儿子下一个工作单元就处理弟弟
  52. if(workingInProgressFiber.sibling){
  53. return workingInProgressFiber.sibling
  54. }
  55. // 没有儿子没有弟弟就处理叔叔,首先指向父亲,循环找父亲的兄弟
  56. workingInProgressFiber = workingInProgressFiber.return
  57. }
  58. }
  59. /**
  60. * 开始:创建真实DOM,并没有挂载,创建fiber子树结构
  61. */
  62. function beginWork(workingInProgressFiber){
  63. console.log('workingInProgressFiber', workingInProgressFiber.props.id)
  64. if(!workingInProgressFiber.stateNode){ // 不存在真实dom就创建
  65. workingInProgressFiber.stateNode = document.createElement(workingInProgressFiber.type)
  66. for(let key in workingInProgressFiber.props){
  67. // 处理属性
  68. if(key !== 'children') workingInProgressFiber.stateNode[key] = workingInProgressFiber.props[key]
  69. }
  70. }
  71. // 创建子fiber
  72. let previousFiber;
  73. if(Array.isArray(workingInProgressFiber.props.children)){
  74. workingInProgressFiber.props.children.forEach((child, index) =>{
  75. let childFiber = {
  76. type: child.type, // DOM节点类型
  77. props: child.props,
  78. return: workingInProgressFiber, // 要指向的父亲
  79. effectTag: 'PLACEMENT', // 这个fiber对应的DOM节点需要被插入到页面中去, 父DOM中去
  80. nextEffect: null , // 下一个有副作用的节点
  81. }
  82. // 大儿子指向
  83. if(index === 0) {
  84. // child 属性 指向大儿子
  85. workingInProgressFiber.child = childFiber;
  86. }else{
  87. // sibling 属性指向其他
  88. previousFiber.sibling = childFiber
  89. }
  90. previousFiber = childFiber
  91. })
  92. }
  93. }
  94. /**
  95. * 完成:在 completeUnitOfWork 方法中构建 effect-list 链表,该 effect list 在下一个 commit 阶段非常重要
  96. * @param {*} workingInProgressFiber
  97. */
  98. function completeUnitOfWork(workingInProgressFiber){
  99. console.log('workingInProgressFiber', workingInProgressFiber.props.id)
  100. let returnFiber = workingInProgressFiber.return //最后一个workingInProgressFiber是C1,它的父亲是 B1
  101. if(returnFiber){
  102. if(!returnFiber.firstEffect){
  103. returnFiber.firstEffect = workingInProgressFiber.firstEffect
  104. }
  105. if(workingInProgressFiber.lastEffect){
  106. if(returnFiber.lastEffect){
  107. returnFiber.lastEffect.nextEffect = workingInProgressFiber.firstEffect
  108. }
  109. returnFiber.lastEffect = workingInProgressFiber.lastEffect
  110. }
  111. // 再把自己挂上去
  112. if(workingInProgressFiber.effectTag){
  113. if(returnFiber.lastEffect){
  114. returnFiber.lastEffect.nextEffect = workingInProgressFiber;
  115. }else{
  116. returnFiber.firstEffect = workingInProgressFiber;
  117. }
  118. returnFiber.lastEffect = workingInProgressFiber;
  119. }
  120. }
  121. }
  122. // 告诉浏览器在空闲的时候执行workloop
  123. requestIdleCallback(workLoop)

开始顺序和完成顺序
image.png

effects list 是render阶段运行的结果。render阶段的重点是确定需要插入,更新或删除哪些节点,以及哪些组件需要调用其生命周期方法,其最终生成了effects list,也正是在提交阶段迭代的节点集。

commit 阶段

commit 阶段是 React 更新真实 DOM 并调用 pre-commit phase 和 commit phase 生命周期方法的地方。与 render 阶段不同,commit 阶段的执行始终是同步的,它将依赖上一个 render 阶段构建的 effect list 链表来完成。
https://juejin.cn/post/6859528127010471949#heading-14