通俗易懂的react(react-native) fiber理论模型(结合源码)

最近在学习react源码。经过接近两周的时间,目前梳理出来了现阶段fiber的工作过程以及Function和ClassComponent的工作细节。其他方面暂未深入。写此篇文章总结一下学习的所得。

后面的内容中会根据需要附上部分源码,为了便于大家对比学习,先说明一下源码版本:

   "react": "16.13.1",
   "react-dom": "16.13.1",

先上结论:react的本质是一套复杂的任务管理系统。fiber是一套复杂的任务调度方案

为什么会这么说?在进入本文正文之前,我想先举个例子来做一个铺垫。

引例

在实际的开发过程中,大家可能会面临处理任务队列的场景。这个时候,可能会这么实现。

  • 任务队列(TaskQueue):可能是个数组或者链表
  • 创建新任务的方法(createTask):创建一个任务(Task),一般会是个对象,记录本次任务相关的数据
  • 创建新任务元素的方法(createTaskElement):创建一个任务元素(TaskElement),一般会是个对象,记录本次任务相关的数据,以及任务队列需要使用的相关数据
  • 任务入队的方法(inQueue): 往任务池内添加一个任务元素
  • 获取队列中第一个元素的方法(seek):复制任务队列中第一个元素
  • 处理任务的方法(handleTask):处理任务
  • 任务处理成功的回调(onTaskSuccess):顺利处理完任务,消耗掉一个任务
  • 任务处理失败的回调(onTaskFailed): 处理过程里出现异常,该任务没消耗掉。
  • 任务出队的方法(outQueue):从任务池里取出一个元素
  • 事件循环(loop): 通过循环不断地执行整个过程。
简单的任务管理

现在我们已经通过队列实现了一个最简单的任务管理了。

这个时候产品经理过来,新增一个需求:现在体验不太好,有的任务执行完以后,需要做点别的处理,现在想做点事情做不了。给每个任务添加执行成功后的回调,执行成功后,都需要执行一下对应的回调。

有回调的任务管理

现在实现了一个支持任务执行成功执行回调的需求了

产品再次新增需求:需要某些任务有更高的优先级。高优先级的任务优先执行。

这个时候我们再次调整一下整个流程

区分高低优先级的任务管理流程

现在每次取出来任务执行的时候,都会先按照优先级的关系,如果高优先级的队列里有任务,先处理高优先级的,没有高优先级的,才处理低优先级的任务。

整个任务管理方式已经不像一开始那么简单了。

产品经理又过来了,说现在体验还是不够好,现在如果正在执行低优先级任务,这个时候有高优先级任务进来,需要等待上一个任务执行完,下一个任务才是高优先级任务,能不能支持一旦有高优先级的任务进来,如果正在执行的任务优先级低于新产生的任务,直接打断当前执行,同时撤销对当前任务的改动,转而去处理新产生的任务。等没有高优先级的任务后,再回过头来执行低优先级任务。

我们再次调整整个流程

高优先级会打断低优先级任务的流程

现在整个流程已经有点复杂,但是整体还能接受。

这个时候产品经理,又过来了,再加一个需求: 我觉得现在的这套任务机制挺好用的,现在有其他几个模块,能不能也采用这套任务管理方案,但是需要所有模块产生的任务,统一调度执行。就是在同一个事件循环里处理不同模块产生的不同优先级的任务。

我们继续尝试着拓展我们的任务管理流程

多模块的任务管理流程

现在已经支持多模块了。

然而这个时候产品经理,又过来了,再加一个需求: 目前多模块使用过程中发现一些问题。各个模块之间的任务,有时候执行顺序有问题。能不能也给不同的模块给个优先级呢?然后列出来各个模块的优先级关系。

此次调整比较简单,按照预定的模块优先级关系,改变事件循环中模块的遍历顺序即可。流程图偷个懒,不画了 。基本和上图一样,就是遍历模块的时候,顺序规定一下。

目前整个任务管理已经到了多模块,多优先级,可打断,可插队的阶段。请再次回忆一下整个过程,是怎么从一个简单的任务队列演变到现在的。

我们总结一下整个例子:


流程总结

我们可以把整个工作过程划分为三个阶段:

  • 阶段一:创建并添加任务到任务池
  • 阶段二: 调度任务池中的所有任务,分为任务入队的调度和出队的调度。
  • 阶段三:(文中的流程图中没有体现该阶段)在整个事件循环过程中,不断地有任务添加进来,也不断的有任务被执行完毕,执行完毕的任务,在某些时间点会将任务结果阶段性的提交给业务方处理。

请牢记这几个阶段,后面会用到。

