react fiber
🗒️react fiber
2023-10-23|最后更新: 2024-1-23
type
status
date
slug
summary
tags
category
icon
password
Blocking
Blocked by
top
URL
Sub-item
Parent item

背景

react-fiber 产生的根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染。
React15及以前,Reconciler采用递归的方式创建虚拟DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,造成卡顿。
为了解决这个问题,react 16.18.0 版本引入 fiber 架构,实现异步可中断更新。先把 vdom 树转成 fiber 链表,然后再渲染 fiber。主要是解决之前由于直接递归遍历 vdom,不可中断,导致当 vdom 比较大的,频繁调用耗时 dom api 容易产生性能问题。

浏览器一帧的流程

一个task(宏任务) -- 队列中全部job(微任务) -- requestAnimationFrame -- 浏览器重排/重绘 -- requestIdleCallback

是什么

React Fiber 是 Facebook 花费两年余时间对 React 做出的一个重大改变与优化,是对 React 核心算法的一次重新实现React Fiber 在React 16 版本发布
react中,主要做了以下的操作:
  • 为每个增加了优先级,优先级高的任务可以中断低优先级的任务。然后再重新,注意是重新执行优先级低的任务
  • 增加了异步任务,调用requestIdleCallback api,浏览器空闲的时候执行
  • dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行
从架构角度来看,Fiber 是对 React核心算法(即调和过程)的重写
从编码角度来看,Fiber是 React内部所定义的一种数据结构,它是 Fiber树结构的节点单位,也就是 React 16 新架构下的虚拟DOM
一个 fiber就是一个 JavaScript对象,包含了元素的信息、该元素的更新操作队列、类型,其数据结构如下:

如何解决

Fiber把渲染更新过程拆分成多个子任务,每次只做一小部分,做完看是否还有剩余时间,如果有继续下一个任务;如果没有,挂起当前任务,将时间控制权交给主线程,等主线程不忙的时候在继续执行
即可以中断与恢复,恢复后也可以复用之前的中间状态,并给不同的任务赋予不同的优先级,其中每个任务更新单元为 React Element 对应的 Fiber节点
实现的上述方式的是requestIdleCallback方法
window.requestIdleCallback()方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
首先 React 中任务切割为多个步骤,分批完成。在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间再进行页面的渲染。等浏览器忙完之后有剩余时间,再继续之前 React 未完成的任务,是一种合作式调度。
该实现过程是基于 Fiber节点实现,作为静态的数据结构来说,每个 Fiber 节点对应一个 React element,保存了该组件的类型(函数组件/类组件/原生组件等等)、对应的 DOM 节点等信息。
作为动态的工作单元来说,每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作。
每个 Fiber 节点有个对应的 React element,多个 Fiber节点根据如下三个属性构建一颗树:
通过这些属性就能找到下一个执行目标
 
 
 

