写点什么

React 源码解读之 React Fiber

作者:flyzz177
  • 2022-12-19
    浙江
  • 本文字数:6761 字

    阅读完需:约 22 分钟

开始之前,先讲一下该文章能帮你解决哪些问题?


  • facebook 为什么要使用重构 React

  • React Fiber 是什么

  • React Fiber 的核心算法 - react 是如何中断重启任务的

  • react fiber 部分源码简化版

前言

该文章涉及的源码部分基于 React v17.0.2

why React Fiber

  • 浏览器渲染过程


从浏览器的运行机制谈起。大家都知道,浏览器是多进程多线程的,多进程包括主进程,渲染进程,插件进程,GPU 进程等,作为前端开发者,我们主要关注其中的渲染进程,这里是页面渲染,HTML 解析,css 解析,js 执行所在的地方。在渲染进程中包括多个线程,此次核心关注页面渲染的两个线程,GUI 线程和 JS 线程。


GUI 线程负责浏览器界面的渲染,包括解析 HTML,CSS,布局绘制等;js 线程包含我们通常编写的 js 代码的解析引擎,最有名的就是 google 的 V8。需要注意的一点是,js 引擎和 GUI 渲染是互斥的,因为 JS 可能会更改 HTML 或者 CSS 样式,如果同时执行会导致页面渲染混乱,所以当 JS 引擎执行时,GUI 渲染线程会被挂起,等 JS 引擎执行完立即执行。


  • GPU 渲染


我们通常看到的动画,视频本质上是通过一张张图片快速的闪过,欺骗人类的双眼,让人以为是连续的动画,每秒内包含的图片越多动画越流畅,正常 60 张图片可以让人眼感觉是流畅的动画,所以当前大部分设备的 FPS 是 60,即,每秒 60 帧。所以 Chrome 要在 16ms 的时间内执行完下图的渲染任务,才能让用户感觉不到掉帧。



所以,如果 JS 执行时间过长,基本上超过 10ms 之后,用户会感觉到明显的卡顿,很影响用户体验(下文中 js 执行都以 16ms 为分界点,不计算后续的渲染,实际的可执行时间肯定小于 16ms)。而 React 执行是要进行两棵树的 diff,虽然 React 根据 html 的特性对 diff 算法做了优化,但是如果两棵树比对的层级较深,依旧会远远超过 16ms。

React Fiber

基于此,那如何解决问题呢?在上图中,React 作为 js,所有的同步操作执行在最开始,在 React 执行完成后,后续的 html 解析,布局渲染等操作才会执行。最容易想到的就是,优化 JS 的执行速度,把 React 占用线程的时间缩短到 16ms 以内。在 React 执行中,最耗时的就是 diff 算法,React 针对 html 这种场景下做了优化,业界已经没有更好的算法可以缩短 diff 算法的时间,所以当树的层次很深时,执行时间依旧很长。


那还有什么办法呢,我们依旧可以看上图,在现代浏览器中,浏览器为了让开发者知道浏览器执行完当前帧所有的操作后,还有多长时间可以使用,提供了 requestIdleCallback 这么一个方法,据此我们可以知道当前还有多长时间可以执行。

requestIdleCallback
requestIdleCallback((deadline) => {    while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextComponent) {        nextComponent = performWork(nextComponent);    }});
复制代码


题外话:

