【源码分析】 一文搞清楚React全流程

Hello 大家好, 我是程序员Knight。今天带来的是React源码阅读。希望通过这一篇文章能够帮你对React有一个新的认知,以及帮助到正在阅读源码的你。

在本篇分享中,我将通过对React源码18.2.0的分析来解析:

  • 核心架构一览(Scheduler, Reconciler, Renderer)
  • 4大核心流程分析:
    • Trigger
    • Schedule
    • Render
    • Commit

希望通过阅读本文后,能够掌握其基本原理,并能在进行React编码过程中,在脑海中构造一个Fiber树,并模拟代码运行过程。

核心流程一览(Trigger,Schedule, Render, Commit)

在React中,从更新到展示一共分为4个阶段,分别是:

🎯 触发阶段 Trigger: 在这个阶段中的主要任务为 "通知部分应用需要渲染",在这个阶段,更像是创建一个任务,并在之后将任务交给调度器处理。比如在初始化React应用时,会通过 ReactDOM.render 或者 setState() 来触发渲染。

🎯 调度阶段 Schedule:在这个阶段,React会通过其内部实现的优先队列(Priority Queue)与任务优先级来处理"待处理的任务"。

🎯 渲染阶段 Render:通过 Diff算法 来计算出新的Fiber(WIP)树与当前Fiber树的区别,并将需要处理的更新应用到真实DOM上。

🎯 提交阶段 Commit:处理需要被更新的内容到真实DOM中,并处理所有的Effects(副作用)。

在这几个阶段中,一共三个比较重要的部分发挥着作用,分别是调度器(Scheduler)、协调器(Reconciler)、渲染器(Renderer):

1️⃣ Scheduler 调度器:主要是用来管理更新优先级,调度器(Scheduler)通过优先级调度、任务切片(time slicing)、支持任务中断与恢复达到了高优任务先执行的特性。确保了UI更新更顺畅,避免了长时间任务的堵塞

2️⃣ Reconciler 协调器:主要是用来计算并更新UI组件树,协调器(Reconciler)通过双缓存技术、任务拆分与调度、Diff算法实现了可中断渲染、自动批量更新、并发模式地高效更新UI。

3️⃣ Renderer 渲染器:主要负责将React Fiber树转换为目标环境可展示的UI,渲染器(Renderer)在不同的宿主环境中存在着不同的版本来适应对应的宿主环境,负责在React内部Commit阶段将已经生成的DOM节点挂载到根节点下来展示给用户。

全流程一览:

接下来让我们一起来看React工作的全流程

我们通常在React项目中都会通过这样的语法来初始化React应用:

javascript 复制代码
import { createRoot } from 'react-dom/client';
import React from 'react';

import App from './App';

const reactRoot = document.querySelector('#app');

const root = createRoot(reactRoot);
root.render(<App />);

在React中,这段代码涉及到两个部分:

  • createRoot 初始化ReactDOM对象
  • render(<App key={'app'} />); : 初始化渲染React应用

初始化 createRoot:

初始化一共做了这三件事: 1️⃣:树结构: 初始化React树结构(FiberRootNode 与 HostRootNode),

  • FiberRootNode 用来管理整个 Fiber

    markdown 复制代码
    FiberRootNode
    ├── current (指向当前 UI 的 Fiber 树)
    ├── workInProgress (指向正在渲染的新 Fiber 树)
  • 初始化 HostRootFiber 的更新队列

2️⃣:React 事件代理: React会对传入的DOM元素(id=root)进行事件代理,在内部会对事件集(例如click、 drag、contextMenu等)进行监听(通过addEventListener)。之后在我们使用 onClick 事件时,React并不会真正的把事件绑定到对应的DOM元素上,而是通过上述代理的方式在React内部找到对应的事件回调进行执行与触发。

3️⃣: 初始化ReactDOM并返回,ReactDOM中只有一个 _internalRoot 属性用来记录当前FiberRootNode,其 prototype 上有两个函数分别为 render 函数与unmount 函数

javascript 复制代码
function ReactDOMRoot(internalRoot) {
  this._internalRoot = internalRoot;
}

ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function (children) {
}

ReactDOMHydrationRoot.prototype.unmount = ReactDOMRoot.prototype.unmount = function () {
}

在初始化完成之后, 我们一起来看React的四大流程

Trigger 阶段

如果是React初次更新时, 由用户主动调用 render 函数来触发,用户需要将渲染的组件传给 render 函数,获取 HostRoot、时间、更新车道(用于调度系统。)、创建更新对象(记录了 payload、callback、next 等信息,用于最后在 render 阶段计算新的 state。)通过scheduleUpdateOnFiber -> ensureRootIsScheduled 触发调度器,并处理过渡更新(startTransition)

如果是React再次更新,通常会调用 dispatchSetState() (该函数也是React中的useState与useReducer状态更新的核心函数),相同地,Recat也会通过当前Fiber获取对应的更新优先级,并生成Update对象,并将其放入到更新队列中,通过scheduleUpdateOnFiber -> ensureRootIsScheduled 触发调度器,并处理过渡更新(startTransition)