Fiber 的主要工作流程

  1. ReactDOM.render() 引导 React 启动或调用 setState() 的时候开始创建或更新 Fiber 树。
  1. 从根节点开始遍历 Fiber Node Tree, 并且构建 workInProgress Tree(reconciliation 阶段)。
      • 本阶段可以暂停、终止、和重启,会导致 react 相关生命周期重复执行。
      • React 会生成两棵树,一棵是代表当前状态的 current tree,一棵是待更新的 workInProgress tree。
      • 遍历 current tree,重用或更新 Fiber Node 到 workInProgress tree,workInProgress tree 完成后会替换 current tree。
      • 每更新一个节点,同时生成该节点对应的 Effect List。
      • 为每个节点创建更新任务。
  1. 将创建的更新任务加入任务队列,等待调度。
      • 调度由 scheduler 模块完成,其核心职责是执行回调。
      • scheduler 模块实现了跨平台兼容的 requestIdleCallback。
      • 每处理完一个 Fiber Node 的更新,可以中断、挂起,或恢复。
  1. 根据 Effect List 更新 DOM (commit 阶段)。
      • React 会遍历 Effect List 将所有变更一次性更新到 DOM 上。
      • 这一阶段的工作会导致用户可见的变化。因此该过程不可中断,必须一直执行直到更新完成。
       
       

      渲染流程

      FiberReact 的一次更新流程拆成了两个阶段:
    1. Render 阶段: 异步可中断的 diff 新老 DOM,找到差异后并不及时立刻更新。而是对该 Fiber 节点打上一个tag(Update/Placement/Delete)。
    2. Commit 阶段: 遍历存在 tag 的 Fiber ,根据 tag 的类型执行对应的 DOM 更新。
    3.  
      • beginWork阶段:顺着child属性向下遍历,找到变化地地方,打上标记
      • complateWork阶段:顺着return属性向上回归,将有标记的地方更新,此时就是更新workInProgress对应地Fiber tree
      • commitRoot阶段:将workInProgress对应的Fiber tree渲染到页面,同时完成上述指针的切换工作
       

      渲染流程

      将更新内容都放到updateQueue里,

      然后在updateQueue中,一起执行
      执行完后进行调度。

      递归到最上层

    4. 先从更新的节点,一直return到hostRootFiber
    5. 再return stateNode到FiberRootNode
    6. 创建workInProgress tree

      如果是初始化,则创建新的Fiber节点
      如果是更新,则更新Fiber节点,复用原本的Fiber节点的一些属性
      更新流程目的:
    7. 生成 wip fiberNode树
    8. 标记副作用的flags
    9.  

      react 状态更新流程

      • 触发更新
      • 创建Update对象插入对应fiber节点的updateQueue上
      • 并加入调度update
      • 从fiber到root
      • 调度更新
      • render阶段
      • commit阶段
      notion image

      effectTag(flags)

      ffectTag 实际上就是需要对节点需要执行的 DOM 操作(也可认为是副作用,即 sideEffect )
      render 阶段是在内存中进行的,render 阶段需要做的是为需要执行 DOM 操作的节点打上标记也就是 effectTag。 当工作结束后会通知 renderer 渲染器需要执行的 DOM 操作,要执行的 DOM 操作的具体类型就保存在 fiber.effectTag 中
      采用二进制表示 effectTag 的类型,可以方便的使用位操作为 fiber.effectTag 赋值多个 effect

      react 优先级

      react中有四种优先级:
    10. 事件优先级、
    11. 更新优先级(lane)、
    12. 任务优先级、
    13. 调度优先级(scheulder)
    14.  

      Lane事件优先级

      可以看到React的事件优先级的值还是使用的Lane的值,那为什么不直接使用Lane呢?我觉得可能是为了不与Lane机制耦合,后面事件优先级有什么变动的话,可以直接修改而不会影响到Lane。
       
      Scheduler优先级
      • 过期任务或者同步任务使用同步优先级
      • 用户交互产生的更新(比如点击事件)使用高优先级
      • 网络请求产生的更新使用一般优先级
      • Suspense使用低优先级
      React需要设计一套满足如下需要的优先级机制:
      • 可以表示优先级的不同
      • 可能同时存在几个同优先级更新,所以还得能表示的概念
      • 方便进行优先级相关计算
      为了满足如上需求,React设计了lane模型。接下来我们来看lane模型如何满足以上3个条件。
      notion image

      react 渲染两阶段

      FiberReact 的一次更新流程拆成了两个阶段:
    15. Render 阶段: 异步可中断的 diff 新老 DOM,找到差异后并不及时立刻更新。而是对该 Fiber 节点打上一个tag(Update/Placement/Delete)。
    16. Commit 阶段: 遍历存在 tag 的 Fiber ,根据 tag 的类型执行对应的 DOM 更新。
    17. render阶段:

      notion image

      beginWork工作流程(从上往下):

      beginWork的是一个从上至下,从左至右的深度遍历顺序。
      beginWork负责向下生成并连接Fiber Node,shengch新Fiber树,通过diff找到差异并记录更新
      工作:
      • 获取新的Fiber根节点
      • 与上一轮状态进行对比,通过diff算法找出差异
      • 生成所有子Fiber节点,构建新Fiber树
      • 检查是否需要更新(shouldComponentUpdate)
      • 在Fiber节点上记录必要的DOM操作
      • 处理所有子节点,冒泡回调相关钩子
       
      notion image
      notion image

      complateWork(从下往上):

      completeWork是一个从叶子节点由左至右,由下至上的遍历过程,比较类似于后续遍历的方式。主要处理 Fiber 节点到 DOM 节点的映射逻辑,并向上收集Fiber的副作用
      关键动作:
    18. 创建 DOM 节点
    19. 将 DOM 节点插入到其父节点对应的DOM节点里
    20. 为 DOM 节点更新属性
    21. completeWork(向上收集Fiber的副作用)负责真实地执行这些更新,如更改DOM和调用生命周期
      工作:
      • 如果还有未处理的子节点,就对其调用beginWork
      • 构建或更新HostComponent对应的DOM节点,
        • 构建过程中,会自下而上将子节点插入到当前节点。
        • 更新过程中,会计算DOM节点的属性,一旦属性需要更新,会为DOM节点对应的workInProgress节点标记Update的effectTag。
      • 自下而上收集effectList,最终收集到root上
      • 否则从父节点向上冒泡返回effectTag
      notion image
       
       

      commit阶段

      beforeMutation(执行DOM操作前)

    22. DOM节点删除以后的autoFous, blur的逻辑
    23. 针对类组件,调用getSnapshotBeforeUpdate,让我们可以在DOM变更前获取组件实例的信息;
    24. 针对函数组件,异步调度useEffect。
    25. 先向下遍历找到最深节点,找到标记flags的节点,再向上遍历

      mutation(执行DOM操作)

      在这里更新 DOM
    26. 针对HostComponent,进行相应的DOM操作
    27. 针对类组件,调用componentWillUnmount;
    28. 针对函数组件,执行useLayoutEffect的销毁函数。
    29. workInProgress 树切换到current树的时机是在mutation结束后,layout开始前。这

      layout(执行DOM操作后)

    30. 在DOM操作完成后,读取组件的状态,
    31. 针对类组件,调用生命周期componentDidMount和componentDidUpdate,调用setState的回调;
    32. 针对函数组件填充useEffect 的 effect执行数组,并调度useEffect
    33. notion image
      总结一下。
      commit 分成三个阶段:BeforeMuation、Muation 以及 Layout 阶段。
    34. BeforeMuation,没做太多事,主要是类组件实例调用 getSnapshotBeforeUpdate 生成快照对象保存起来;
    35. Muation:
      1. DOM 更新阶段 React会对组件的子树进行更新,包括创建、销毁和移动子节点等操作。
    36. Layout:
      1. 1. 调用类组件的 componentDidMount、componentDidUpdate、setState 的回调函数
      2. 调用函数组件 useLayoutEffect 的 create;
      3. 最后更新 ref。
       
       

      面试题

      Fiber

      jsx与fiber关系

      答:jsx 映射出 静态结构Fiber(保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息); 动态结构Fiber动态的工作单元来说,保存了本次更新中该组件改变的状态、优先级、要执行的工作(需要被删除/被插入页面中/被更新...)等; 静态 + 动态组成完整的Fiber结构

      Fiber是什么,它为什么能提高性能

      答:Fiber是一个js对象,能承载节点信息、优先级、updateQueue,同时它还是一个工作单元。
    37. Fiber双缓存可以在构建好wip Fiber树之后切换成current Fiber,内存中直接一次性切换,提高了性能
    38. Fiber的存在使异步可中断的更新成为了可能,作为工作单元,可以在时间片内执行工作,没时间了交还执行权给浏览器,下次时间片继续执行之前暂停之后返回的Fiber
    39. Hooks

      为什么hooks不能写在条件判断中

      hook会按顺序存储在链表中,如果写在条件判断中,就没法保持链表的顺序

      useEffect 原理是什么

    40. effect函数会被记录在Fiber节点的updateQueue中。
    41. 在commit阶段,会遍历updateQueue依次执行队列中的effect函数。
    42. effect函数执行后,会清空当前节点的副作用队列。
    43. 如果effect依赖项有变化,下次渲染就会再次插入新的函数入队。
    44. 清除函数会在即将卸载组件时调用,清理原先设置的副作用。
    45. 状态/生命周期

      setState是同步的还是异步的

      答:legacy模式下:命中batchedUpdates时是异步 未命中batchedUpdates时是同步的

      组件

    46. react元素$$typeof属性什么
        • 用来表示react元素的类型,是一个symbol类型
        • 非React元素对象不会有这个属性。
    47. react怎么区分Class组件和Function组件
      1. 答:Class组件prototype上有isReactComponent属性
    48. 函数组件和类组件的相同点和不同点
      1. 答:相同点:都可以接收props返回react元素
        不同点:
        编程思想:类组件需要创建实例,面向对象,函数组件不需要创建实例,接收输入,返回输出,函数式编程
        内存占用:类组建需要创建并保存实例,占用一定的内存
        值捕获特性:函数组件具有值捕获的特性

      开放题

      优化手段

      shouldComponentUpdate
      不可变数据结构
      列表key
      pureComponent:是 React 的一个高阶组件。如果 props 和 state 没有变化,则返回 false 跳过更新。
      react.memo
      useEffect、useCallback、useMemo、b
      ailoutOnAlreadyFinishedWork

      react为什么引入jsx

      jsx是js语法的扩展 可以很好的描述ui
      jsx是React.createElement的语法糖
      想实现什么目的:
    49. 声明式 代码结构简洁 可读性强 结构样式和事件可以实现高内聚 低耦合 、复用和组合 不需要引入新的概念和语法.
    50. 只写js, 虚拟dom快平台
    51. 有哪些可选方案:模版语法 vue ag引入了控制器 作用域 服务等概念
      jsx原理:babel抽象语法树 classic是老的转换 automatic新的转换
       
       

      合成事件

      类型
      原生事件
      合成事件
      命名方式
      全小写
      小驼峰
      事件处理函数
      字符串
      函数对象
      阻止默认行为
      返回false
      event.preventDefault()
      • React把事件委托到document上(v17是container节点上)
      • 先处理原生事件 冒泡到document上在处理react事件
      • React事件绑定发生在reconcile阶段 会在原生事件绑定前执行
      优势:
      • 进行了浏览器兼容。顶层事件代理,能保证冒泡一致性(混合使用会出现混乱)
      • 默认批量更新
      • 避免事件对象频繁创建和回收,react引入事件池,在事件池中获取和释放对象(react17中废弃) react17事件绑定在容器上了
       
    52. 我们写的事件是绑定在dom上么,如果不是绑定在哪里? 答:v16绑定在document上,v17绑定在container上
    53. 为什么不能用 return false 来阻止事件的默认行为?
      1. 说到底还是合成事件和原生事件触发时机不一样
    54. react怎么通过dom元素,找到与之对应的 fiber对象的?
    55. 通过internalInstanceKey对应
       

      编程题

      打印顺序是什么

       

      useLayout/componentDidMount和useEffect的区别是什么

      在commit阶段不同时机执行,useEffect在commit阶段结尾异步调用,useLayout/componentDidMount同步调用
      notion image
       
       

      参考

       
 
react与vue区别react diff算法
Loading...