有兴趣可以在控制台执行输出一下 requestIdleCallback 回调参数的 requestIdleCallback((deadline) ,在不同的网页上得到的时间可能不同。甚至可能会超过 16ms(在 React 官网就显示 49.9ms)因为 requestIdleCallback 的一些限制原因,React 源码中未使用 requestIdleCallback,而是自己实现了一套类似的机制。


使用此方法我们知道每帧的剩余时间之后,这样就可以在剩余时间内进行工作,如果当前帧时间不够,就把剩余的工作放到下一帧的 requestIdleCallback 中执行。这就是 React 所说的时间切片(time slicing)。


所以要使用此方法,需要把基于 js 内置栈调用的同步递归遍历的 diff 算法改为异步增量更新。按照 React 负责人的说法就是


如果你仅仅依赖 js 内置的调用栈,它会一直执行直到栈为空...,如果我们可以任意的中断并且手动的操作调用栈,不是更完美吗?这就是 React Fiber 的目的。Fiber 是针对 React Component 的栈的重新实现。你可以认为一个 Fiber 就是一个虚拟的栈中的一项任务。


说人话,就是原来树的递归是深度递归遍历,现在需要把递归算法重新实现,以便于我不依赖于栈的调用,可以对 react 组件一个一个节点的遍历,中途任意时间可以中断和从当前开始。

stack Reconciliation vs Fiber Reconciliation

stack Reconciliation

假如我们有如下一个 html 结构



转化成类 React 组件的 js 对象如下


const a1 = {name: 'a1'};const b1 = {name: 'b1'};const b2 = {name: 'b2'};const b3 = {name: 'b3'};const c1 = {name: 'c1'};const c2 = {name: 'c2'};const d1 = {name: 'd1'};const d2 = {name: 'd2'};
a1.render = () => [b1, b2, b3];b1.render = () => [];b2.render = () => [c1];b3.render = () => [c2];c1.render = () => [d1, d2];c2.render = () => [];d1.render = () => [];d2.render = () => [];
复制代码


正常情况,我们会使用像下面这种方式递归来遍历这棵"树",在 React 最早的版本就是基于此来递归遍历 dom 树


function walk(instance) {  console.log(instance.name);  let children = instance.render();  children.forEach((child) => {    walk(child);  });}walk(a1);
复制代码


可以看到,这种方式,是可以遍历完整棵树,可是它没办法做到我们之前所说的中断递归,如果你中途中断了递归这棵树,下次你要重新从根节点整个遍历。这显然是不行的,它只能不断递归遍历,直到 stack 调用栈为空。那 React Fiber 是如何中断重启任务呢?


答案是单链表树遍历算法。简单来说就是把原来树本身的嵌套结构,改为单链表形式的树。

Fiber Reconciliation

React 具体是如何使用链表遍历树呢?为了实现这种算法,首先先看下我们需要的数据结构


  • child,指向该节点第一个子节点

  • sibling,指向该节点的下一个兄弟节点

  • return,指向该节点的父节点


还是之前的 dom 树结构,现在变成了这样



构建 Fiber 树的过程就不描述了,我们直接看遍历算法(父节点优先,深度优先)


let root = fiber;let node = fiber;while (true) {  if (node.child) {    node = node.child;    continue;  }  if (node === root) {    return;  }  while (!node.sibling) {    if (!node.return || node.return === root) {      return;    }    node = node.return;  }  node = node.sibling;}
复制代码


可以看到,拿到根节点后,不断遍历子节点,直到最深节点,然后从最深的子节点开始遍历兄弟节点,如果没有兄弟节点就返回该节点父节点,如果有兄弟节点,把每个兄弟节点的子节点遍历完,直到最后一个子节点,然后返回父节点。这样不断遍历,直到返回根节点。


下面是在 React 源码中 Fiber 的数据对象。其实说到底,Fiber 就是一个对象。他相对于之前 React createElement 生成的 element 对象,多了一层数据结构来支撑上述的单链表遍历算法。

Fiber 数据结构

下面是 React 源码中的 Fiber 对象的属性,具体可以直接看注释。


相关参考视频讲解:进入学习


function FiberNode(  tag: WorkTag,  pendingProps: mixed,  key: null | string,  mode: TypeOfMode) {  // Instance  this.tag = tag; //Fiber标记是什么类型的Fiber/component,WorkTag 0-24  this.key = key; // 唯一标识  this.elementType = null;  this.type = null;  this.stateNode = null; //stateNode:class div
// Fiber 数据结构 this.return = null; // 父节点 this.child = null; // 第一个子节点 this.sibling = null; // 兄弟节点 this.index = 0; //
this.ref = null;
this.pendingProps = pendingProps; //newprops this.memoizedProps = null; // oldProps 上次的props // updateQueue数据结构: // { // baseState: fiber.memoizedState, // firstBaseUpdate: null, // lastBaseUpdate: null, // shared: { // pending: null, // interleaved: null, // lanes: NoLanes, // }, // effects: null, // }; this.updateQueue = null; // 批处理队列
this.memoizedState = null; //oldState this.dependencies = null;
this.mode = mode;
// Effects this.flags = NoFlags; // 标记该fiber变更方式 this.subtreeFlags = NoFlags; this.deletions = null; // 优先级调度 this.lanes = NoLanes; this.childLanes = NoLanes;
this.alternate = null; //work-in-progress current互为alternate}
复制代码

fiber 带来的效果提升

  1. 可以通过看下重构前后的对比 Demo,体会一下带来的体验提升

  2. 为后续 React Concurrent 模式做了基础

Fiber 流转过程

画了一个简单的流程图说明 Fiber 的流转流程。


图示说明:

react 在 performUnitOfWork 和 completeUnitOfWork 两个方法中,处理上述 Fiber 遍历算法的逻辑,在 beginwork 和 completeWork 中完成处理组件的逻辑。在 beginwork 中会处理 state 的更新,此阶段相应生命周期的调用,reconcile 的过程(给 Fiber 节点打上新增,删除,移动等标记的过程。在 completeWork 阶段,会把所有 flags 的标记,冒泡到父节点。以便于在 commit 阶段更新。


我记得 Dan Abramov 对 effect list 有过一个形象的比喻,可以写一下(大致意思是这样)


你可以把 react fiber 看做一棵圣诞树,effect list 就是这颗圣诞树上悬挂的装饰灯

React 源码 ---太长不看系列

下面是 React 中关于 Fiber 的一些核心源码---已删除了很多跟此次文章无关的代码,大家可以自行选择是否服用。


包含代码注释,及代码在 React 仓库中的所在位置。大家可以直接看代码注释,不作具体解读了。


// https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L1635function workLoopConcurrent() {  // Perform work until Scheduler asks us to yield  while (workInProgress !== null && !shouldYield()) {    performUnitOfWork(workInProgress);  }}
复制代码


performUnitOfWork


// https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L1642function performUnitOfWork(unitOfWork: Fiber): void {  const current = unitOfWork.alternate;  let next;  // 一直返回unitOfWork.child,不会处理sibling    next = beginWork(current, unitOfWork, subtreeRenderLanes);  unitOfWork.memoizedProps = unitOfWork.pendingProps;  // 该fiber需要做的处理完成,返回下一个待处理的fiber  if (next === null) {    // 到达该链路的最底层的叶子节点,在该函数中处理sibling节点    completeUnitOfWork(unitOfWork);  } else {    workInProgress = next;  }}
复制代码


beginWork


// https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberBeginWork.old.js#L3083function beginWork(  current: Fiber | null,  workInProgress: Fiber,  renderLanes: Lanes): Fiber | null {  let updateLanes = workInProgress.lanes;    // tag有很多,这里只保留了常用的FunctionComponent和ClassComponent,后续只看updateClassComponent  switch (workInProgress.tag) {    case FunctionComponent: {      const Component = workInProgress.type;      const unresolvedProps = workInProgress.pendingProps;      const resolvedProps =        workInProgress.elementType === Component          ? unresolvedProps          : resolveDefaultProps(Component, unresolvedProps);      return updateFunctionComponent(        current,        workInProgress,        Component,        resolvedProps,        renderLanes      );    }    case ClassComponent: {      const Component = workInProgress.type;      const unresolvedProps = workInProgress.pendingProps;      const resolvedProps =        workInProgress.elementType === Component          ? unresolvedProps          : resolveDefaultProps(Component, unresolvedProps);      // 返回值为workInProgress.child,可以在finishClassComponent中看到      return updateClassComponent(        current,        workInProgress,        Component,        resolvedProps,        renderLanes      );    }  }}function updateClassComponent(  current: Fiber | null,  workInProgress: Fiber,  Component: any,  nextProps: any,  renderLanes: Lanes) {
const instance = workInProgress.stateNode; let shouldUpdate; // 在此阶段处理更新生命周期和批处理的更新, if (instance === null) { if (current !== null) { // A class component without an instance only mounts if it suspended // inside a non-concurrent tree, in an inconsistent state. We want to // treat it like a new mount, even though an empty version of it already // committed. Disconnect the alternate pointers. current.alternate = null; workInProgress.alternate = null; // Since this is conceptually a new fiber, schedule a Placement effect workInProgress.flags |= Placement; } // In the initial pass we might need to construct the instance. constructClassInstance(workInProgress, Component, nextProps); mountClassInstance(workInProgress, Component, nextProps, renderLanes); shouldUpdate = true; } else if (current === null) { // In a resume, we'll already have an instance we can reuse. 复用之前未完成 shouldUpdate = resumeMountClassInstance( workInProgress, Component, nextProps, renderLanes ); } else { // 在此阶段处理生命周期和批处理的更新 shouldUpdate = updateClassInstance( current, workInProgress, Component, nextProps, renderLanes ); } const nextUnitOfWork = finishClassComponent( current, workInProgress, Component, shouldUpdate, hasContext, renderLanes ); return nextUnitOfWork;}
function finishClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, shouldUpdate: boolean, hasContext: boolean, renderLanes: Lanes) { const instance = workInProgress.stateNode; // Rerender ReactCurrentOwner.current = workInProgress; let nextChildren; nextChildren = instance.render(); //初始化或者执行dom diff //ReactChildFiber.old.js reconcileChildren(current, workInProgress, nextChildren, renderLanes); //child return workInProgress.child;}
复制代码


completeUnitOfWork


// https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L1670function completeUnitOfWork(unitOfWork: Fiber): void {  // Attempt to complete the current unit of work, then move to the next  // sibling. If there are no more siblings, return to the parent fiber.  let completedWork = unitOfWork;  do {    const current = completedWork.alternate;    const returnFiber = completedWork.return;
let next; // 返回值一直为null next = completeWork(current, completedWork, subtreeRenderLanes); const siblingFiber = completedWork.sibling; if (siblingFiber !== null) { // If there is more work to do in this returnFiber, do that next. workInProgress = siblingFiber; return; } // Otherwise, return to the parent completedWork = returnFiber; // Update the next thing we're working on in case something throws. workInProgress = completedWork; } while (completedWork !== null);}
// https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberCompleteWork.old.js#L645function completeWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes,): Fiber | null { const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) { case FunctionComponent: bubbleProperties(workInProgress); return null; case ClassComponent: { const Component = workInProgress.type; if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } bubbleProperties(workInProgress); return null; } }
复制代码


用户头像

flyzz177

关注

还未添加个人签名 2021-12-07 加入

还未添加个人简介

评论

发布
暂无评论
React源码解读之React Fiber_React_flyzz177_InfoQ写作社区