React进阶之React核心源码解析(一)

React核心源码解析

cline + deeseek => AI工具,上手成本非常低,非常便宜

cursor 编辑器

带着问题学源码

  1. 为什么 react 会引入 fiber 架构
  2. 简述 fiber 节点的结构和作用
  3. fiber 架构 => 架构+流程
  4. diff 算法
  5. hooks 原理

学习方法论:由大到小 对比回答问题

react 特点

  1. 单向数据流
    能够快速响应用户操作
    公式:ui = render(data)

什么原因导致响应慢?

cpu卡顿,js执行导致画面卡顿

IO卡顿,网络问题等 延迟

CPU卡顿

浏览器一秒60hz,16.6ms刷新一次,超过就会有掉帧现象

就比如这个例子

javascript 复制代码
// index.js
import ReactDOM from "react-dom";

import App from "./App";

const rootElement = document.getElementById("root");

// ReactDOM.render(<App />, rootElement);
ReactDOM.createRoot(rootElement).render(<App />);

// APP.js
import "./styles.css";

export default function App() {
  const len = 3000;

  return (
    <ul>
      {Array(len)
        .fill(0)
        .map((_, i) => (
          <li>{i}</li>
        ))}
    </ul>
  );
}

React是怎么解决的?

时间分片:把更新过程碎片化 优先级 应用更新 后台预渲染

  • 同步 阻塞 渲染
  • 异步 非阻塞 用户优先级渲染

16.8后提出的 concurrent mode

javascript 复制代码
ReactDOM.createRoot(rootEl).render(<App />)

IO 卡顿

接口响应时间长,解决:

  • loading
  • suspence 兜底 fallback
  • error-boundary 错误兜底

新老 react 架构对比

v15 同步的,不可中断的

v16.8 异步,可中断

v18 ssr升级 流式 ssr stream,针对页面所有组件是分块,分局部输出,而不是等所有页面拼接组装好后一次性传输给客户端

页面上的不同接口,包裹成流式,然后传输给客户端,提升TTFB(首字节响应时间)

性能优化有需求,使用nextjs做提升

v15

  • Reconciler 协调器,diff 负责找出变化的组件
    • update 更新
    • component render jsx 渲染 => vdom
    • vdom diff
    • 找出变化的元素
    • 通知 renderer 渲染

通过递归的方式找出变化的组件

mount 阶段 => 调用 mountComponent

update 阶段 => 调用 updateComponent

递归更新子组件

=> 缺点:层级深,递归时间超过16ms

  • Renderer 渲染器,负责将变化的组件渲染到页面上
    • ReactDom.render
    • ReactNative.render

不可中断,中断则后续内容不执行

v16.8

多了一个 Scheduler 调度器

  • Scheduler 调度器 调度任务的优先级
  • Reconciler 协调器 负责找出变化的组件 递归=>可中断
  • Renderer 渲染器 是拿着Reconciler提供的标识 同步渲染

Scheduler 调度器

将大型任务分割成小任务,每一帧分配一定的时间执行小任务

  • 时间切片
  • 优先级调度
  1. 每个工作单元,对应一个fiber节点
  2. 时间分配
  3. 调度循环 维护任务队列
  4. 时间检查
  5. 暂停与恢复
  6. 利用浏览器API
  • requestAnimationFrame 用于在下一帧开始的时候执行回调函数
  • requestIdleCallBack 浏览器空闲的时候执行回调函数 setTimeout 模拟了

Reconciler 协调器

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

/** @noinline */
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

React fiber

原理

react 内部实现的数据结构,支持状态更新,可中断可恢复,恢复后可以复用之前的中间状态

  • 架构:v15 stack reconciler,v16 fiber reconciler
  • 数据结构:每个 fiber 节点对应 react element,多个组件类型,dom节点各种属性数据
  • 动态的工作单元,改变的状态,要执行的工作