Schedule 阶段

接下来React进入到调度阶段,在这里如果任务为可中断时,则通过调度器来进行任务处理,调度器负责任务的调度、优先级管理、时间切片。

这里一共有两个区分点:

  • 如果优先级为 Synclane: 则通过 scheduleMicrotask 微任务 的方式执行 performSyncWorkOnRoot
  • 其他的话:
    • 调用 调度器中的 scheduleCallback
    • 调度器会通过 优先级队列 进行任务队列管理和定时任务队列管理。
    • 简单来说,每次会把最高优先级的任务拿出来执行。

总结React调度器的实现如下: 调度器通过任务队列(即将到期的任务)与时间队列(处理延迟任务)协同管理任务调度优先级,将任务按照过期时间进行排序,保证高优先级任务快速被取出。并提供了通过时间判断实现了让出JS主线程的能力(在并发更新下的React会通过该方式判断是否需要让出主线程给到用户输入)。以异步的方式处理任务,减少堵塞主线程的可能。

Render 阶段

根据调用方式不同,接下来React会进入到以下两个函数(还未正式标志进入render阶段): performConcurrentWorkOnRoot

  • 规则判断:
    • 当前运行上下文不能在 renderContextcommitContext, 如果存在则报错
    • 是否存在 passiveEffects 如果存在并且没有任务时,则直接退出。
  • 调用 getNextLanes 获取当前更新优先级
  • 如果当前优先级 既不是 blockingLane 也不是 expiredlane 时 调用 renderRootConcurrent 否则调用 renderRootSync
  • 获取render结果:
    • RootErrored: 重试机制
    • RootFatalErrored: 重大错误
    • RootDidNotComplete: concurrent render
    • 其他/RootCompleted:调用 finishConcurrentRender 准备进入 commit 阶段
  • 调用 ensureRootIsScheduled(root, now());

performSyncWorkOnRoot

  • 规则判断

    • 当前运行上下文不能在 renderContextcommitContext, 如果存在则报错
    • flushPassiveEffects
  • 调用 getNextLanes 获取当前更新优先级

  • 调用 renderRootSync, 当结束结果为:

    • RootErrored: 重试机制

    • RootFatalErrored:

    • RootDidNotComplete: 错误

    • RootCompleted:

      javascript 复制代码
      var finishedWork = root.current.alternate;
      root.finishedWork = finishedWork;
      root.finishedLanes = lanes;
      commitRoot(root, workInProgressRootRecoverableErrors, workInProgressTransitions); 
  • ensureRootIsScheduled(root, now());

当调用到 renderRootConcurrent/renderRootSync则标志着进入到render阶段中

不论是renderRootConcurrent()/renderRootSync(), 其大致流程如下:

  • workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes: 检查当前正在渲染的 Fiber 树是否已经发生了变化,如果变化了,就可能需要重新开始渲染。如果需要重新渲染,则调用 prepareFreshStack(root, lanes); 生成 WIP
    • prepareFreshStack(root, lanes):根据当前树,克隆出双缓存中的"正在构建中"的树,
  • 接下来则进入到 workLoopSync() 或者 workLoopConcurrent(): 两者的主要不同在于workLoopConcurrent中具有可中断的特性(shouldYield()),可以在render过程中退出,从而避免过多任务导致主线程堵塞,利用空闲时间处理未完成的Fiber任务。
javascript 复制代码
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

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

performUnitOfWork(workInProgress)performUnitOfWork 开始,则开始处理 Diff 过程。

整个函数的遍历流程如下函数:从传参进来的 unitOfWork/全局变量WorkInProgress 从顶向下遍历,

