🧑💻 写在开头
点赞 + 收藏 === 学会🤣🤣🤣
本系列我们将探寻react的实现原理,实现一些小
demo
,让我们能够更直观的理解原理,最终实现一个mini react
。
🥑 你能学到什么?
希望在你阅读本篇文章之后,不会觉得浪费了时间。如果你跟着读下来,你将会学到:
- jsx语法、基本使用、方法集
- jsx是如何转化为DOM的
- jsx的转换流程
- jsx为什么被提到了运行时
- ReactElement的数据结构
- 源码
createElement
解析 - 源码
ReactElement
解析
🍑 一、jsx的几大问题你都知道吗?
- JSX的本质是什么?他和js的关系是什么?
- JSX 语法是如何在 js 中生效的?
- 既然jsx最终会转换为
React.createElement
,我们为啥不直接用React.createElement
- JSX是如何映射为DOM的,转换流程是什么样的?【转换为DOM我们后续单开一节介绍】
1.JSX的本质是什么?他和js的关系是什么?
JSX是js 的一种语法扩展,它和模板语言很接近,但是它充分具备js 的能力,也就是说你可以把jsx放在各种运算中,或者是函数参数中,甚至在函数中返回。
本质是JS的扩展,是生成createElement
的语法糖,通过调用createElement
去处理key,ref,等等,调用ReactElement
函数去生成ReactElement
2.一句话总结createElement
的作用
createElement
仅仅是格式化数据 ,让数据符合ReactElement
函数的入参【看完文章的后续,你会豁然开朗】
3.JSX 语法是如何在 js 中生效的?
都说jsx会被编译成React.createElement
,那这个编译过程是如何做的?谁做的,为什么浏览器不直接支持他?
从jsx
的本质上来看,它的定位是js
的语法扩展,都不是语法标准,那谁会支持他?
那我们如何编译jsx呢?这就引入了babel
,babel
是一个工具链,主要用于将 es6+版本的代码转换为向后兼容的 js 语法,以便能够运行在当前和旧饭本的浏览器或其他环境中,他也可以做一些其他的语法转换之类的,比如ts转js,jsx转js等等,后续我们会有一个篇文章专门介绍babel
4.既然jsx最终会转换为React.createElement
,我们为啥不直接用React.createElement
一张图就能看明白,就是为了开发体验,如果嵌套很深,React.createElement
能写吐我们,我们去babel转换一下,之前搭建的项目里面的app.tsx
,代码如下 记得选择这个,后续的react
版本中jsx已经被提到了运行时中
tsx
import React from 'react';
import img from '../javascript.png';
import './app.scss';
import {Button} from '@components/Button/Button';
function App() {
return (
<>
<Button label='Button' onClick={() => {}} primary />
<div className='App'>React18 + Ts5 + webpack5 开发模板搭建</div>;
<img src={img}></img>
</>
);
}
export default App;
5.jsx被提到运行时的原因
在React 17
及以后的版本中,引入了一项新的JSX转换方式,称为"新的JSX转换"(New JSX Transform)。主要就是将JSX的转换逻辑从编译时(通过Babel插件转换)更多地移动到运行时。原因如下:
- 减少
import React
的需求 :在旧的转换方式中,每个使用JSX的文件都需要在文件顶部导入React(import React from 'react';
),因为转换后的代码需要React.createElement
函数。新的JSX转换方式使得这种导入变得不必要,因为转换后的代码不再依赖于React.createElement
。相反,它可以直接使用从React 17开始引入的新入口来创建JSX元素。 - 自动导入 :新的转换方式意味着Babel(或其他编译工具)会自动插入所需的JSX创建函数的导入声明。这可以让代码看起来更简洁,避免了冗余的
import
语句,并且降低了新手可能遇到的"忘记导入React"的问题。 - 更好的打包和树摇(Tree-shaking) :通过将JSX创建函数的处理推迟到运行时,React库可以更好地支持模块化和树摇优化。这意味着,如果你的应用中有些React特性没有被用到,那些代码可以在最终打包时被更有效地排除掉,减少应用的体积。
- 为将来的优化打下基础:新的JSX转换方式还允许React团队在不破坏现有应用的情况下引入新特性和性能优化。通过在运行时而不是编译时处理JSX,React可以更灵活地更新和优化元素的创建逻辑。
在旧的版本中,转换结果依赖于React全局变量的存在(因此需要导入React)。转换完成的代码在运行时不需要执行额外的转换,直接被执行即可。
新版中,提到运行时不是说在react
运行的时候才去编译jsx
,而是将转换为React.createElement
的代码封装到模块中导出,babel
会将这些模块自动导入,然后将我们的jsx转换为模块中的导出函数,这个过程不需要我们关注,导出的函数也不是依赖于React全局变量的,这样自然不再需要我们去手动引入React
,具体我们看官网的转换
、
🥑 二、用babel手搓模拟一下jsx的转换
1.简单介绍下
在babel
中,将JSX
转为js
使用的是一个插件预设@babel/preset-react
,主要是以下几个插件:
@babel/plugin-syntax-jsx
: 仅允许Babel解析(但不转换)JSX语法。@babel/plugin-transform-react-jsx
: 转换JSX为React.createElement
调用。这是实现JSX的主要插件。@babel/plugin-transform-react-display-name
: 自动添加displayName
属性到React组件,有助于调试。@babel/plugin-transform-react-jsx-source
(开发模式下可用): 添加源文件和行号信息到JSX元素,增强调试能力。@babel/plugin-transform-react-jsx-self
(开发模式下可用): 添加对this
的引用到JSX元素,以便在React的开发者工具中使用。
2.手搓模拟
编写test.js
文件
js
import React from 'react';
import img from '../javascript.png';
import './app.scss';
import {Button} from '@components/Button/Button';
function App() {
return (
<>
<Button label='Button' onClick={() => {}} primary />
<div className='App'>React18 + Ts5 + webpack5 开发模板搭建</div>;
<img src={img}></img>
</>
);
}
export default App;
2.编写转换代码jsx.js
就是读取文件,然后调用babel插件模拟转换一下。
js
const fs = require('fs');
const babel = require('@babel/core');
fs.readFile('./test.js', (e, data) => {
// 读取数据
const code = data.toString('utf-8');
// 转换jsx文件
const result = babel.transformSync(code, {
plugins: ['@babel/plugin-transform-react-jsx'],
});
// 写入文件
fs.writeFile('./result.js', result.code, function () {});
});
3.转换结果result.js
js
import React from 'react';
import img from '../javascript.png';
import './app.scss';
import { Button } from '@components/Button/Button';
function App() {
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Button, {
label: "Button",
onClick: () => {},
primary: true
}), /*#__PURE__*/React.createElement("div", {
className: "App"
}, "React18 + Ts5 + webpack5 \u5F00\u53D1\u6A21\u677F\u642D\u5EFA"), ";", /*#__PURE__*/React.createElement("img", {
src: img
}));
}
export default App;
🍉 三、源码探究
1.createElement
createElement
仅仅是做格式化数据 ,让数据符合ReactElement
函数的入参
- 处理ref、key、self、source等
- 处理children
- 将属性提取到props中
js
/**
* createElement
*/
export function createElement(type, config, children) {
// propName 变量用于储存后面需要用到的元素属性
let propName;
// props 变量用于储存元素属性的键值对集合
const props = {};
// key、ref、self、source reac属性,key、ref应该很熟悉了吧
let key = null;
let ref = null;
let self = null;
let source = null;
// config 对象中存储的是元素的属性
if (config != null) {
// ref赋值
if (hasValidRef(config)) {
ref = config.ref;
}
// 此处将 key 值字符串化
if (hasValidKey(config)) {
key = "" + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 属性提取到props中去
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// 处理children
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// 处理 defaultProps
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props
);
}
2.ReactElement
$$typeof
眼熟吗?我们打印下之前搭建的项目里面的app.tsx
的返回值,控制台查看一下,这不就是jsx返回的一个一个ReactElement
吗?react就是根据这个去构建fiber
树的,fiber
我们后续会说的。
js
const ReactElement = function (type, key, ref, self, source, owner, props) {
element = {
// REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner,
};
//
if (__DEV__) {
// 一些调试逻辑
}
return element;
};
🍎 推荐阅读
工程化系列
本系列是一个从0到1的实现过程,如果您有耐心跟着实现,您可以实现一个完整的react18 + ts5 + webpack5 + 代码质量&代码风格检测&自动修复 + storybook8 + rollup + git action
实现的一个完整的组件库模板项目。如果您不打算自己配置,也可以直接clone组件库仓库切换到rollup_comp
分支即是完整的项目,当前实现已经足够个人使用,后续我们会新增webpack5优化、按需加载组件、实现一些常见的组件封装:包括但不限于拖拽排序、瀑布流、穿梭框、弹窗等
- 手把手带你搭建前端项目:react18、ts5、lint四剑客、webpack、storybook【保姆级教程一】
- 手把手带你搭建前端项目:react18、ts5、lint四剑客、webpack、storybook【保姆级教程二】
- 手把手带你搭建前端项目:react18、ts5、lint四剑客、webpack、storybook【保姆级教程三】
- 手把手带你搭建前端项目:react18、ts5、lint四剑客、webpack、storybook【保姆级教程四】
- 前端三大包管理器你知道多少?npm、yarn、pnpm
- 前端怎么可以不会GitHub Action一键部署?
面试手写系列
其他
🍋 写在最后
如果您看到这里了,并且觉得这篇文章对您有所帮助,希望您能够点赞👍和收藏⭐支持一下作者🙇🙇🙇,感谢🍺🍺!如果文中有任何不准确之处,也欢迎您指正,共同进步。感谢您的阅读,期待您的点赞👍和收藏⭐!
感兴趣的同学可以关注下我的公众号ObjectX前端实验室
🌟 少走弯路 | ObjectX前端实验室 🛠️「精选资源|实战经验|技术洞见」