【React】让我看看谁还不知道 JSX 底层运行机制👀

⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖

一、【分析】🚩

JSX 是一个语法糖,使得开发人员可以通过熟悉的 HTML 标签来构建视图。从 JSX 代码到页面上渲染的真实 DOM,中间主要经历了两大部分的处理:

  1. 通过 babel 插件将 JSX 编译为虚拟 DOM 对象(即 VDOM)
  2. 执行 render 函数将虚拟 DOM 对象渲染为真实 DOM(即 RDOM)

二、【如何将 JSX 编译为 VDOM】🚩

React 16 版本中是通过 createElement 进行编译,而 React 17 及以上版本改为了通过 jsx 进行编译,至于为什么要更改编译方式,可以查看官方说明。本文中使用的版本是 React 18,源码可到 Github 获取。

1. ⌈判断处理模式⌋🍥

在 webpack 配置文件中找到 JSX 的编译配置

  • 这里引入了 babel 的预设插件 babel-preset-react-app

  • 并通过 hasJsxRuntime 获取运行时来判断基于哪种方式进行编译(点击展开)

    • automatic:表示采用新的编译模式
    • classic:表示采用旧版本的编译模式
  • 代码截图(点击展开)

2. ⌈babel-preset-react-app⌋🍥

  • 引入了新包 @babel/preset-react,并传递了三个参数

  • 其中的 runtime 值为上一步中的判断结果,并设置了默认值为 classic

  • 代码截图(点击展开)

3. ⌈@babel/preset-react⌋🍥

  • 这个包统一处理 React 的语法转换

  • 根据环境变量(开发环境还是生产环境)判断处理方式,下文中都是以生产环境处理为例进行的说明

  • 针对于生产环境,引入了 @babel/plugin-transform-react-jsx

  • 代码截图(点击展开)

4. ⌈@babel/plugin-transform-react-jsx⌋🍥

  • babel 在编译代码时,会经历三个步骤:解析、转换、生成(点击展开)
    • 解析:该步骤接收代码并生成 AST 抽象语法树,这期间会经历词法分析和语法分析两个阶段
    • 转换:将解析后的 AST 树,经过一系列的添加、更改、删除操作,转换成我们需要的新的 AST 结构
    • 生成:拿到转换后的 AST 语法树后,使用生成器生成最终的代码
  • 在遍历 AST 树时,每个节点都是一个访问者(visitor)。当不同类型(即 AST 结构中的 type)的节点到来时,会执行 visitor 中同类型名的函数

  • 转换前的 AST 只有一个节点,它的类型就是一个函数,也就是 Program。那么它在访问时,会执行到 Program 这个函数中(点击展开)

    • 根据运行时判断编译模式
    • classic 模式下,会使用 createElement 转换出最终的语法结构
    • automatic 模式下,会使用 jsx 转换出最终的语法结构

  • automatic 编译模式的额外处理(点击展开)

    • 经过上述转换,已经可以拿到最终的语法结构了
    • 如果是使用 jsx 进行的转换,会在节点离开时,对结构额外做一次处理
    • 对于 jsx 来说,key、_source、_self 这三个参数是通过单独的参数传递的,而不是通过 args 属性传递。我们需要通过 props 过滤掉这三个值,再把它们以单独的参数形式传递

5. ⌈createElement 和 jsx⌋🍥

  • 经过前面的编译,原始代码已经被编译成了 createElement 或者 jsx 格式

  • createElement 和 jsx 会根据 type 类型返回一个 ReactElement 对象

  • 代码截图(点击展开)

  • 两者源码对比(点击展开)

    • 旧版本依赖于 React,所以在编写代码时,要在首行导入 React,否则会报错。

6. ⌈配置编译模式⌋🍥

  • React 17 及以上版本默认使用新的编译模式(automatic)

  • 若想使用旧版编译模式,可以在 webpack 配置文件中做一些配置

  • 前面提到获取进行时的时候有一个判断条件 DISABLE_NEW_JSX_TRANSFORM === true

  • 我们可以对其进行设置,设置方式为:新增一个 build-create 脚本,添加上相关条件去执行

  • 这样我们先通过 npm run build-create 运行脚本,然后在 JSX 编译时,就会在 hasJsxRuntime 中检测到编译环境设置了禁用新版 jsx 编译,从而返回 false,最终使用 classic 编译模式

  • 代码截图(点击展开)

三、【如何将 VDOM 渲染为 RDOM】🚩

1. ⌈render⌋🍥

  • 在项目入口文件 src/main.jsx 中调用 render 函数(点击展开)

    import 复制代码
    import ReactDOM from 'react-dom/client'
    import App from './App.jsx'
    
    ReactDOM.createRoot(document.getElementById('root')).render(
      <React.StrictMode>
        <App />
      </React.StrictMode>,
    )
  • 查看 React 源码可知,render 函数内部其实是一连串的函数调用,最终执行的是 legacyRenderSubtreeIntoContainer 函数,来实现挂载和更新 (点击展开)

