likes
comments
collection
share

React17系列(4)-初识Fiber与RIC

作者站长头像
站长
· 阅读数 13

前言

此章介绍下Fiber的一些基础知识,算是对Fiber的一个入门,所以不会涉及到太多源码。同时,建议在大家如果在阅读源码时遇到了瓶颈,觉得无法进行下去的时候,可以适当放弃,先去找一个大神的mini-react,先对react这个框架有个整体了解,再回过头来读源码也未尝不可。这里推荐下我最近阅读的一篇文章,逐步引导我们完成一个mini版的react,不过文章是纯英文的,日后我也会写一个系列记录下此文章:Build your own react

ReactElement (jsx)/ Fiber / DOM的关系

首先通过这三者的关系认识下Fiber这个陌生的单词

ReactElement

之前的文章有提到过,我们书写的jsx文件最终都被React.createElement(...)的方式,创建出来与之对应的ReactElement对象

Fiber

fiber就是一个对象,通过ReactElement对象进行创建,每个dom节点都有其对应的fiber对象,多个fiber就构成了一颗Fiber树,可以理解为这颗大的树记录了页面要渲染的所有的DOM节点。(注意我这里的大小写,并不是写错了,一颗Fiber树中包含了许多fiber对象)

DOM

dom节点就不解释了,文档对象模型

 <div>
   <h1>
     <p />
     <a />
   </h1>
   <h2 />
 </div>

如上这段代码对应的Fiber树如下图:(parent: 父节点; child: 子节点: sibling: 兄弟节点)

React17系列(4)-初识Fiber与RIC

为什么要引入Fiber

看了以上三者的关系,即使未接触过Fiber也会有个大概的理解。接下来介绍为什么react抛弃了之前的架构,采用了现在的Fiber架构。

React15

react15的构架分为两层:

  • Reconciler(协调器): 进行diff运算等,每当组件更新时,其将会做如下操作:

    • jsx转为VDOM
    • 此次VDOM与上次更新的VDOM对比
    • diff算法找出差异
  • Renderer(渲染器): 将组件渲染到页面上

在Reconciler阶段,中途是不能被打断的(Stack Reconciler),直至栈空。整个过程的js计算,会一直占据浏览器主线程,页面就没办法得到及时的更新,当组件过于庞大时,就有可能出现掉帧的现象。

Now

针对上面的问题,Fiber架构产生了,主要针对如下两个问题进行解决

  • reconciler阶段能否被打断暂停
  • dom diff算法时,如何在一帧内不阻塞浏览器的渲染

React源码中实现过于复杂,但万变不离其宗,接下来介绍一个重要的API,react官方也是使用此api的思想来解决了如上问题。

浏览器一帧内会干什么以及RequestIdleCallback的启示

浏览器一帧内的任务

React17系列(4)-初识Fiber与RIC

我们现在看到的网页都是浏览器一帧一帧绘制出来的,一秒绘制越多帧,画面就会越细腻。目前大多浏览器是60Hz(60帧/s,打游戏的各位可能会为帧数有些多的了解),每一帧绘制的时间大概也就是16.67ms左右,如上图可以知道在这一帧内的时间浏览器做了什么:

  • 处理输入事件
  • 执行事件回调
  • Begin frame 开始帧
  • 执行RAF(RequestAnimationFrame,不了解此api的可以自行查阅)
  • 计算样式、更新布局、绘制渲染
  • 执行RIC(RequestIdleCallback)

最后一步执行RIC并不是每一帧都会执行,只有当前帧内还有空余时间,并且还有未执行完的任务,才会执行此步骤。执行RIC事件不会中断,所以此事件时间不要过长,否则会影响下一帧的渲染。

RequestIdleCallback

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

为了让大家更好理解此函数怎么用,解决什么问题,我们直接看示例:

这个示例通过setInterval不断更新dom节点,在更新的同时会执行一些计算任务,通过常规执行计算任务以及使用RIC执行计算任务来帮助大家理解

