• React源码解读之React Fiber


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

    • 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的时间内执行完下图的渲染任务,才能让用户感觉不到掉帧。

    img

    所以,如果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);
        }
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5

    题外话:

    有兴趣可以在控制台执行输出一下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结构

    image-47.png

    转化成类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 = () => [];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

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

    function walk(instance) {
      console.log(instance.name);
      let children = instance.render();
      children.forEach((child) => {
        walk(child);
      });
    }
    walk(a1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

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

    Fiber Reconciliation

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

    • child,指向该节点第一个子节点
    • sibling,指向该节点的下一个兄弟节点
    • return,指向该节点的父节点

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

    image-48-2.png

    构建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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

    下面是在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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    fiber带来的效果提升

    1. 可以通过看下重构前后的对比Demo,体会一下带来的体验提升
    2. 为后续React Concurrent模式做了基础

    Fiber流转过程

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

    react-fiber.png

    图示说明:

    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#L1635
    function workLoopConcurrent() {
      // Perform work until Scheduler asks us to yield
      while (workInProgress !== null && !shouldYield()) {
        performUnitOfWork(workInProgress);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    performUnitOfWork

    // https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L1642
    function 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;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    beginWork

    // https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberBeginWork.old.js#L3083
    function 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105

    completeUnitOfWork

    // https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L1670
    function 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#L645
    function 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;
          }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

  • 相关阅读:
    Python调用域控
    打车出行小程序APP定制开发代驾拼车专车
    信号量解决生产者消费者问题
    python爬虫
    vue组件间的通讯方式
    栅栏涂色题
    艾美捷曲妥珠单抗Trastuzumab参数和相关研究
    白领要预防肾结石的发生
    FFmpeg
    Linux网络编程2-多进程和多线程版本服务器
  • 原文地址:https://blog.csdn.net/weixin_59558923/article/details/127682222