react源码解析(二)时间管理大师fiber

825 阅读8分钟

React15的渲染和diff会递归对比vDom树,找出有改动(增删改)的节点,然后同步的更新他们,如果页面的节点数量非常庞大,React会一直占用浏览器资源,导致用户操作得不到响应,二则会掉帧,用户能感知到明显的卡顿。这个时候的React就像一个反应迟钝的直男,忙起来就冷落了女朋友😅,这篇我们来看下时间管理大师fiber是如何帮助React平衡事业(浏览器任务)和爱情(触发响应)的。

v2-5a029326c4774080fce8efd740c26330_r的副本.jpg

Fiber是什么

fiber并不是计算机术语中的新名词,他的中文翻译叫做纤程,与进程(Progress)、线程(Thread)、协程(Coroutine)同为执行过程。React Fiber可以理解为:React内部的一套更新机制。支持任务不同优先级,可中断与恢复,并且在恢复之后复用之前的保存状态。 其中每个任务(work)更新单元为React Element对应的Fiber节点。

React Fiber 与浏览器的核心交互流程

页面在渲染时是以帧为单位的,一般情况下设备的刷新频率时1s 60次,也就是每秒内绘制的帧数(fps)如果低于60,页面就会出现明显的卡顿,所以一帧绘制的时间不能超过16.7ms(这个时间很重要)。

image.png

相较于React15,React16中新增了Scheduler(调度器)。

首先React向浏览器申请调度,如果浏览器在一帧内(16.7ms)还有空余时间,就会去判断是否有待执行的任务,如果不存在就继续渲染页面,如果存在需要执行的任务,执行完成后就会判断是否还有时间,一直到浏览器任务完成,我们可以说 Fiber 是一种数据结构(堆栈帧),也可以说是一种解决可中断的调用任务的一种解决方案,它的特性就是时间分片(time slicing)和暂停(supense)

更新工作从递归变成了可以中断的循环过程。每次循环都会调用shouldYield判断当前是否有剩余时间。

// react源码 packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  // 听调度器的,他说执行再执行
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

Scheduler(调度器)

我们需要一种机制,来告诉我们浏览器是否有时间。requestIdleCallback 是实现这一机制的关键api,他能让用户的操作快速的响应,不阻塞用户的交互,而神奇的是,它并没有减少计算,只是将碎片化的时间运用到了极致,其实部分浏览器已经实现了这个API,但是因为浏览器兼容问题,React放弃使用,自己实现了功能更完备的requestIdleCallback polyfill,这就是Scheduler。

Scheduler 是独立于React的库

image.png

Fiber的结构

// react源码 packages/react-reconciler/src/ReactInternalTypes
{
    
    type: any, // 对于类组件,它指向构造函数;对于DOM元素,它指定HTML tag
    key: null | string, // 唯一标识符
    stateNode: any, // 保存对组件的类实例,DOM节点或与fiber节点关联的其他React元素类型的引用
    child: Fiber | null, // 大儿子
    sibling: Fiber | null, // 下一个兄弟
    return: Fiber | null, // 父节点
    tag: WorkTag, // 定义fiber操作的类型, 详见https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js
    nextEffect: Fiber | null, // 指向下一个节点的指针
    updateQueue: mixed, // 用于状态更新,回调函数,DOM更新的队列
    memoizedState: any, // 用于创建输出的fiber状态
    pendingProps: any, // 已从React元素中的新数据更新,并且需要应用于子组件或DOM元素的props
    memoizedProps: any, // 在前一次渲染期间用于创建输出的props
    // ……     
}

  • type & key

    fiber 的 type 和 key 与 React 元素的作用相同。fiber 的 type 描述了它对应的组件,对于复合组件,type 是函数或类组件本身。对于原生标签(div,span等),type 是一个字符串。随着 type 的不同,在 reconciliation 期间使用 key 来确定 fiber 是否可以重新使用。

  • stateNode

    stateNode 保存对组件的类实例,DOM节点或与 fiber 节点关联的其他 React 元素类型的引用。一般来说,可以认为这个属性用于保存与 fiber 相关的本地状态。

  • child & sibling & return

    child 属性指向此节点的第一个子节点(大儿子)。 sibling 属性指向此节点的下一个兄弟节点(大儿子指向二儿子、二儿子指向三儿子)。 return 属性指向此节点的父节点,即当前节点处理完毕后,应该向谁提交自己的成果。如果fiber 具有多个子 fiber,则每个子 fiber 的 return fiber 是 parent 。

这里我非常好奇,为什么父级指针叫做return而不是parent,参照卡颂大佬的 《React技术揭秘》 中的解释 子Fiber节点及其兄弟节点完成工作后会返回其父级节点,所以用return指代父级节点

Fiber的遍历流程

可以类比王朝的嫡长子继承制,制度是这样的,如果皇帝驾崩,会传位给大儿子,如果皇帝无后,那传位给兄弟,如果没有直系血统,那就要传位皇叔了,直到龙脉枯竭,王朝结束。

image.png

执行传序为 A1 B1 C1 C2 B2 C3 C4

我们根据这个规则手写下这套深度优先遍历的算发

// 看源码还能学算法 🐶
// 遍历函数
const performUnitOfWork = (Fiber) => {
  // 太子第一顺位
  if (Fiber.child) {
    return Fiber.child
  }
  while (Fiber) {
    // 二皇子第二顺位
    if (Fiber.sibling) {
      return Fiber.sibling
    }
    // 本支断绝,回去挑选皇叔
    Fiber = Fiber.return
  }
}

const workloop = (nextUnitOfWork) => {
  // 如果有待执行的执行单元则执行,返回下一个执行单元
  while (nextUnitOfWork) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
  }
  if (!nextUnitOfWork) {
    console.log('reconciliation阶段结束')
  }
}

