React源码解读之React Fiber

news2025/7/13 18:45:27

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

  • 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);
    }
});

题外话:

有兴趣可以在控制台执行输出一下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 = () => [];

正常情况,我们会使用像下面这种方式递归来遍历这棵"树",在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树结构,现在变成了这样

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;
}

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

下面是在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-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);
  }
}

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;
  }
}

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;
}

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;
      }
  }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/8589.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Go 语言搭建个人博客(qiucode.cn 重构篇 二)

1、MVC模式 MVC模式是一种 WEB 长期累积的总结,但这并不是唯一模式。 对于 MVC 模式,想必有过搭建 WEB 项目的开发者并无陌生。 服务器端负责将客户端发送过来的 HTTP 请求,进行处理(处理器),解析路由(Route),而后把 URL 映射到对应的控制器(Controller)。 MVC …

智慧公路解决方案-最新全套文件

智慧公路解决方案-最新全套文件一、建设背景二、思路架构三、建设方案1、智慧路产管理2、智慧基础设施3、智慧信息服务4、智慧交通管控5、智慧系统平台6、智慧辅助决策四、获取 - 智慧公路全套最新解决方案合集一、建设背景 交通出行主要面临的痛点是安全和拥堵,而…

算法与数据结构 - 散列表

文章目录引言一、散列表概述1.1 哈希函数1.2 散列表二、算法实战2.1 两数之和题目题解1. 暴力破解2. hash表结语点赞再看,养成习惯引言 某日,韩梅梅和李雷来到一家新开的网红图书馆借阅书籍。 韩梅梅: 李雷,快来帮我找下《数据结构从入门到放弃》 李雷看…

测试项目(MSTest)中涉及到读取App.config 操作(.net6)

文章目录环境问题排查过程查看Nuget包是否正确查看配置文件是否正确解决办法(手动)解决办法(自动)为什么是这样的呢?环境 VS2022MSTest项目.Net6版本 问题 在测试过程中发现读取App.config中的连接字符串是null&…

颜色杂项笔记

面向用户的HSV颜色模型的三个属性 面向用户的颜色模型HSV,有如下几个属性 Hue(色度、色调、色相):描述具体颜色,比如红、蓝、黄、绿等,可以理解为color的专业说法。 Saturation(饱和度&#xff…

红帽8使用nfs共享本地镜像

实验环境 FFF-server 192.168.80.100 SSS-client 192.168.80.254 实验前提关闭selinux和防火墙 第一步在开始之前我们可以先看一下nfs-server本地上的一个镜像挂载情况。如图1. 可以看到本地镜像已经被挂载上去。 第二步我们继续看一下server的yum仓库是否已经成功配置。如…

公布一小时下载量达10W:京东T5级架构师出品高并发核心编程手册

高并发有多重要? 高并发面试已经成了各大厂面试必问的题目,尤其是阿里、京东这样的大厂,面试的时候会往深了去问,可以说是你能回答出多少,你拿到大厂offer的概率就有多大! 因高并发的问题回答不上来的案例…

采购软件能否降低企业采购成本?如何实现的?

在如今的疫情影响下,降低成本一直是企业决策者的主要目标之一,在采购领域尤其如此。而很多企业在销售业绩下滑时,纷纷通过采购软件来降低采购成本从而提升利润,达到了不可思议的效果。那么采购软件能否降低企业采购成本&#xff1…

ELK日志实时分析

项目实训报告:ELK日志实时分析 任务目标 使用filebeat采集日志数据,通过kafka将数据传输给logstash进行过滤,最后输出到Elasticsearch绘制数据图表。 数据说明 实施步骤 将数据上传到家目录(/home/hadoop)&#…

CentOS7.5虚拟机扩展xfs文件系统

1.目标 虚拟机xfs文件系统挂载点根目录(也就是/dev/sda3)扩展空间 2.软件版本 Vmware WorkStation 16pro CentOS7.5 3.外部硬盘增加空间 硬盘空间只能增大,不能缩小,最大磁盘大小必须大于现在的size,否则扩展按钮置灰…

JSON 学习(FastJson和Jackson)

JSON 学习 文章目录JSON 学习1. Json数据格式1.1 Json 数据格式1.2 Json 对象格式1.3 数组对象相互嵌套格式1.3.1 数组中的元素是对象1.3.2 对象中的值是数组1.3.3 你中有我,我中有你2. 使用场景3. java里面操作json有哪些技术4. Fastjson4.1 FastJson的优点4.2 Fas…

Spring Security认证之基本认证

本文内容来自王松老师的《深入浅出Spring Security》,自己在学习的时候为了加深理解顺手抄录的,有时候还会写一些自己的想法。 快速入门 在Spring Boot项目中使用Spring Security非常方便,创建一个新的Spring Boot项目我们只要引入Web和Spri…

java项目:前后端分离SpringBoot+Vue+Element的校内跑腿平台

收藏点赞不迷路 关注作者有好处 项目编号:BS-XX-155 一,项目简介 近年来,随着国内都市化的发展,生产生活节奏变快、各种往来频繁。经济的高速发展催生出“懒人经济”。不想走出家门,饭菜可以送上门;不方便交水、电、…

Java中restTemplate的使用

原文链接 代码地址 本文介绍restTemplate基础用法。 Java中get和post的用法请参考:https://mp.weixin.qq.com/s/mC0D1nuCqIori5bWtLorWQ 1 提供get/post接口 1.1 Controller RestController RequestMapping("/homepage") public class MyController…

电脑如何清理重复文件,查找电脑重复文件的软件

在电脑上面,不论是我们可以保存的,还是自动缓存的,都会有大量的重复文件,可能我们自己并没有发现,占据着电脑大量的空间,长此以往下去,会让电脑变得卡顿,我们就需要来清理一下这些不…

低代码维格云甘特视图入门教程

功能简介 低代码维格云甘特图主要通过条状图来显示某些时间相关的活动(任务、阶段、项目等)随着时间进展的情况,以便管理者直观地查看活动进度,把控全局。又称为时间视图、横道图、条状图(Bar chart)。 低代码维格云甘特图适用场景 项目管理生产管理其他领域:建筑、IT软件…

嵌入式FreeRTOS学习八,xTaskCreate创建任务的细节以及恢复中断任务实现

一.创建任务函数xTaskCreate 任务也不是很复杂的东西,任务也就是一个函数xTaskCreate。简单得说,创建一个任务,你得提供它的执行函数,你得提供它的栈的大小,函数的执行空间,函数的优先级等重要的条件。因为…

IPWorks EDI Translator Delphi Edition

IPWorks EDI Translator Delphi Edition 一套轻量级可编程EDI解析和翻译组件。 IPWorks EDI转换器包括便于电子数据交换(EDI)解析、翻译和验证的软件组件。这些组件包括灵活的模式支持,使开发人员能够使用各种模式格式,从而更容易与现有EDI处理应用程序集…

Maven项目属性与版本管理

本次将介绍两个内容,分别是: 属性版本管理 1. 属性 1.1 问题分析 我们先来分析一下问题: 前面在父工程中的dependencyManagement标签中对项目中所使用的jar包版本进行了统一的管理,但是如果在标签中有如下的内容:…

React源码分析4-深度理解diff算法

上一章中 react 的 render 阶段,其中 begin 时会调用 reconcileChildren 函数, reconcileChildren 中做的事情就是 react 知名的 diff 过程,本章会对 diff 算法进行讲解。 diff 算法介绍 react 的每次更新,都会将新的 ReactElem…