javascript 复制代码
function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance,静态节点的数据结构属性
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // Fiber,用来链接其他fiber节点形成的fiber树
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  // 作为动态的工作单元的属性
  this.pendingProps = pendingProps; //即将应用到组件上新属性 相当于是新值newValue 
  this.memoizedProps = null; // 上次渲染时使用的属性值 相当于是旧值oldValue
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  // 记录变化的节点 effectlist=>render
  this.effectTag = NoEffect;
  this.subtreeTag = NoSubtreeEffect;
  this.deletions = null;
  this.nextEffect = null;

  // effectslist链 = firstEffect -> nextEffect -> nextEffect -> lastEffect => 交给render => 渲染
  this.firstEffect = null;
  this.lastEffect = null;

  // 作为调度优先级的属性
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 指向该fiber在另一次更新时对应的fiber
  this.alternate = null;

  if (enableProfilerTimer) {
    // Note: The following is done to avoid a v8 performance cliff.
    //
    // Initializing the fields below to smis and later updating them with
    // double values will cause Fibers to end up having separate shapes.
    // This behavior/bug has something to do with Object.preventExtension().
    // Fortunately this only impacts DEV builds.
    // Unfortunately it makes React unusably slow for some applications.
    // To work around this, initialize the fields below with doubles.
    //
    // Learn more about this here:
    // https://github.com/facebook/react/issues/14365
    // https://bugs.chromium.org/p/v8/issues/detail?id=8538
    this.actualDuration = Number.NaN;
    this.actualStartTime = Number.NaN;
    this.selfBaseDuration = Number.NaN;
    this.treeBaseDuration = Number.NaN;

    // It's okay to replace the initial doubles with smis after initialization.
    // This won't trigger the performance cliff mentioned above,
    // and it simplifies other profiler code (including DevTools).
    this.actualDuration = 0;
    this.actualStartTime = -1;
    this.selfBaseDuration = 0;
    this.treeBaseDuration = 0;
  }

  if (__DEV__) {
    // This isn't directly used but is handy for debugging internals:
    this._debugID = debugCounter++;
    this._debugSource = null;
    this._debugOwner = null;
    this._debugNeedsRemount = false;
    this._debugHookTypes = null;
    if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
      Object.preventExtensions(this);
    }
  }
}

举个例子:

javascript 复制代码
import React, { Component } from 'react';

class Header extends Component {
    render() {
        return <h1>{this.props.title}</h1>;
    }
}

function Content(props) {
    return (
        <div>
            <p>{props.text}</p>
            <Footer />
        </div>
    );
}

class Footer extends Component {
    render() {
        return <footer>Footer Content</footer>;
    }
}

class App extends Component {
    render() {
        return (
            <div>
                <Header title="Welcome to My App" />
                <Content text="This is some example content." />
            </div>
        );
    }
}

export default App;

Fiber结构:

Root Fiber Node

└── Type: "div" (elementType: "div")

├── Child Fiber Node

│ ├── Type: "Header" (elementType: Header)

│ │ └── State Node: Header instance

│ │ └── Props: { title: "Welcome to My App" }

│ └── Sibling: Content Fiber Node

└── Child Fiber Node

├── Type: "Content" (elementType: Content)

│ └── State Node: null (函数组件没有状态节点)

│ └── Props: { text: "This is some example content." }

│ └── Child Fiber Node

│ ├── Type: "Footer" (elementType: Footer)

│ │ └── State Node: Footer instance

│ │ └── Props: {}

│ └── Sibling: null

更新dom

双缓存机制

内存中绘制当前的 fiber dom,绘制完后直接替换上一帧的fiber dom,这样省去两帧之间替换的计算时间,就不会存在白屏的情况,因此就有两棵fiber树

  • current fiber 屏幕上正在显示的内容
  • workingprogress fiber 内存中正在构建的树 简称 wip fiber

alternate 连接


mount 构建过程

  • 应用级别的节点 ReactDom.render 创建 fiberRootNode
  • rootFiber 组件树的根节点

render阶段 --- scheduler reconciler

通过遍历 找到所有的fiber结构 实现可中断的异步递归

  1. 递 => 生成树
    创建节点,形成节点之间的关系
    vdom

从 rootfiber 深度优先遍历 fiber 调用 beginwork

  • 根据传入的 fiber 节点创建子 fiber 节点,连接两个 fiber 节点
  • 遍历叶子节点,进入归的阶段

  1. 调用completework
  • 创建真实的dom节点
  • 将当前节点下的子节点挂载到当前节点上
  • 收集当前节点的effectlist

递 归 交错执行,直到归到rootFiber

react源码解析

react github

react-dom

react-dom/src/client/ReactDOMRoot.js


