React的fiber原理

在读完这篇文章之后,大家可以回到文章开头再捋一下以下几个关键词,将React的 Fiber架构原理彻底搞清楚。
关键词:

  • requestIdleCallback、IdleDeadline
  • Fiber:React的一个执行单元

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

React 16 之前,更新 Virtual DOM 的过程是采用Stack架构(循环加递归)实现的,任务一旦开始就无法中断;
如果 Virtual DOM 层级较深,主线程被长期占用,直到整颗虚拟DOM树对比更新完成之后主线程才能被释放,主线程才能执行其他任务,这会导致一些用户交互、动画无法立即执行;用户体感:页面卡顿。

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"))

jsx代码被转换成了真实的DOM添加到了页面中
在这里插入图片描述

Fiber 如何解决性能问题

  • 放弃递归调用,因为递归调用一旦开始,无法终止;采用循环来模拟递归,因为循环可以随时被中断;(由链表取代了树,将虚拟dom连接,使得组件渲染的工作分片,到时会主动让出渲染主线程)
  • 将大的渲染任务拆分成一个个小任务(小任务指的是 Fiber节点的构建);
  • 使用 requestIdleCallback 去利用浏览器的空闲时间去执行小任务,React 在执行一个任务单元后,查看是否有其他高优先级的任务;如果有,放弃占用线程,先执行优先级高的任务。

fiber 这种数据结构使得节点可以回溯到其父节点、只要保留下中断的节点索引,就可以恢复之前的工作进度;

这里,简单解释一下 requestIdleCallback

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

requestIdleCallback 的作用:

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

requestIdleCallback 详解

Fiber 原理分析

Fiber 是一种数据结构,支撑 Fiber 构建任务的运转;
Fiber其实就是JavaScript对象(这个对象中有child属性表示节点的子节点,有sibling属性表示节点的下一个兄弟节点,有return属性表示节点的父级节点

那么当一个 Fiber 任务执行完成后,通过 链表结构找到下一个要执行的任务单元。

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

Fiber 工作共分为两个阶段:render阶段和commit阶段;

  • render阶段:构建Fiber对象,构建链表,在链表中标记要执行的DOM操作,可中断;
  • commit阶段:根据构建好的链表进行DOM操作,不可中断;

Fiber 的主要工作流程:

  • ReactDOM.render() 引导 React 启动或调用 setState() 的时候开始创建或更新 Fiber 树。

  • 从根节点开始遍历 Fiber Node Tree, 并且构建 WokeInProgress Tree(reconciliation 阶段)。

    • 本阶段可以暂停、终止、和重启,会导致 react 相关生命周期重复执行。
    • React 会生成两棵树,一棵是代表当前状态的 current tree,一棵是待更新的 workInProgress tree。
    • 遍历 current tree,重用或更新 Fiber Node 到 workInProgress tree,workInProgress tree 完成后会替换 current tree。
    • 每更新一个节点,同时生成该节点对应的 Effect List。
    • 为每个节点创建更新任务。
  • 将创建的更新任务加入任务队列,等待调度。

    • 调度由 scheduler 模块完成,其核心职责是执行回调。
    • scheduler 模块实现了跨平台兼容的 requestIdleCallback。
    • 每处理完一个 Fiber Node 的更新,可以中断、挂起,或恢复。
  • 根据 Effect List 更新 DOM (commit 阶段)。

    • React 会遍历 Effect List 将所有变更一次性更新到 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()
  }
  requestIdleCallback(workLoop)
}

function commitRoot() {
  let currentFiber = workInProgressRoot.firstEffect
  while (currentFiber) {
    currentFiber.return.stateNode.appendChild(currentFiber.stateNode)
    currentFiber = currentFiber.nextEffect
  }
}

// 在浏览器空闲的时候执行任务
requestIdleCallback(workLoop)

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
    }
  }
}

为什么 vue 不需要 fiber 架构?

react 直到哪个组件触发了更新,但是不知道哪些子组件会受到影响。因此react 需要生成该组件下的所有虚拟DOM结构,与原本的虚拟DOM结构进行对比,找出变动的部分。

在 vue 中,一切影响页面内容的数据都应该是响应式的, vue通过拦截响应式数据的修改,知道哪些组件应该被修改,不需要遍历所有子树。vue的diff算法是对组件内部的diff,如果存在子组件,会判断子组件上与渲染相关的属性是否发生变化,无需变化的化则复用原本的DOM,不会处理子组件。

模板语法让vue能够进行更好地编译时分析,提高优化过程的效率,react缺少这部分,无法识别哪些是静态节点,哪些是动态节点。

  • 8
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值