2. ⌈legacyRenderSubtreeIntoContainer⌋🍥

  • 第一次运行项目时,ReactDOM.render 也是第一次执行,获取的 contaienr._reactRootContainer 肯定是没有值的,所以会进入到 Initial mount 逻辑中

  • 在该逻辑中会调用 legacyCreateRootFromDOMContainer 创建 FiberRoot

  • 代码截图(点击展开)

3. ⌈legacyCreateRootFromDOMContainer⌋🍥

  • 该函数中会根据 container 类型的不同分别执行 createHydrationContainer 和 createContainer(点击展开)

  • 而这两个函数中最终都会执行 createFiberRoot(点击展开)

  • 在 createFiberRoot 函数中,会通过 new FiberRootNode 创建一个 FiberRoot 实例(点击展开)

    • FiberRootNode 包含很多属性,这些属性在任务调度阶段都发挥着各自的作用

  • 然后通过 createHostRootFiber 创建 fiber tree 的根节点,即 rootFiber(点击展开)

    • FiberNode 构造函数用于创建一个 FiberNode 实例,即 fiber 节点
    • 刚创建出来的这个 fiber 节点,会作为整个 fiber tree 的根节点,即 rootFiber

  • 至此,就成功地创建出了 rootFiber 节点,接下来回到 legacyRenderSubtreeIntoContainer 函数中,会执行 getPublicRootInstance 函数(点击展开)

4. ⌈getPublicRootInstance⌋🍥

  • 首先获取到当前节点,即上一步中创建出来的 rootFiber

  • 如果 rootFiber 没有子节点,则返回 null

  • 如果有,则返回其子节点的实例

  • 至此,render 函数的主要流程就结束了

  • 代码截图(点击展开)

5. ⌈updateContainer⌋🍥

当页面发生变化,进行更新渲染时,legacyRenderSubtreeIntoContainer 函数中就不会执行 Initial mount 的逻辑,而是进入 Update 逻辑,调用 updateContainer 函数

  • 该函数中会先计算出当前更新的优先级(lane)

  • 然后通过 createUpdate 创建一个更新任务

  • 接着通过 enqueueUpdate 将创建的更新任务插入到循环任务队列中

  • 最后调用 scheduleUpdateOnFiber 来处理优先级和挂载更新节点(点击展开)

    更新以当前 Fiber 节点为根节点的 Fiber Tree 的过期时间,主要内容在 ensureRootIsScheduled 方法中

    • 计算此次任务的过期时间和优先级

    • 如果当前节点已有任务在调度中,若过期时间相同,且已有任务的优先级更高,则取消本次调度,否则取消已有任务

    • 将任务推入 Scheduler 中的调度队列,并设置其优先级和过期时间

    • 代码截图(点击展开)

  • 代码截图(点击展开)

6. ⌈渲染流程总结⌋🍥

  1. render 会调用 legacyRenderSubtreeIntoContainer 方法
  2. 如果是第一次渲染,legacyRenderSubtreeIntoContainer 会先初始化应用根节点 FiberRoot,同时生成根节点的 Fiber 实例
  3. 如果是更新渲染,legacyRenderSubtreeIntoContainer 会调用 updateContainer 方法,计算出本次更新的过期时间,并生成更新任务,将其插入更新队列,然后调用 scheduleUpdateOnFiber 进行任务调度
  4. scheduleUpdateOnFiber 会更新以该 Fiber 节点为根节点的 Fiber Tree 的过期时间,然后调用 ensureRootIsScheduled 进行调度
  5. ensureRootIsScheduled 中会将任务与其具体执行的函数进行绑定,然后交给 Scheduler 处理

【说明】🚩

  • 编译流程中的代码截图来自于本地创建的 React 项目,主要是 webpack.config.js 和 node_modules
  • 渲染流程中的代码截图来自于克隆到本地的 React 官方源码,主要是 react-dom 和 react-reconciler
  • 本文只是作者的个人理解和思路,如果大家有看到更好的相关文章,可以分享在评论区
  • 如果本文对您有帮助,烦请动动小手点个赞,谢谢
相关推荐
&白帝&23 分钟前
uniapp中使用picker-view选择时间
前端·uni-app
谢尔登30 分钟前
Babel
前端·react.js·node.js
ling1s30 分钟前
C#基础(13)结构体
前端·c#
卸任36 分钟前
使用高阶组件封装路由拦截逻辑
前端·react.js
lxcw1 小时前
npm ERR! code CERT_HAS_EXPIRED npm ERR! errno CERT_HAS_EXPIRED
前端·npm·node.js
秋沐1 小时前
vue中的slot插槽,彻底搞懂及使用
前端·javascript·vue.js
这个需求建议不做1 小时前
vue3打包配置 vite、router、nginx配置
前端·nginx·vue
QGC二次开发1 小时前
Vue3 : Pinia的性质与作用
前端·javascript·vue.js·typescript·前端框架·vue
云草桑1 小时前
逆向工程 反编译 C# net core
前端·c#·反编译·逆向工程
布丁椰奶冻1 小时前
解决使用nvm管理node版本时提示npm下载失败的问题
前端·npm·node.js