最终返回ReactDOMRoot这个实例

ReactDOMRoot 和 ReactDOMHydrationRoot 上面都挂载 render 方法

接收children

调用updateContainer方法,传入children,updateContainer来进行递归的这个阶段,创建当前节点

后面还挂载了unmount方法

react-reconciler

react-reconciler/src/ReactFiberReconciler.js

这里着重是 Scheduler

创建FiberRootNode节点绑定root

render渲染方法传入children(App),调用updateContainer方法,进行大任务拆分小任务,优先级调度

react-reconciler/src/ReactFiberWorkLoop.js

着重介绍循环创建fiber树的方法

核心方法:performUnitOfWork,workLoopConcurrent

workLoopSync 同步方法,不会判断 shouldYield,这是和workLoopConcurrent方法的区别

workLoopConcurrent方法是异步模式,都是调用performUnitOfWork构造fiber树

这里是Scheduler调度器将渲染任务拆分成不同的任务单元去创建对应的fiber,fiber通过performUnitOfWork去完成fiber单元的创建,然后通过shouldYield判断是否执行这样的任务

workInProgress是全局的变量,存储在全局

performUnitOfWork中记录当前"递"和"归"的一个过程,判断当前满足条件,进入beginWork

Reconciler 协调器阶段,创建fiber树,递归遍历,diff算法比较差异

performUnitOfWork中判断是否是开发环境,开发环境开启性能调优则计算执行时间

调用beginWork进行节点的递阶段,拆解组件内容,并且返回下一个组件

当深度遍历到最底层的时候,开始进行归的阶段

则next为空,调用completeUnitOfWork开始"归"的阶段,归回父节点,更新父节点状态

"递"和"归"阶段是交错执行的,直到回到rootFiber为止

react-reconciler/src/ReactFiberBeginWork.js

  1. beginWork

    if 阶段:后续进入到diff的过程,非首次渲染
    否则为else阶段

后面:

  1. updateHostComponent

  2. reconcileChildren

    mount组件:创建新的子Fiber节点

react-reconciler/src/ReactChildFiber.js



通过调用useFiber创建fiber节点

react-reconciler/src/ReactFiber.js

找到workInProgress,为null则创建Fiber节点


不论是哪个方法,最终返回的都是 workInProgress.child下一个节点

react-reconciler/src/ReactFiberWorkLoop.js

performUnitOfWork中

上述 beginWork返回的是workInProgress.child下一个节点,因此next就会发生变化

next为null时候,则叶子节点为空,调用completeUnitOfWork

next不为空,则将next指针赋值给workInProgress,修改workInProgress指向,重新执行beginWork

在 completeUnitOfWork 中,创建对应的dom元素,如果sibling不为null,然后创建对应的指针

commit阶段 同步阶段

effectlist

  • before mutation 阶段,执行 dom操作前
  • mutation 阶段,执行dom操作阶段,遍历effectlist,执行mutation
  • layout 阶段,执行dom操作后,绘制

可以自己写一个实例,然后打断点看操作数据,操作结果

一句话来总结commit阶段所作的事情:

基于链表 的方式存储副作用,并根据优先级执行这些更新,直至所有的更新完成。

  1. React遍历fiber树并将需要执行副作用的节点以链表的形式收集起来
  2. 根据优先级存储更新
  3. 递归执行更新
相关推荐
酷爱码37 分钟前
css中的 vertical-align与line-height作用详解
前端·css
沐土Arvin1 小时前
深入理解 requestIdleCallback:浏览器空闲时段的性能优化利器
开发语言·前端·javascript·设计模式·html
专注VB编程开发20年1 小时前
VB.NET关于接口实现与简化设计的分析,封装其他类
java·前端·数据库
小妖6661 小时前
css 中 content: “\e6d0“ 怎么变成图标的?
前端·css
L耀早睡2 小时前
mapreduce打包运行
大数据·前端·spark·mapreduce
HouGISer2 小时前
副业小程序YUERGS,从开发到变现
前端·小程序
outstanding木槿2 小时前
react中安装依赖时的问题 【集合】
前端·javascript·react.js·node.js
霸王蟹3 小时前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹3 小时前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
专注VB编程开发20年3 小时前
asp.net IHttpHandler 对分块传输编码的支持,IIs web服务器后端技术
服务器·前端·asp.net