为什么要花费这么多的篇幅来举这么个例子。因为这个例子继续拓展下去,就会逐渐的演变成react的工作流程,整个react就是这么一套创建任务,执行任务的逻辑。react16的fiber是阶段二使用的所有任务的调度算法。

这个例子继续演变下去:

  • 模块分层:所有的模块划分层级关系,形成一个树结构。每个模块都会对应一个组件。
  • 任务分类:整体先分为同步任务、异步任务等,每一类任务的优先级再次细分,分为空闲时段执行的任务,用户交互产生的任务等。同时所有模块的所有任务,统一使用过期时间来区分执行的先后顺序。每次产生新任务的时候,都根据任务的类型,匹配不同的计算方法,计算出来任务的过期时间。然后在事件循环中,每次找出来所有模块所有任务中在当前时间点所有的过期任务,然后全部执行完毕。
  • 添加更多的回调:除了每个任务支持成功的回调以外,每个队列执行成功后和每个模块的任务在执行的不同阶段,也支持回调。这些回调最终会对应到setState的回调,以及组件的声明周期方法。
  • 任务处理成功的后续:单一模块上的任务执行完毕后,需要做一些后续的处理逻辑。react则最终会刷新页面。
  • 关于异常的处理: 在整个流程中,任何一个环节出错,应该怎么处理。对应react项目内的警告,错误提示灯

等等等等。

例子介绍到这里,这一阶段算是到了尾声,这里要结合例子中的概念引出来几个react及fiber中的几个重要概念:

  • workLoop: 直译,事件循环。对应我们的例子中的事件循环
  • unitOfWork(Fiber):直译,工作单元。是一个Fiber对象。对应我们的例子中的一个模块。该对象内有一个updateQueue字段,通过链表记录所有的任务。react中事件循环是以fiber作为任务的工作单元的。
  • update: 直译,更新。从词性上讲是一个名词。可以理解为是一个个的任务。对应案例里面的各个模块中队列里的任务元素。在Fiber中存放在updateQueue中。
  • ReactElement:虚拟Dom(vDom)。是一个不同于Fiber的对象。每一个vDom最终都会和一个Fiber一一对应。
  • expirationTime:直译,过期时间。用来区分每个任务的优先顺序的,哪个任务更早过期,哪个任务需要更早执行。

结合源码理解react和fiber

还记得一开始我抛出来的结论吧:
react的本质是一套复杂的任务管理系统。fiber是一套复杂的任务调度方案

为什么这么说?下面我们开始进入react的源码部分,我们结合源码去理解这个事情。

推荐大家先下载一份v16.13.1源码,便于根据文内的代码位置,查看相关的源码。
为了便于快速理解,代码内必要的地方我会逐行注释

我们先看一短最简单的代码

import React, { Component } from 'react'

export default class StateComponent extends Component {
    state = {
        num: 1,
    }

    onClick = () => {
        const { num } = this.state;
        this.setState({num: num + 1});
    }

    render() {
        const { num } = this.state;
        return (
            <div>
                <div>{num}</div>
                <button onClick={this.onClick}>按钮</button>
            </div>
        )
    }
}

一个很简单的状态组件

作为一个react的使用者,肯定都知道setState以后,会触发页面刷新,渲染出最新的state值。不知道你有没有好奇过当你调用setState后,发生了什么?

在往下阅读前,建议停下来几秒钟,自己先设想一下这个问题,如果是你要实现上述效果,这个方法内你会做什么。然后结合自己思考的结果,继续往下看,效果会更好。

react中调用setState发生了什么?

不废话,上源码

这里需要多个部分的源码:Component,setState

Component

packages/react/src/ReactBaseClasses.js line20

function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};

setState

packages/react/src/ReactBaseClasses.js line57

Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

看到了吗?component就是一个对象,对象原型上有setState这个方法。
我们调用的setState,实际上就是执行了

this.updater.enqueueSetState(this, partialState, callback, 'setState');

我当时看到这里的时候,心里一万个卧槽和一万零一个疑问,这就完了?这就是Component?生命周期方法呢?这就是setState?这个updater是啥?enqueueSetState又是啥?不知道此时的你,有没有和我一样的想法。

带着疑问,我们继续

updater

packages/react-reconciler/src/ReactFiberClassComponent.js line181