javascript 复制代码
function performUnitOfWork(unitOfWork) { // unitOfWork === workInProgress
  var current = unitOfWork.alternate;
  var next;

  next = beginWork(current, unitOfWork, subtreeRenderLanes);
  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

整个Render阶段做的事情可以整理为三件事情: 1️⃣ 在初始化时:根据当前React Element生成对应的Fiber树。在更新阶段,根据Props,state或context更新Fiber阶段。 2️⃣ 为Fiber树进行打标(flags)根据变更类型(如插入、更新、删除)标记 subtreeFlags 和 flags 3️⃣ 在CompleteWork结束后,生成Fiber树对应的真实DOM树

简单的延伸:

  • 函数式组件会在beginWork()中调用。
  • 当React检查没有`props, st
  • Reconciler会进行一系列优化来减少时间复杂度:
    • 如果当前Fiber, Props 未变化、Context 未变化、组件类型未变化、无待处理的更新或 Context 变更、未处于错误/Suspense 的第二次渲染,跳过当前 Fiber 节点及其子树的Diff过程(即"提前退出"优化)。(beginWork中核心流程)(通过使用memo来包裹函数式组件,能够触发该流程,达到减少开销的目的)

    • React通过唯一key来识别元素,减少不必要的节点操作。我们在列表场景下需要给每个元素使用唯一的key来保证下次render时的元素复用

    • 当React进行更新时,识别到不同的元素时,React直接销毁旧的子树并创建新树。

Commit 阶段

当Render结束后,会进入到React的Commit阶段,在Commit阶段中,React 任务如下: 1️⃣ DOM更新:按照Render阶段后带处理标签的Fiber树结构变更对真实DOM进行 删除(先处理删除), 插入,属性更新等操作 2️⃣ 生命周期调用与Ref绑定处理: 调用函数式组件以及类组件中的生命周期相关函数。 3️⃣ 收尾:将Fiber树标记为 当前树(current

commitRoot(): 该函数是Commit阶段的起点,其函数大致结构如下:

javascript 复制代码
do {
  flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
  
// ...

// DOM操作前:
commitBeforeMutationEffects(root, finishedWork);
// 执行DOM改变操作
commitMutationEffects(root, finishedWork, lanes);
// DOM 更新后的副作用运行
commitLayoutEffects(finishedWork, root, lanes);

整个流程可以总结为4大阶段:

  1. 首先通过do-while语句 + flushPassiveEffects() 来处理所有待处理的被动副作用,通过do-while语句可以将重复的副作用在这一次处理完成。
  2. 在处理完副作用后,则进入到 commitBeforeMutationEffects() ,在这个运行过程中会执行类组建中的 getSnapshotBeforeUpdate() 函数。
  3. commitMutationEffects()的调用则标志着DOM变更的开始。在这个阶段中会将render阶段中已经被打好标签的Fiber应用到真实DOM中。React会遍历Fiber结构中的副作用链表,根据每个Fiber中的 flags 执行不同的操作。针对于删除操作,React会进行组件卸载的操作:
    • 针对类组件,会调用 componentWillUnmount进行同步处理
    • 针对于函数组件且存在 useEffect 则将其副作用的销毁函数标记为代执行(会在下次flushPassiveEffects)时进行执行。
    • 针对于Ref,也会进行对应的解绑操作 在这个函数结束则会执行 root.current = finishedWork 的操作(将FiberRoot的current指针切换到新的Fiber树)表示完成了本次更新。
  4. 执行Layout阶段的副作用,在这个阶段主要是执行需要在DOM变更后才需要的副作用,对于:
  • 类组件:componentDidMount(), componentDidUpdate() (mount或update)会在这个阶段被执行
  • 函数组件:useLayoutEffect() 会在这个阶段被执行,React会遍历Fiber节点中的Hooks链表,如果链表中存在 useLayoutEffect() 则会在这个阶段同步执行。
  • Ref:针对于Ref与真实DOM的绑定,也会在这个阶段进行替换。

简单的延伸:

  • commitMutationEffects() 运行中,真实DOM已经随之改变。
  • useLayoutEffect是在DOM更新完成,浏览器会之前同步执行,如果在回调函数中调用了setState,会立即触发更新,并在当前帧内同步完成新的渲染和提交,确保最终用户看到的是最新状态。useEffect是异步触发的/浏览器绘制后执行,对于不依赖DOM状态的副作用可以放到该回调函数中运行。
  • render阶段是可被中断的,但commit阶段是不可中断的。
javascript 复制代码
// 异步执行:可能导致布局闪烁
useEffect(() => {
  const newWidth = divRef?.current.offsetWidth;
  setWidth(newWidth); // 立即更新宽度
}, []);

// 同步执行:在绘制前调整布局
useLayoutEffect(() => {
  const newWidth = divRef?.current.offsetWidth;
  widthRef.current = newWidth
  setWidth(newWidth); // 立即更新宽度
}, []);

总结

在本文中,我们主要探讨了React的核心流程。希望对你平时的编程时在脑海中构建React Fiber树有帮助。如果你对我的文章感兴趣, 也麻烦进行点赞、收藏和关注~ React的源码涉及的内容实在过多,一篇文章还是显得过于单薄了。之后也会考虑再次总结一下出一个React源码阅读系列篇。

我是Knight,让我们下次再见~

相关推荐
招风的黑耳7 分钟前
Web元件库 ElementUI元件库+后台模板页面(支持Axure9、10、11)
前端·elementui·axure
雯0609~8 分钟前
CSS:使用内边距时,解决宽随之改变问题
前端·css
Dolphin_海豚18 分钟前
10 分钟带你入坑 electron
前端·javascript·electron
乐闻x28 分钟前
性能优化:javascript 如何检测并处理页面卡顿
前端·javascript·性能优化
雯0609~33 分钟前
vue3:八、登录界面实现-忘记密码
前端·javascript·vue.js
烂蜻蜓1 小时前
深入理解 HTML 中的<div>和元素:构建网页结构与样式的基石
开发语言·前端·css·html·html5
木木黄木木1 小时前
HTML5 Canvas弹跳小球游戏开发实战与技术分析
前端·html·html5
Anlici2 小时前
Axios 是基于 Ajax 还是 Fetch?从源码解析其实现
前端·面试
一个处女座的程序猿O(∩_∩)O2 小时前
Vue 中的 MVVM、MVC 和 MVP 模式深度解析
前端·vue.js·mvc
鱼樱前端2 小时前
前端程序员集体破防!AI工具same.dev像素级抄袭你的代码,你还能高傲多久?
前端·javascript·后端