DOM部分:
<div id="ui">0</div>
<div>执行<span id="test">0</span></div>
<button onclick="startNormalTask()">常规执行计算任务</button>
<button onclick="startRequestIdleCallbackTask()">
  在requestIdleCallback中执行计算任务
</button>
通过setInterval一直执行dom的更新操作
setInterval(() => {
  document.getElementById("ui").innerHTML =
    parseInt(document.getElementById("ui").innerHTML) + 1;
}, 100);
定义如下两个任务队列:
let task1 = [
    function () {
      for (var i = 0; i < 3000; i++) {console.log("")}
      console.log("第一个任务");
      document.getElementById("test").innerHTML = 1;
    },
    function () {
      for (var i = 0; i < 3000; i++) {console.log("")}
      console.log("第一个任务");
      document.getElementById("test").innerHTML = 1;
    },
    function () {
      for (var i = 0; i < 3000; i++) {console.log("")}
      console.log("第一个任务");
      document.getElementById("test").innerHTML = 1;
    }
];
// let task2 ...(同task1)
startNormalTask函数
function startNormalTask() {
  if (task1.length > 0) {
    work1(task1);
  }
}
function work1(tasks) {
    tasks.shift()();
    console.log("执行任务");
    if (task1.length > 0) {
    work1(task1);
  }
}

执行此函数会很明显的看到一直递增的数字会卡顿一下,也就是出现了掉帧现象

startRequestIdleCallbackTask函数
//将js计算任务放在requestIdleCallback中运算
function startRequestIdleCallbackTask() {
  requestIdleCallback(myNonEssentialWork);
}
function myNonEssentialWork(deadline) {
  // 如果帧内有富余的时间 并且还有未完成的任务
  while (
    deadline.timeRemaining() > 0 && task2.length > 0
  ) {
      work2(task2);
    }
  //开启下一个空闲时间的回调函数
  if (task2.length > 0) {
    requestIdleCallback(myNonEssentialWork);
  }
}
function work2(tasks) {
  tasks.shift()();
  console.log("执行任务");
}

这种时候,页面将不会出现一闪掉帧的现象,大家可以自行尝试

那么react团队为什么又polyfill这个方法呢:

  • 浏览器兼容不好
  • RIC的FPS只有20,也就是50ms刷新一次,影响用户的体验感

(至于React源码中具体如何实现,我研究清楚了再说)

fiber结构的类型定义以及如何关联起整个Fiber树

类型定义

src/react/node_modules/react-reconciler/src/ReactInternalTypes.js

这里只介绍几个关键的类型:

type Fiber = {
  // 指向父节点
  return: Fiber | null 
  // 指向第一个!!!孩子节点
  child: Fiber | null
  // 指向第一个孩子节点的兄弟节点
  sibling: Fiber | null
  // 指向上次生成的Fiber树 link to old Fiber
  alternate: Fiber | null
}

这里较难理解的是,正常情况我们会将父节点下所有的孩子节点都作为child,可是Fiber这里是仅仅将第一个孩子节点作为child,其余的孩子节点通过第一个子节点的sibling关联,这样子构成了整颗Fiber树。

Fiber树渲染的顺序

通过一个简易版示例来理解下:

  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }

React17系列(4)-初识Fiber与RIC

  • 首先可以理解为深度优先遍历

    • Root => 第一个子节点div => div的子节点h1 => h1的子节点p
  • p标签没有子节点,寻找其兄弟节点a

  • a标签既没有子节点,也没有兄弟节点,找到其父节点,渲染父节点的兄弟节点h2

  • 以此类推。。。

总结

此文章并没有涉及到过多源码,主要为了能对Fiber有一个浅显的认知,了解其是什么,为什么要采用Fiber的架构,其中心思想简单来说也不过是借鉴了RIC,了解下fiber的类型定义,渲染时按照什么顺序来进行渲染,更详细的知识会在此系列继续讲解。

转载自:https://juejin.cn/post/7136215878429278216
评论
请登录