「从0实现React18系列」自己动手实现一个JSX转换
很久没有在掘金发表文章了,记得上一次还是2020年。这次回来在写这篇文章之前想写一个“深入浅出React核心模块的实现原理”,JSX的转换是这个系列的第一篇。
由于本人水平有限,还希望大家多多提出宝贵意见,谢谢大家!
在看文章之前,我们可以先想几个问题:
- JSX 是什么语法?
- JSX 有什么优势,它的转换规则是什么或者它内部是如何实现的?
- 既然 React 一直在使用 JSX,那它的实现被写应该写在哪个包里(比如
react、react-dom,react-reconciler
)? - 在React 17之前和React 17之后,JSX转换的方法实现有哪些异同?
- 如何实现React.createElement方法和运行时的 jsx 方法?
- 写一个Demo引入自己实现的jsx方法,看看运行结果
big-react 是我根据卡颂老师的课,从0到1实现的React框架
这篇文章可能更像是笔记 -,-
如果自己实现一个 React 框架,它需要包含哪些内置的包:
react
包是 React 的核心库,提供了创建和管理组件所需的基本功能(比如组件创建、组件生命周期管理、虚拟DOM以及Hooks等),主要是一些和宿主环境无关的方法。react-reconciler
包实现了 React 的reconciliation
协调算法,是一种核心优化策略的实现,主要自定义协调器的实现。以及在不同的平台或环境中使用 React。shared
包是big-react公用的辅助方法,和宿主环境无关。
如果还有一个必要的包,那就是react-dom
:
react-dom
:这个包提供了将 React 与 DOM(浏览器环境)集成的方法。它包含了用于将 React 组件渲染到 DOM 中的ReactDOM.render()
函数,以及其他与浏览器环境相关的实用功能。对于在浏览器中运行的 React 应用程序,react-dom
是必需的。
扩展
当我们在项目中使用 React 构建界面时,主要使用的就是 react
包。它提供了开发者需要的所有API。如React.Component
、React.createElement
、React.useState
等等,所以它也是大多数 React 项目的基础。
react-reconciler
包是一个更底层、更高级的库,它实现了reconciliation
协调算法,reconciliation
是 React 的一种核心优化策略,用于在更新组件时比较虚拟DOM树的差异,并将实际更改应用到实际的DOM树。这有助于提高性能,因为避免了不必要的DOM操作。
它主要用于创建自定义渲染器,以及在不同的平台中去使用 React。例如,react-dom(用于Web平台)和react-native(用于移动应用)都使用react-reconciler作为底层库,实现了针对各自平台的渲染逻辑。
JSX 是什么
const element = <div className="container">Hello, world!</div>;
在React中,JSX是一种JavaScript语法扩展,允许你在JavaScript代码中编写类似HTML的标记。要使用JSX,需要在构建过程中将其转换为标准的JavaScript代码。
通常,这个转换过程包括两个主要部分:
- 编译时:通常指将 JSX 语法转换为浏览器可以理解的普通 JavaScript 代码的过程,这个过程通常由 Babel 完成。
- 构建时:在将JSX语法转换为标准的JavaScript代码后,通常会使用构建和打包工具(如Webpack、Rollup)对代码进行优化、压缩和打包。打包工具将源代码和依赖项组合成一个或多个文件(“bundles”或“chunks”),用于在浏览器中运行。
- 运行时:React会根据编译后的代码创建虚拟DOM树,然后将其渲染到实际的DOM中。还会发生的阶段有状态管理和更新、事件处理和Diff算法的比较等。
JSX 被 Babel 编译成了什么
在React 17之前,JSX语法会被编译成React.createElement
函数的调用,用来创建虚拟DOM元素。
转换结果如下:
const element = React.createElement(
"div",
{ className: "container" },
"Hello, world!"
);
从React 17开始,引入了新的JSX转换功能,称为"Runtime Automatic"(自动运行时)。这意味着在使用JSX语法时,不再需要手动引入React库
。在自动运行时模式下,JSX会被转换成新的入口函数,import {jsx as _jsx} from 'react/jsx-runtime'; 和 import {jsxs as _jsxs} from 'react/jsx-runtime';
。
转换结果如下:
import { jsx as _jsx } from "react/jsx-runtime";
const element = _jsx("div", {
className: "container",
children: "Hello, world!"
});
接下来我们就来实现jsx
方法或React.createElement
方法(包括dev、prod两个环境)。
工作量包括:
- 实现
jsx
方法 - 实现打包流程
- 实现调试打包结果的环境
实现 jsx 转换方法
jsx 转换方法包括:
React.createElement
方法jsxDEV方法
(dev环境)jsx
方法(prod环境)
实现React.createElement
在React 17之前,JSX转换应用的是createElement
方法,下面是它的实现:
/**
*
* @param type 元素类型
* @param config 元素属性,包括key,不包括子元素children
* @param maybeChildren 子元素children
* @returns 返回一个ReactElement
*/
const createElement = (
type: ElementType,
config: any,
...maybeChildren: any
) => {
// reactElement 自身的属性
let key: Key = null;
let ref: Ref = null;
// 创建一个空对象props,用于存储属性
const props: Props = {};
// 遍历config对象,将ref、key这些ReactElement内部使用的属性提取出来,不应该被传递下去
for (const prop in config) {
const val = config[prop];
if (prop === 'key') {
if (val !== undefined) {
key = '' + val;
}
continue;
}
if (prop === 'ref') {
if (val !== undefined) {
ref = val;
}
continue;
}
// 去除config原型链上的属性,只要自身
// 一般使用{...props}将所有属性都传递下去,所以摘除ref、key属性外需要被保存到props中
if ({}.hasOwnProperty.call(config, prop)) {
props[prop] = val;
}
}
const maybeChildrenLength = maybeChildren.length;
if (maybeChildrenLength) {
// [child] [child, child, child]
if (maybeChildrenLength === 1) {
props.children = maybeChildren[0];
} else {
props.children = maybeChildren;
}
}
return ReactElement(type, key, ref, props);
};
注意:React.createElement方法和jsx方法的区别这里只体现在第三个参数上。
实现jsx方法
从React 17之后,JSX转换应用的是jsx
方法,下面是它的实现:
/**
*
* @param type 元素类型
* @param config 元素属性
* @param maybeKey 可能的key值
* @returns 返回一个ReactElement
*/
const jsx = (type: ElementType, config: any, maybeKey: any) => {
// 初始化key和ref为空
let key = null;
let ref = null;
// 创建一个空对象props,用于存储属性
const props: Props = {};
// 遍历config对象,将ref、key这些ReactElement内部使用的属性提取出来,不应该被传递下去
for (const prop in config) {
const val = config[prop];
if (prop === "key") {
continue;
}
if (prop === "ref") {
if (val !== undefined) {
ref = val;
}
continue;
}
// 一般使用{...props}将所有属性都传递下去,所以摘除ref、key属性外需要被保存到props中
if ({}.hasOwnProperty.call(config, prop)) {
props[prop] = val;
}
}
// 将 maybeKey 添加到 key 中
if (maybeKey !== undefined) {
key = "" + maybeKey;
}
return ReactElement(type, key, ref, props);
};
这段代码定义了一个jsx
函数,主要用于创建React元素。首先,它会提取可能存在的key和ref属性,并将剩余属性添加到一个新的props对象中。最后用ReactElement
函数创建一个React元素并返回。
从上面代码中可以看到还实现了ReactElement方法
:
// jsx-runtime.js
const supportSymbol = typeof Symbol === 'function' && Symbol.for;
// 为了不滥用 React.elemen,所以为它创建一个单独的键
// 为React.element元素创建一个 symbol 并放入到 symbol 注册表中
export const REACT_ELEMENT_TYPE = supportSymbol
? Symbol.for('react.element')
: 0xeac7;
export const ReactElement = function (type, key, ref, props) {
const element = {
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
ref,
props,
_mark: 'lsh',
};
return element;
};
export const jsx =...
用自己实现的的jsx接入Demo
我们试着把自己实现的jsx方法
,创建一个ReactElement
,看它是否能够渲染在页面上。
jsx方法和createElement的区别
jsx
函数和createElement
函数都用于在React中创建虚拟DOM元素,但它们的语法和用法有所不同。jsx函数来自于React 17及更高版本中的新的JSX转换功能,称为"Runtime Automatic"。
以下是两者之间的主要区别:
- 语法和转换方式:
jsx
函数用于处理新的JSX转换方式,其语法更简洁。createElement
函数用于处理传统的JSX转换方式。
例如,一个JSX元素:
const element = <div className="container">Hello, world!</div>;
使用createElement
转换后的代码如下:
const element = React.createElement(
"div",
{ className: "container" },
"Hello, world!"
);
使用jsx
函数(自动运行时)转换后的代码如下:
import { jsx as _jsx } from "react/jsx-runtime";
const element = _jsx("div", { className: "container", children: "Hello, world!" });
- 子元素和key值处理:
jsx
函数将子元素作为属性(children
)传递,而createElement
函数将子元素作为额外的参数传递。同时子元素上的key
值在jsx
函数中也会以第三个参数的形式传递,而在createElement
函数中,则是存在于config
第二个参数中。
在createElement
函数中:
React.createElement("div", {className: "app", key: "appKey"}, "hello,app");
在jsx
函数中:
import { jsx as _jsx } from "react/jsx-runtime";
_jsx("div", {className: "app", children: "hello,app"}, "appKey");
- 兼容性和版本:
createElement
函数在所有React版本中可用,而jsx函数仅在React 17及更高版本中提供。尽管React团队推荐使用新的JSX转换方式,但许多现有项目可能仍在使用createElement
函数。
这时可能产生两个疑问:
- 从React 17之后使用
Runtime Automatic
自动运行时有什么好处?
- 简化组件代码:不再需要在每个组件文件顶部添加import React from 'react';。这使得组件代码更简洁,更易于阅读和维护。
- 节省包大小:由于不再需要导入整个React对象,构建工具可以更好地优化输出代码,从而减小输出包的大小。
- 改成
jsx
函数后,为什么要把key
属性单独拿出来放在第三个参数?
在之前的React版本中,每当创建一个新的React元素时,React都需要从属性对象中提取key
和ref
,这会导致额外的性能开销。
将key
作为单独的参数传递,可以让React在处理虚拟DOM树时更容易地访问key
,无需每次都从属性对象中查找。这有助于提高React的性能和效率,特别是在处理大量元素和复杂组件树时。
实现打包流程
打包流程稍微有些复杂,后续写到文章里。
简单来说就是使用 Rollup,将编写jsx方法的文件打包出来,通过pnpm link --global
的方式生成一个全局的react
包,这样就可以通过pnpm link react --global
调试自己创建的 create-react-app demo项目了。
big-react 项目地址
转载自:https://juejin.cn/post/7212235580104917053