一、React Fiber 为什么呈现

在 React15 以前,React 的组件更新创立虚拟 DOM 和 Diff 的进程是同步并且不行中止的
假如需要更新组件树层级十分深的话,在 Diff 的进程会十分占用浏览器的线程,而浏览器履行 JS 的线程和烘托实在 DOM 的线程是互斥的具体能够看一下这篇文章),也便是同一时刻内,浏览器要么在履行 JS 的代码运算,要么在烘托页面,假如 JS 的代码运行时刻过长则会形成页面卡顿

二、React Fiber 是什么

依据以上原因 React 团队在 React16 之后就改写了整个架构,将本来数组结构的虚拟DOM,改成叫 Fiber 的一种数据结构,依据这种 Fiber 的数据结构能够完结由本来同步的不行中止的更新进程变成异步的可中止的更新

对于 React Fiber 是什么,从架构视点来看,官方的解释是:React Fiber 是对核心算法的一次从头完结
从编码视点来看,Fiber 是 React 内部定义的一种数据结构,它是 Fiber 树结构的节点单位,也便是 React 16 新架构下的虚拟 DOM。

React Fiber 架构的核心是”可中止”、”可恢复”、”优先级”

React Fiber 首要经过 FiberNode 的一些特点去保存组件相关的一些信息:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  /** 作为静态数据结构的特点 */
  this.tag = tag;  // 组件类型,如 Function/Class
  this.key = key;  // 仅有值,一般会在列表中运用
  this.elementType = null;
  this.type = null;  // 元素类型,字符串或类或函数,如"div"/Class/ComponentFn
  this.stateNode = null;  // 指向实在 DOM 目标
  // 靠以下特点连成一个树结构的数据,也便是 Fiber 链表
  this.return = null;  // 指向父级 Fiber 节点
  this.child = null;   // 指向子 Fiber 节点的第一个
  this.sibling = null; // 指向兄弟 Fiber 节点
  this.index = 0;
  this.ref = null;
  // 作为动态的作业单元的特点
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;
  this.mode = mode;
  this.effectTag = NoEffect;
  this.nextEffect = null;
  this.firstEffect = null;
  this.lastEffect = null;
  // 调度优先级相关
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
  // 指向该fiber在另一次更新时对应的fiber
  this.alternate = null;
}

比如:

function App() {
  return (
    <div className="app">
      <span>Hello</span>, World!
    </div>
  )
}

形成的 Fiber 树为:

说一下你了解的 React Fiber

三、React Fiber 做了什么

之前,递归烘托 vdom,然后 diff 下来做 patch(补丁) 的烘托,整个烘托和 diff 是递归进行的:

说一下你了解的 React Fiber

现在,是先把 vdom 转为 fiber(reconcile 谐和的进程),因为 fiber 是链表结构,能够打断,闲暇时调度(requestIdleCallback)就行,最后,全部转换完之后,再一次性 render,这个进程叫 commit 阶段

说一下你了解的 React Fiber

React16 的架构分为三层:

  • Scheduler(调度器):调度使命的优先级,高优先级的使命优先进入 Reconciler。
  • Reconciler(协调器):担任找出改变的组件。
  • Renderer(烘托器):担任将改变的组件烘托到页面上。

在 React16 版本中,首要做了以下的操作:

  • 做了时刻分片,拆分了多个使命,并且为每个使命增加了优先级,优先级高的使命能够中止低优先级的使命。然后再从头履行优先级低的使命。
  • 增加了异步使命,调用 requestIdleCallback api,在浏览器闲暇的时分履行
  • 运用了双缓存 Fiber 树,DOM diff树变成了链表,一个 DOM 对应两个 fiber,对应两个队列,这都是为找到被中止的使命,从头履行。

使命优先级

  • NoPriority:无优先级
  • ImmediatePriority:立即履行
  • UserBlockingPriority:用户堵塞优先级,不履行或许会导致用户交互堵塞
  • NormalPriority:普通优先级
  • LowPriority:低优先级
  • IdlePriority:闲暇优先级

requestIdleCallback

requestIdleCallback 是一个高档的调度办法,用于在浏览器闲暇时履行使命。
它会在浏览器的主事件循环闲暇时履行指定的回调函数,以避免堵塞用户交互和其他高优先级使命。
requestIdleCallback 的回调函数将提供一个 IdleDeadline 参数,能够用于判别剩余的闲暇时刻,并依据需要,履行使命的片段。

function performTask(deadline) {
  while (deadline.timeRemaining() > 0) {
    // 在闲暇时刻内履行你的操作
  }
  // 假如使命没有完结,持续恳求下一次闲暇回调
  requestIdleCallback(performTask);
}
// 启动闲暇回调
requestIdleCallback(performTask);
// 停止闲暇回调
var idleCallbackId = requestIdleCallback(performTask);
cancelIdleCallback(idleCallbackId);

