⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖
一、【分析】🚩
JSX 是一个语法糖,使得开发人员可以通过熟悉的 HTML 标签来构建视图。从 JSX 代码到页面上渲染的真实 DOM,中间主要经历了两大部分的处理:
- 通过 babel 插件将 JSX 编译为虚拟 DOM 对象(即 VDOM)
- 执行 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 函数(点击展开)
importimport 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. ⌈渲染流程总结⌋🍥
- render 会调用 legacyRenderSubtreeIntoContainer 方法
- 如果是第一次渲染,legacyRenderSubtreeIntoContainer 会先初始化应用根节点 FiberRoot,同时生成根节点的 Fiber 实例
- 如果是更新渲染,legacyRenderSubtreeIntoContainer 会调用 updateContainer 方法,计算出本次更新的过期时间,并生成更新任务,将其插入更新队列,然后调用 scheduleUpdateOnFiber 进行任务调度
- scheduleUpdateOnFiber 会更新以该 Fiber 节点为根节点的 Fiber Tree 的过期时间,然后调用 ensureRootIsScheduled 进行调度
- ensureRootIsScheduled 中会将任务与其具体执行的函数进行绑定,然后交给 Scheduler 处理
【说明】🚩
- 编译流程中的代码截图来自于本地创建的 React 项目,主要是 webpack.config.js 和 node_modules
- 渲染流程中的代码截图来自于克隆到本地的 React 官方源码,主要是 react-dom 和 react-reconciler
- 本文只是作者的个人理解和思路,如果大家有看到更好的相关文章,可以分享在评论区
- 如果本文对您有帮助,烦请动动小手点个赞,谢谢