const classComponentUpdater = {
   // inst 是调用setState方法的那个组件对应的对象,就是虚拟Dom对象
   // payload 是调用setState方法的传入的第一个参数,一般是新的state对象或者是一个函数
   // setState执行成功后对应的回调函数
  enqueueSetState(inst, payload, callback) {
    // 根据vDom获取其对应的fiber对象,vDom和fiber是一一对应的
    const fiber = getInstance(inst);
    // 计算当前的时间
    const currentTime = requestCurrentTimeForUpdate();
    // 获取suspense对应的配置信息,暂时可忽略
    const suspenseConfig = requestCurrentSuspenseConfig();
    // 计算过期时间
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );
    // 创建update任务
    const update = createUpdate(expirationTime, suspenseConfig);
    // 设置任务的实际工作内容
    update.payload = payload;
    // 判断是否有回调函数
    if (callback !== undefined && callback !== null) {
       // 开发模式下,一些警告的提醒内容,可以略过
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState');
      }
      // 将回调也赋值给任务
      update.callback = callback;
    }
     // 任务入队
    enqueueUpdate(fiber, update);
     // 开始调度任务
    scheduleWork(fiber, expirationTime);
  },
}

看到这里,是不是觉得有那么点熟悉,和我刚刚讲的例子是不是有那么点的相似??

这里结合开头的例子来解释一下。setState的本质,就是创建了一个更新任务,并将其添加到任务队列中。对应案例的阶段一。

我们继续往下看,看看enqueueUpdate(fiber, update);做了什么,看名字就知道了,入队。我们看看细节

packages/react-reconsiler/src/ReactUpdateQueue.js line205

export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
   // 取出来调用setState方法的当前组件上对应的fiber对象中的更新队列的值
  const updateQueue = fiber.updateQueue;
   // 如果队列为空,直接返回,只发生在整个项目初始化的阶段
  if (updateQueue === null) {
    // Only occurs if the fiber has been unmounted.
    return;
  }
  // 目前我还没找到这里为什么要添加shared这层,有清楚的大佬,留言指点一下。thanks~~
  const sharedQueue = updateQueue.shared;
   // pending是指本次事件循环内,新增的任务,是一个环形链表。
   // pending指向当前链表的最后一个元素
  const pending = sharedQueue.pending;
  // 如果链表最后一个元素为空
  if (pending === null) {
    // 先让当前的update形成一个环,自己的next指向自己
    // This is the first update. Create a circular list.
    update.next = update;
  } else {
    // 如果最后一个元素不为空,就将新添加的update放在环形链表的最后一个位置。
    update.next = pending.next;
    pending.next = update;
  }
  // 重新让pending指针指向环形链表的最后一个元素
  sharedQueue.pending = update;

  if (__DEV__) {
    if (
      currentlyProcessingQueue === sharedQueue &&
      !didWarnUpdateInsideUpdate
    ) {
      console.error(
        'An update (setState, replaceState, or forceUpdate) was scheduled ' +
          'from inside an update function. Update functions should be pure, ' +
          'with zero side-effects. Consider using componentDidUpdate or a ' +
          'callback.',
      );
      didWarnUpdateInsideUpdate = true;
    }
  }
}

然后再看一下scheduleWork(fiber, expirationTime);做了什么,

packages/react-reconciler/src/ReactFiberWorkLoop.js line449

scheduleWorkscheduleUpdateOnFiber重命名。直译一下就是:安排工作 = 安排Fiber上的update。

export const scheduleWork = scheduleUpdateOnFiber;

packages/react-reconciler/src/ReactFiberWorkLoop.js line379

