2. fiber

167 阅读4分钟

fiber

之前的render

  • react15 传统的渲染流程:先创建父元素,然后循环递归创建子元素(render方法),这是没有fiber之前的渲染流程
  • 这个方法的问题是:如果节点特别多,层级特别多,因为是递归调用,(栈调和),调用栈会特别深,递归的特点是不能暂停,一执行就执行到底。因为js是单线程,而且ui渲染和js执行是互斥的,如果递归需要的时间很长会阻塞渲染线程导致卡顿

图中的element是jsx,即虚拟节点树 image.png

  • 为了解决这个问题,react团队决定把整个架构利用fiber全部重写

  • 大多数设备的屏幕刷新率是60次/秒,每帧大约16.66ms。每帧的开头包含样式计算/布局和绘制
  • 当每秒绘制的帧数达到60,页面是流畅的,小于这个值时,用户会觉得卡顿
  • requestAnimationFrame会在每帧布局绘制之前执行

Fiber

  • fiber是一个执行单元。每次执行完一个执行单元,react会检查还剩多少时间,如果没有时间就将控制权交出去
  • fiber也是一个数据结构。
    • 要想把节点的渲染进行拆分,需要以一种结构来存储这些节点,也就是当执行完某个节点,需要暂停,怎么拿到下一个要执行的节点,这就需要一个明确的遍历顺序,永远知道下一个任务是谁。
    • react目前的做法是使用链表,每个虚拟节点内部表示成一个fiber
    • 链表通过child/sibling/return相互之间产生关联。节点之间:child指向第一个大儿子, sibling指向兄弟节点,每个节点的return指向自己的父亲节点
  • 通过fiber可以把渲染过程分成一个个小任务,可以暂停(每个节点都是一个任务单元)
  • 浏览器一般来说每帧会有10ms的空闲时间,可以利用这个空闲时间来进行渲染工作

requestAnimationFrame

  • requestAnimationFrame回调函数会在绘制之前执行
  • requestAnimationFrame(callback) 每次callback执行的时机都是浏览器刷新下一帧渲染周期的起点上
// 实现一个进度条

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div style="background-color:blue;width:0;height:20px"></div>
  <button>开始</button>
  <script>
    let div = document.querySelector('div');
    let button = document.querySelector('button');
    let startTime;
    function progress() {
      div.style.width = div.offsetWidth + 1 + 'px';
      div.innerHTML = div.offsetWidth + '%';
      if(div.offsetWidth < 100) {
        console.log(Date.now() - startTime + 'ms');
        startTime = Date.now();
        requestAnimationFrame(progress);
      }
    }
    button.onclick = function() {
      div.style.width = 0;
      startTime = Date.now();
      // 浏览器会在每一帧渲染前执行progress
      requestAnimationFrame(progress);
    }
  </script>
</body>
</html>

requestIdleCallback

  • 正常帧任务完成后没超过16ms,就会执行requestIdleCallback里注册的任务
  • requestAnimationFrame的回调会在每一帧确定执行,requestIdleCallback则不一定,属于低优先级任务

MessageChannel

  • 目前 requestIdleCallback 目前只有Chrome支持
  • 所以目前 React利用 MessageChannel模拟了requestIdleCallback,将回调延迟到绘制操作之后执行
  • MessageChannel API允许我们创建一个新的消息通道,并通过它的两个MessagePort属性发送数据
  • MessageChannel创建了一个通信的管道,这个管道有两个端口,每个端口都可以通过postMessage发送数据,而一个端口只要绑定了onmessage回调方法,就可以接收从另一个端口传过来的数据
  • MessageChannel是一个宏任务

fiber执行阶段

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

render阶段

  • render阶段会构建fiber树

  • 遍历规则

    • 先儿子,后弟弟,再叔叔,辈份越小越优先
    • 什么时候一个节点遍历完成? 没有子节点,或者所有子节点都遍历完成了
    • 没爹了就表示全部遍历完成了
  • fiber其实也是一个普通的js对象

  • workLoop 开启工作循环

  • performUnitOfWork 执行当前的工作单元并返回下一个工作单元

  • nextUnitOfWork 是一个工作单元(js对象)

    • stateNode 这个fiber对应的dom节点
    • props fiber的属性 image.png
  • 要将虚拟dom树 =〉 变成fiber树

  • 执行一个工作单元 performUnitOfWork

    • 先开始这个节点 beginWork(做了两件事情:1.创建真实dom,并没有挂载;2.创建fiber子树)
    • 查看有没有子节点 如果有直接返回; 如果没有 则当前节点就完成了 completeUnitOfWork;
    • 如果有弟弟 就返回弟弟节点; 如果没有,先指向父亲,又进入循环,父亲节点也完成,找父亲的兄弟节点

image.png

  • beginWork
    • 第一步: 创建真实dom
    • 第二步: 循环当前节点的虚拟dom数组创建子fiber树

image.png

image.png

  • completeUnitOfWork
    • 构建副作用链 effectList 只有那些有副作用的节点
    • 如何构建副作用链
      • 把当前节点上的副作用链向上归并到父节点身上
      • 看当前节点本身是否有副作用,如果有,挂到后面

image.png

image.png

commit阶段

  • 整个循环结束之后 得到最终的副作用链 提交根节点 commitRoot

image.png