最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React Fiber 架构原理

    正文概述 掘金(毛小星)   2021-08-10   450

    Fiber 原理

    在 Fiber 出现之前 React 存在什么问题

    在 React 16 之前的版本对比更新 VirtualDOM 的过程是采用 Stack 架构实现的,也就是循环加递归。这种对比方式有一个问题,就是一旦任务开始进行就无法中断,如果应用中的组件数量庞大,Virtual DOM 的层级比较深,主线程被长期占用,直到整棵 VirtualDOM 树比对更新完成之后主线程才能被释放,主线程才能执行其他任务。这就会导致一些用户交互,动画等任务无法立即得到执行,页面就会产生卡顿, 非常的影响用户体验。 核心问题:递归无法中断,执行任务耗时长,JavaScript 是单线程的,和 Native GUI 互斥,比较 VirtualDOM 的过程中无法执行其他任务,导致任务延迟页面卡顿,用户体验差。

    Stack 架构的简单实现

    我们来实现一个简单的获取 jsx,然后将 jsx 转换成 DOM ,然后添加到页面中的过程

    const jsx = (
      <div id="a1">
        <div id="b1">
          <div id="c1"></div>
          <div id="c2"></div>
        </div>
        <div id="b2"></div>
      </div>
    )
    
    function render(vdom, container) {
      // 创建元素
      const element = document.createElement(vdom.type)
      // 为元素添加属性
      Object.keys(vdom.props)
        .filter(propName => propName !== "children") // 过滤 children 属性
        .forEach(propName => (element[propName] = vdom.props[propName]))
      // 递归创建子元素
      if (Array.isArray(vdom.props.children)) {
        vdom.props.children.forEach(child => render(child, element))
      }
      // 将元素添加到页面中
      container.appendChild(element)
    }
    
    render(jsx, document.getElementById("root"))
    

    React Fiber 架构原理

    可以看到,jsx 代码被转换成了真实的 DOM 添加到了页面中

    Fiber 如何解决性能问题的思路

    1. 在 Fiber 架构中 React 放弃了递归调用,采用循环来模拟递归,因为循环可以随时被中断。
    2. Fiber 将大的渲染任务拆分成一个个小任务
    3. React 使用 requestIdleCallback 去利用浏览器的空闲时间去执行小任务,React 在执行一个任务单元后,查看是否有其他高优先级的任务,如果有,放弃占用线程,先执行优先级高的任务。

    requestIdleCallback

    我们先来看看 requestIdleCallback 在 MDN 上的解释

    requestIdleCallback 的语法

    requestIdleCallback 接收两个参数,一个名为 IdleDeadline 的回调函数,一个是可选参数

    React Fiber 架构原理

    IdleDeadline 参数上有一个 timeRemaining() 的方法,返回一个时间 DOMHighResTimeStamp, 并且是浮点类型的数值,它用来表示当前闲置周期的预估剩余毫秒数。如果idle period已经结束,则它的值是0。你的回调函数(传给requestIdleCallback的函数)可以重复的访问这个属性用来判断当前线程的闲置时间是否可以在结束前执行更多的任务。

    requestIdleCallback 的作用

    浏览器的页面都是通过引擎一帧一帧绘制出来的,当每秒绘制的帧数达到 60 的时候,页面就是流畅的,玩过 fps 游戏的都知道,当这个帧数小于 60 的时候,人的肉眼就能感知出来卡顿。一秒 60 帧,每一帧分到的时间就是 1000/60 ≈ 16 ms,如果每一帧执行的时间小于 16 ms,就说明浏览器有空余时间,那么能不能通过浏览器的空余时间去处理任务呢,这样就不用一直等待主任务执行完了,requestIdleCallback 就是利用浏览器的空余时间去执行任务的。

    上面说了一堆,有的人可能已经懵了,你别废话,直接上代码给我一个示例就行了。诶,那我们就用下面的例子看看 requestIdleCallback 到底有什么神奇之处

    <style>
      #box {
        padding: 20px;
        background: palegoldenrod;
      }
    </style>
    
    <!-- body -->
    <div id="box"></div>
      <button id="btn1">执行计算任务</button>
      <button id="btn2">更改背景颜色</button>
    <script>
      const box = document.getElementById('box')
      const btn1 = document.getElementById('btn1')
      const btn2 = document.getElementById('btn2')
    
      let number = 999999
      let value = 0
    
      function calc() {
        while (number > 0) {
          value = Math.random() < 0.5 ? Math.random() : Math.random()
          console.log(value)
          number--
        }
      }
    
      btn1.onclick = function () {
        calc()
      }
    
      btn2.onclick = function () {
        box.style.background = 'green'
      }
    </script>
    

    上面的代码,我们通过一个很长的循环创建随机数,增加浏览器的计算量,你可以通过本地的 ide 试试这个 demo,就会发现当你点击 执行计算任务 后,再点击 更改背景颜色 按钮后,box 的颜色不会立马改变,而是会等待几秒后才发生改变(如果电脑性能差,可能会更慢)。这时因为 native GUI 和 v8引擎的渲染是互斥的,所以页面渲染会有一些延迟。

    <style>
      #box {
        padding: 20px;
        background: palegoldenrod;
      }
    </style>
    <!-- body -->
    <div id="box"></div>
    <button id="btn1">执行计算任务</button>
    <button id="btn2">更改背景颜色</button>
    <script>
      const box = document.getElementById('box')
      const btn1 = document.getElementById('btn1')
      const btn2 = document.getElementById('btn2')
    
      let number = 999999
      let value = 0
    
      function calc(deadline) {
        while (number > 0 && deadline.timeRemaining() > 0) {
          value = Math.random() < 0.5 ? Math.random() : Math.random()
          console.log(value)
          number--
        }
        requestIdleCallback(calc)
      }
    
      btn1.onclick = function () {
        requestIdleCallback(calc)
      }
    
      btn2.onclick = function () {
        box.style.background = 'green'
      }
    </script>
    

    上面的代码是使用了 requestIdleCallback 去优化的,运行之后,在点击 更改背景颜色 的按钮后,立马就能看到颜色的变化,这就是 requestIdleCallback 的作用。

    Fiber 原理分析

    下面我们通过实现一个简易版本的 Fiber 来了解一下 Fiber 的原理

    什么是 Fiber

    我们闲扯了这么多,那么 Fiber 到底是什么呢? Fiber 是 React 的一个执行单元,在 React 16 之后,React 将整个渲染任务拆分成了一个个的小任务进行处理,每一个小任务指的就是 Fiber 节点的构建。 拆分的小任务会在浏览器的空闲时间被执行,每个任务单元执行完成后,React 都会检查是否还有空余时间,如果有就交换主线程的控制权。

    React Fiber 架构原理

    Fiber 是一种数据结构,支撑 Fiber 构建任务的运转。当某一个 Fiber 任务执行完成后,怎样去找下一个要执行的 Fiber 任务呢?React 通过链表结构找到下一个要执行的任务单元。 Fiber 其实就是 JavaScript 对象,在这个对象中有 child 属性表示节点的子节点,有 sibling 属性表示节点的下一个兄弟节点,有 return 属性表示节点的父级节点。

    // 简易版 Fiber 对象
    type Fiber = {
      // 组件类型 div、span、组件构造函数
      type: any,
      // DOM 对象
      stateNode: any,  
      // 指向自己的父级 Fiber 对象
      return: Fiber | null,
      // 指向自己的第一个子级 Fiber 对象
      child: Fiber | null,
      // 指向自己的下一个兄弟 iber 对象
      sibling: Fiber | null,
    }
    

    实现一个简易版 Fiber

    Fiber 的工作分为两个阶段:render 阶段和 commit 阶段。

    • render 阶段:构建 Fiber 对象,构建链表,在链表中标记要执行的 DOM 操作 ,可中断。
    • commit 阶段:根据构建好的链表进行 DOM 操作,不可中断。
    const jsx = (
      <div id="a1">
        <div id="b1">
          <div id="c1"></div>
          <div id="c2"></div>
        </div>
        <div id="b2"></div>
      </div>
    )
    
    const container = document.getElementById("root")
    
    // 构建根元素的 Fiber 对象
    const workInProgressRoot = {
      stateNode: container,
      props: {
        children: [jsx]
      }
    }
    
    // 下一个要执行的任务
    let nextUnitOfWork = workInProgressRoot
    
    function workLoop(deadline) {
      // 1. 是否有空余时间
      // 2. 是否有要执行的任务
      while (nextUnitOfWork && deadline.timeRemaining() > 0) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
      }
      // 表示所有的任务都已经执行完成了
      if (!nextUnitOfWork) {
        // 进入到第二阶段 执行DOM
        commitRoot()
      }
    }
    
    function performUnitOfWork(workInProgressFiber) {
      // 1. 创建 DOM 对象并将它存储在 stateNode 属性
      // 2. 构建当前 Fiber 的子级 Fiber
      // 向下走的过程
      beginWork(workInProgressFiber)
      // 如果当前Fiber有子级
      if (workInProgressFiber.child) {
        // 返回子级 构建子级的子级
        return workInProgressFiber.child
      }
    
      while (workInProgressFiber) {
        // 向上走,构建链表
        completeUnitOfWork(workInProgressFiber)
    
        // 如果有同级
        if (workInProgressFiber.sibling) {
          // 返回同级 构建同级的子级
          return workInProgressFiber.sibling
        }
        // 更新父级
        workInProgressFiber = workInProgressFiber.return
      }
    }
    
    // 构建子集
    function beginWork(workInProgressFiber) {
      // 1. 创建 DOM 对象并将它存储在 stateNode 属性
      if (!workInProgressFiber.stateNode) {
        // 创建 DOM
        workInProgressFiber.stateNode = document.createElement(
          workInProgressFiber.type
        )
        // 为 DOM 添加属性
        for (let attr in workInProgressFiber.props) {
          if (attr !== "children") {
            workInProgressFiber.stateNode[attr] = workInProgressFiber.props[attr]
          }
        }
      }
      // 2. 构建当前 Fiber 的子级 Fiber
      if (Array.isArray(workInProgressFiber.props.children)) {
        let previousFiber = null
        workInProgressFiber.props.children.forEach((child, index) => {
          let childFiber = {
            type: child.type,
            props: child.props,
            effectTag: "PLACEMENT",
            return: workInProgressFiber
          }
          if (index === 0) {
            // 构建子集,只有第一个子元素是子集
            workInProgressFiber.child = childFiber
          } else {
            // 不是第一个,则构建子集的 兄弟级
            previousFiber.sibling = childFiber
          }
          previousFiber = childFiber
        })
      }
      // console.log(workInProgressFiber)
    }
    
    function completeUnitOfWork(workInProgressFiber) {
      // 获取当前 Fiber 的父级
      const returnFiber = workInProgressFiber.return
      // 父级是否存在
      if (returnFiber) {
        // 需要执行 DOM 操作的 Fiber
        if (workInProgressFiber.effectTag) {
          if (!returnFiber.lastEffect) {
            returnFiber.lastEffect = workInProgressFiber.lastEffect
          }
    
          if (!returnFiber.firstEffect) {
            returnFiber.firstEffect = workInProgressFiber.firstEffect
          }
    
          if (returnFiber.lastEffect) {
            returnFiber.lastEffect.nextEffect = workInProgressFiber
          } else {
            returnFiber.firstEffect = workInProgressFiber
          }
          returnFiber.lastEffect = workInProgressFiber
        }
      }
    }
    
    function commitRoot() {
      let currentFiber = workInProgressRoot.firstEffect
      while (currentFiber) {
        currentFiber.return.stateNode.appendChild(currentFiber.stateNode)
        currentFiber = currentFiber.nextEffect
      }
    }
    
    // 在浏览器空闲的时候执行任务
    requestIdleCallback(workLoop)
    

    构建 Fiber 链表

    上面的 completeUnitOfWork 函数就是用来构建 Fiber 链表的,只在在链表中的才会渲染

    1. 链表的构建顺序是怎么样的 ?

    链表的顺序是由 DOM 操作的顺序决定的,c1 是第一个要执行 DOM 操作的所以它是链的开始,A1 是最后一个被添加到 Root 中的元素,所以它是链的最后。

    React Fiber 架构原理

    1. 如何向链的尾部添加新元素?

    在链表结构中通过 nextEffect 存储链中的下一项。

    在构建链表的过程中,需要通过一个变量存储链表中的最新项,每次添加新项时都使用这个变量,每次操作完成后都需要更新它。这个变量在源码中叫做 lastEffect。

    lastEffect 是存储在当前 Fiber 对象的父级上的,当父级发生变化时,为避免链接顺序发生错乱,lastEffect 要先上移然后再追加nextEffect

    1. 将链表保存在什么位置?

    链表需要被保存在 Root 中,因为在进入到第二阶段时,也就是 commitRoot 方法中,是将 Root 提交到第二阶段的。 在源码中,Root Fiber 下有一个叫 firstEffect 的属性,用于存储链表。 在构建链表的遍历过程中,C1 开始,Root 是结尾,如何才能将 C1 存储到 Root 中呢? 其实是链头的不断上移做到的。

    React Fiber 架构原理

    总结

    其实 Fiber 的总体思路就是利用循环和链表代替递归去优化性能,Fiber 架构有两个阶段,render 阶段就是负责构架 Fiber 对象和链表,而 commit 阶段就是负责去构建 DOM,上面简单的实现了 Fiber ,只是为了理解方便 下面是实现 Fiber 的完整版 github.com/maoxiaoxing…


    起源地下载网 » React Fiber 架构原理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元