workloop(rootFiber)

// rootFiber在后面手写的时候构造

本地调试源码

下面我们在实际项目中打印一个Fiber节点观察一下

我们还是使用create-react-app来创建一个项目,调试源码需要在项目里做一些配置

  • npm run eject(暴露出webpack配置)
  • 在src文件中添加react源码文件夹
  • 修改webpack中react的引用路径

这里就不详细展开配置的细节 如何在本地调试react源码,或者使用我配置好的项目,直接clone调试.

这样我们就可以在本地调试源码了

// packages/react-dom/src/ReactDOMLegacy.js
function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: Container,
  forceHydrate: boolean,
  callback: ?Function,
) {
  if (__DEV__) {
    topLevelUpdateWarnings(container);
    warnOnInvalidCallback(callback === undefined ? null : callback, 'render');
  }

  // TODO: Without `any` type, Flow says "Property cannot be accessed on any
  // member of intersection type." Whyyyyyy.
  let root: RootType = (container._reactRootContainer: any);
  let fiberRoot;
  if (!root) {
    // Initial mount 初次渲染
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }

    // Initial mount should not be batched.
    // 初次渲染是非批量更新,可以保证更新效率与用户体验
    unbatchedUpdates(() => {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Update
    updateContainer(children, fiberRoot, parentComponent, callback);
  }

  console.log('fiber-----current', fiberRoot.current); // 在这里我们打印一下fiber树
  return getPublicRootInstance(fiberRoot);
}
// 我们jsx是这样写的,div下有一个h1标签和一个a标签
const jsx = (
   <div className="content">
     我是
     <a href="www.baidu.com">明非</a>

   </div>
 )

看下打印结果

image.png

我们画一个 fiber的链表图图谱

image.png

我们来手写一个fiber

可以继续使用我们react源码解析(一) 手写render函数的代码

这里我们只关注fiber的type,props,stateNode,child,sibling,return属性。// fiber js对象。

// 我们在render函数中,构造一个fiber节点
let wipRoot = null;
function render(vnode, container) {
  wipRoot = {
    type: "div",
    props: {
      children: {...vnode},
    },
    stateNode: container,
  };
  nextUnitOfWOrk = wipRoot;
}

// 原生标签节点,接收work正在执行的fiber节点
function updateHostComponent(workInProgress) {
  const {type, props} = workInProgress;
  // 插入节点前要考虑是否已经存在节点
  if (!workInProgress.stateNode) {
    workInProgress.stateNode = createNode(workInProgress);
  }
  reconcileChildren(workInProgress, workInProgress.props.children);
  console.log("workInProgress", workInProgress); 
}

// 协调子节点
function reconcileChildren(workInProgress, children) {
  if (typeof children === "string" || typeof children === "number") {
    return;
  }
  // React16之后为 一个元素包裹,或者数组
  const newChildren = Array.isArray(children) ? children : [children];
  // 上一个fiber节点
  let previousNewFiber = null;
  for (let i = 0; i < newChildren.length; i++) {
    let child = newChildren[i];
    // 构造新的fiber节点
    let newFiber = {
      type: child.type,
      props: {...child.props},
      stateNode: null,
      child: null,
      sibling: null,
      return: workInProgress,
    };

    if (i === 0) {
      // 第一个子fiber,首先大儿子
      workInProgress.child = newFiber;
    } else {
      // 没有给兄弟
      previousNewFiber.sibling = newFiber;
    }

    // 记录上一个fiber
    previousNewFiber = newFiber;
  }
}

function performUnitOfWork(workInProgress) {
  // step1 执行任务
  // todo
  const {type} = workInProgress;
  if (typeof type === "string") {
    // 原生标签节点
    updateHostComponent(workInProgress);
  }

  // step2 并且返回下一个执行任务
  if (workInProgress.child) {
    return workInProgress.child;
  }

  let nextFiber = workInProgress;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.return;
  }
}

function workLoop(IdleDeadline) {
  while (nextUnitOfWOrk && IdleDeadline.timeRemaining() > 1) {
    // 执行任务, 并且返回下一个执行任务
    nextUnitOfWOrk = performUnitOfWork(nextUnitOfWOrk);
  }

  // 提交
  if (!nextUnitOfWOrk && wipRoot) {
    commitRoot();
  }
}
// 实现时间切片
requestIdleCallback(workLoop);

// 调度结束,渲染
function commitRoot() {
  commitWorker(wipRoot.child);
  wipRoot = null;
}

// 提交work执行结果
function commitWorker(workInProgress) {
  // 提交自己
  if (!workInProgress) {
    return;
  }

  let parentNodeFiber = workInProgress.return;
  let parentNode = parentNodeFiber.stateNode;

  if (workInProgress.stateNode) {
    parentNode.appendChild(workInProgress.stateNode);
  }

  // 提交子节点
  commitWorker(workInProgress.child);

  // 提交兄弟节点
  commitWorker(workInProgress.sibling);
}



上效果

image.png

代码已上传到git代码地址

思路,创建根节点,在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点,使用深度优先遍历构建出一棵dom树,

总结

fiber架构是React的基石,通过时间切片实现了将同步的更新变成了可中断的异步更新,这次我们了解了fiber和浏览器交互流程,实现了一个简单的fiber树,遍历流程根据深度优先遍历。但是这里没有覆盖到优先级机制,如何断点续传,和如何收集任务结果。之后的文章想分析下这些机制,看react是如何比对(diff)更新的。

参考链接

珠玉在前,只是想表达出自己的理解,推荐卡颂 《react技术揭秘》

react技术揭秘

完全理解React Fiber

浅谈 React Fiber

React Fiber 源码解析

走进 React Fiber 的世界