双缓存 Fiber 树

在 React 中,最多会一起存在两棵 Fiber 树。当时屏幕上显示内容对应的 Fiber 树称为 current Fiber 树,正在内存中构建的 Fiber 树称为 workInProgress Fiber 树

current Fiber 树 中的 Fiber 节点被称为 current fiberworkInProgress Fiber 树 中的 Fiber 节点被称为 workInProgress fiber,它们之间经过 alternate 特点衔接。

说一下你了解的 React Fiber

workInProgress Fiber 树 构建完结交给 Renderer 烘托在页面上后,运用根节点的 current 指针指向 workInProgress Fiber 树,此刻 workInProgress Fiber 树 就变成 current Fiber 树

每次状况更新都会发生新的 workInProgress Fiber 树,经过 currentworkInProgress 的替换,完结 DOM 更新。

双缓存 Fiber 树在 mount 阶段的构建流程

function App() {
  const [num, setNum] = useState(0);
  return <p onClick={() => setNum(num + 1)}>{num}</p>;
}
ReactDOM.render(<App />, document.getElementById("root"));
  1. 首次履行 ReactDOM.render 会创立 fiberRootNode(源码中叫 fiberRoot)和 rootFiberfiberRootNode 是整个运用的根节点(只要一个),rootFiber<App /> 地点组件树的根节点。
    由所以首屏烘托,页面中还没有挂载任何 DOM,所以 fiberRootNode.current 指向的rootFiber没有任何子 Fiber节点(即current Fiber 树为空)。

说一下你了解的 React Fiber

  1. 接下来进入render阶段,依据组件回来的 JSX,在内存中顺次创立Fiber 节点并衔接在一起构建Fiber树,被称为workInProgress Fiber 树
    workInProgress Fiber 树 的创立能够复用current Fiber 树对应的节点数据。 鄙人面的 diff 算法中会阐明怎么判别是否可复用。

说一下你了解的 React Fiber

  1. 将现已构建完的workInProgress Fiber 树在 commit 阶段烘托到页面。使得workInProgress Fiber 树变为current Fiber 树

说一下你了解的 React Fiber

双缓存 Fiber 树在 update 阶段的更新流程

  1. 点击 p 标签触发状况更新,会开启一次新的 render 阶段,并构建一棵新的 workInProgress Fiber 树

说一下你了解的 React Fiber

  1. 将现已构建完的workInProgress Fiber 树在 commit 阶段烘托到页面。使得workInProgress Fiber 树变为current Fiber 树

说一下你了解的 React Fiber


React Diff 算法

一个DOM 节点在某一时刻最多会有4个节点和它相关:

  1. current Fiber,假如该DOM 节点现已在页面上,current Fiber代表该DOM 节点对应的 Fiber 节点。
  2. workInProgress Fiber,假如该DOM 节点将在本次更新中烘托到页面上,那么workInProgress Fiber代表该DOM 节点对应的Fiber 节点
  3. DOM 节点本身
  4. JSX 目标,即类组件或函数组件回来的结果,JSX 目标中包括描述DOM 节点的信息。

Diff 算法的本质是对比 1 和 4,生成 2。

为了下降算法复杂度,React 的 Diff 算法会预设三个约束:

  1. 只对同级元素进行 diff。假如一个 DOM 节点在前后两次更新时跨过了节点,那么 React 不会复用它。
  2. 两个不同类型的元素会发生不同的树。假如元素从div变成p,那么 React 会毁掉div及其后代节点,并新建p及其后代节点。
  3. 可经过key来表明哪些子元素在不同的烘托状况下能保持稳定。
    // 更新前
    <p key="hello">hello</p>
    <div key="world">world</div>
    // 更新后
    <div key="world">world</div>
    <p key="hello">hello</p>
    
    假如没有key,则符合 2 的限定。
    但假如我们运用key指明晰节点前后的对应关系后,React 知道key="hello"p标签在更新后还存在,那么DOM 节点可复用,仅仅需要交换一下顺序。

从同级的数量类型可将 Diff 分为两类:

  1. newChild 类型为 object、number、string,代表同级只要一个节点;
  2. newChild 类型为 Array,代表同级有多个节点。

单节点 Diff

说一下你了解的 React Fiber

说一下你了解的 React Fiber
怎么判别 DOM 节点是否可复用?

说一下你了解的 React Fiber

  • key 不同
    // 更新前
    <ul>
      <li key="one">one</li>
      <li key="two">two</li>
      <li key="three">two</li>
    </ul>
    // 更新后
    <ul>
      <p key="two">three</p>
    </ul>
    
    key="one"的 li 标签和 key="two"的 p 标签,仅表明遍历到的该 fiber不能被 p 复用,后边还有兄弟 fiber 没有遍历到,所以只需要符号删除该 fiber 节点。
  • key 相同,type 不同
    // 更新前
    <ul>
      <li key="two">two</li>
      <li key="three">two</li>
    </ul>
    // 更新后
    <ul>
      <p key="two">three</p>
    </ul>
    
    key="two"的 li 标签和 key="two"的 p 标签,type 不同,表明仅有的或许性不能复用了,那么后续的兄弟 fiber 也没机会了,所以都能够符号清楚。