export function scheduleUpdateOnFiber(
  fiber: Fiber,
  expirationTime: ExpirationTime,
) {
  // 检查嵌套层级,可以理解为做一下相关的校验好检查工作
  checkForNestedUpdates();
  // DEV模式下使用的用来抛出一些警告信息,可以忽略
  warnAboutRenderPhaseUpdatesInDEV(fiber);
  // 这个方法内通过递归的方法,从当前节点开始,逐步对比并修改父Fiber的过期时间数据的。
  // 大家可以自行查看。
  // 基本逻辑是:如果当前任务的过期时间早于父节点记录的childExpirationTime,即所有子节点的已知任务的最早过期时间,就修改为当前的。
  // 意味着每个父节点都会记录自己所有子节点所有任务中,最早过期的那个任务对应的过期时间。
  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
  // 也是抛警告的代码
  if (root === null) {
    warnAboutUpdateOnUnmountedFiberInDEV(fiber);
    return;
  }
  // 检查任务是不是被打断,里面记录被哪个任务打断的
  checkForInterruption(fiber, expirationTime);
   // 会标记几个值,还没找到啥作用,不影响对本文的理解。有大佬请指点一下。
  recordScheduleUpdate();

  // TODO: computeExpirationForFiber also reads the priority. Pass the
  // priority as an argument to that function and this one.
   // 获取当前工作的优先级
  const priorityLevel = getCurrentPriorityLevel();
  // 如果是同步任务,就是需要立即执行的任务,坦白讲我也只梳理出来同步任务相关的内容。异步的还在啃
  if (expirationTime === Sync) {
    if (
      // Check if we're inside unbatchedUpdates
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      // Check if we're not already rendering
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      // Register pending interactions on the root to avoid losing traced interaction data.
      schedulePendingInteractions(root, expirationTime);

      // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
      // root inside of batchedUpdates should be synchronous, but layout updates
      // should be deferred until the end of the batch.
     // 这里进入任务执行阶段。
     // 每次都是从树的根节点作为入口,然后挨个往下找,找到要执行的任务,然后开始执行。
     // 然后继续找下一个任务,继续执行。
      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root);
      schedulePendingInteractions(root, expirationTime);
      if (executionContext === NoContext) {
        // Flush the synchronous work now, unless we're already working or inside
        // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
        // scheduleCallbackForFiber to preserve the ability to schedule a callback
        // without immediately flushing it. We only do this for user-initiated
        // updates, to preserve historical behavior of legacy mode.
        flushSyncCallbackQueue();
      }
    }
  } else {
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, expirationTime);
  }

  if (
    (executionContext & DiscreteEventContext) !== NoContext &&
    // Only updates at user-blocking priority or greater are considered
    // discrete, even inside a discrete event.
    (priorityLevel === UserBlockingPriority ||
      priorityLevel === ImmediatePriority)
  ) {
    // This is the result of a discrete event. Track the lowest priority
    // discrete update per root so we can flush them early, if needed.
    if (rootsWithPendingDiscreteUpdates === null) {
      rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
    } else {
      const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
      if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
        rootsWithPendingDiscreteUpdates.set(root, expirationTime);
      }
    }
  }
}

这里做的事情就是对应阶段二里面的开始执行任务。是不是和前面的例子更像了?

我们再去找一找,例子里面,执行完一个模块,再去执行下一个模块的逻辑:
(省略一些非关键代码)

packages/react-reconciler/src/ReactFiberWorkLoop.js line990

function performSyncWorkOnRoot(root) {
...
do {
      try {
        workLoopSync();
        break;
      } catch (thrownValue) {
        handleError(root, thrownValue);
      }
    } while (true);
...
}

packages/react-reconciler/src/ReactFiberWorkLoop.js line1459

function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

packages/react-reconciler/src/ReactFiberWorkLoop.js line1459

function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
  // The current, flushed, state of this fiber is the alternate. Ideally
  // nothing should rely on this, but relying on it here means that we don't
  // need an additional field on the work in progress.
  const current = unitOfWork.alternate;

  startWorkTimer(unitOfWork);
  setCurrentDebugFiberInDEV(unitOfWork);

  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork(current, unitOfWork, renderExpirationTime);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, renderExpirationTime);
  }

  resetCurrentDebugFiberInDEV();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    next = completeUnitOfWork(unitOfWork);
  }

  ReactCurrentOwner.current = null;
  return next;
}

进入了一个循环,每次都把当前fiber的子节点返回作为下一个unitOfWork。

介绍到这里,例子中的各个部分基本上都能对照源码解释一下了。可见整个react就是一个复杂的任务管理方案。不断地创建任务,调度任务,然后执行任务。

本文重点是在于介绍react的理论模型。源码部分仅作为补充对照的说明。暂时不对源码做深入分析。(因为我也没吃透。。。)

希望本文能对学习react源码提供一定的帮助。如果感受到了有用,欢迎点赞转发分享。鉴于水平有限,文中如果有错误的地方,也请大佬留言指正。共同进步。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • React Fiber 结构 介绍 React Fiber 是对React核心算法的重新实现,也是React团队花...
    eaTong阅读 6,920评论 0 4
  • 背景 前段时间准备前端招聘事项,复习前端React相关知识;复习React16新的生命周期:弃用了componen...
    萧强阅读 2,209评论 0 2
  • 在《JavaScript异步机制》这篇文章中我们说到,Js引擎是单线程的,它负责维护任务栈,并通过 Event L...
    tobAlier阅读 2,978评论 3 6
  • 真是DOM 的缺陷: js 操纵Dom 会 影响到整个渲染流水线 我们可以调用document.body.appe...
    Lyan_2ab3阅读 692评论 0 1
  • 发了一条朋友圈,说,感谢有那么多天使宝宝,然后,提醒谁看,54个孩子里,我选了3个家长,最后估摸着,应该是有9个(...
    Mimiaa阅读 322评论 1 0