多节点 diff

针对多节点的更新,会有以下三种状况:

  1. 节点更新
    // 更新前
    <ul>
      <li key="0" className="before">0</li>
      <li key="1">1</li>
    </ul>
    // 更新后 - 状况1 节点特点发生改变
    <ul>
      <li key="0" className="after">0</li>
      <li key="1">1</li>
    </ul>
    // 更新后 - 状况2 节点类型更新
    <ul>
      <li key="0">0</li>
      <li key="1">1</li>
    </ul>
    
  2. 节点新增或削减
    // 更新前
    <ul>
      <li key="0">0</li>
      <li key="1">1</li>
    </ul>
    // 更新后 - 状况1 新增节点
    <ul>
      <li key="0">0</li>
      <li key="1">1</li>
      <li key="2">2</li>
    </ul>
    // 更新后 - 状况2 删除节点
    <ul>
      <li key="1">1</li>
    </ul>
    
  3. 节点方位改变
    // 更新前
    <ul>
      <li key="0">0</li>
      <li key="1">1</li>
    </ul>
    // 更新后
    <ul>
      <li key="1">1</li>
      <li key="0">0</li>
    </ul>
    

多节点 diff更新进程如下
第一轮遍历:

  1. let i = 0,遍历newChildren,将newChildren[0]oldFiber比较,判别oldFiber是否可复用;
  2. 假如可复用,i++,持续比较newChildren[i]oldFiber.sibling,假如可复用,则持续遍历;
  3. 假如不行复用:
    • key 不同导致不行复用,马上跳出整个遍历,第一轮遍历完毕
    • key 相同而 type 不同导致不行复用,将oldFiber符号删除,持续遍历。
  4. 假如 newChildren 遍历完(i === newChildren.length - 1)或者oldFiber遍历完(oldFiber.sibling === null),跳出遍历,第一轮遍历完毕

从上述过程 3 中跳出,此刻newChildren没有遍历完,oldFiber也没有遍历完,如下:

// 更新前
<li key="0">0</li>
<li key="1">1</li>
<li key="2">2</li>
// 更新后
<li key="0">0</li>
<li key="2">1</li>
<li key="1">2</li>
`遍历到 key === 2 时,发现更新前后 key 不同,不行复用,跳出第一轮遍历;`
`此刻 oldFiber 剩余 key = 1、key = 2 未遍历,newChildren 剩余 key = 2、key = 1 未遍历`

从上述过程 4 中跳出,或许newChildren遍历完,或者oldFiber遍历完,或者两个都遍历完,如下:

// 更新前
<li key="0" className="a">0</li>
<li key="1" className="b">1</li>
// 更新后 - 状况1 newChildren 和 oldFiber 都遍历完
<li key="0" className="aa">0</li>
<li key="1" className="bb">1</li>
// 更新后 - 状况2 newChildren 未遍历完,oldFiber 遍历完
<li key="0" className="aa">0</li>
<li key="1" className="bb">1</li>
<li key="2" className="cc">2</li>
// 更新后 - 状况3 newChildren 遍历完,oldFiber 未遍历完
<li key="0" className="aa">0</li>

第二轮遍历:

  • newChildren 和 oldFiber 都遍历完:
    在第一轮遍历完毕后更新组件,Diff 完毕;

  • newChildren 没遍历完,oldFiber 遍历完:
    表明有新增节点,只需要将剩余的 newChildren 生成 workInProgress Fiber,并顺次符号新增(Placement);

  • newChildren 遍历完,oldFiber 没遍历完:
    表明有节点被删除,只需要遍历剩余的 oldFiber,顺次符号删除(Deletion);

  • newChildren 和 oldFiber 都没遍历完:
    表明有节点在本次更新中改变了方位。 声明一个变量:

    let lastPlacedIndex = 0;  // 表明最后一个可复用的节点在 oldFiber 中的方位索引
    

    说一下你了解的 React Fiber

    说一下你了解的 React Fiber

四、面试题

key 的效果

在 diff 算法中经过 key 和 type 判别 DOM 节点是否可复用。
key 的值不能是 index 或 random。


React diff 算法为什么不支持双指针?

尽管 JSX 目标的 newChildren 为数组类型,同层级的 fiber 节点之间是经过 sibling 指针链接成的单链表,不支持双指针遍历。
newChildren[0]fiber 比较,newChildren[1]fiber.sibling 比较。
所以,React diff 算法不支持双指针。

如有问题,欢迎纠正~

说一下你了解的 React Fiber