之前的一篇 基于 Webpack 从 0 到 1 启动一个 React 项目 文章中有介绍的是如何从 0
到 1
配置 React
项目中的 JSX
转换,在查阅文档时有介绍到
从本质上讲,JSX 只是为
React.createElement(component, props, ...children)
函数提供的语法糖
但真正在浏览器查看编译结果时发现 JSX
没有完全和 React.createElement(component, props, ...children)
画等号,实际上编译结果有可能是两个
React.createElement(component, props, ...children)
JSX runtime
您可以在线查看完整的示例源代码
区别
首先从示例里面找到这两个编译结果,分别如下
// React.createElement
return /*#__PURE__*/ React.createElement(
"div",
null,
"count:" + count,
/*#__PURE__*/ React.createElement(
"button",
{
onClick: handleClick,
},
"click + 1"
)
);
// JSX runtime
return /*#__PURE__*/ (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxs)(
"div",
{
children: [
"count:" + count,
/*#__PURE__*/ (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx)(
"button",
{
onClick: handleClick,
children: "click + 1",
}
),
],
}
);
为了便于观看,我做了格式化处理,实际编译结果应该只有一行
阅读过后会发现只有渲染函数上有区别((0, fn)()
是 IFFE
),但是根据 React
文档上说 JSX
实际上是 React.createElement
的语法糖,那为什么还需要 react_jsx_runtime__WEBPACK_IMPORTED_MODULE_1__.jsx
(其实就是 JSX runtime
)
JSX runtime
其实对于这个问题,React
文档专门有一个博客文章来介绍这个部分,JSX runtime
是一种新的 JSX
转换,像过去做 JSX
转换的时候,比如使用 @babel/preset-react
去转,默认是将 JSX
编译成 React.createElement
的形式,而现在你使用通过配置以后再转就是 JSX runtime
的形式,具体配置如下
// .babelrc
// React.createElement
{
"presets": [
["@babel/preset-react", {
"runtime": "classic"
}]
]
}
// .babelrc
// JSX runtime
{
"presets": [
["@babel/preset-react", {
"runtime": "automatic"
}]
]
}
对于 React
这种世界级流行的前端框架,换渲染函数(无论是 React.createElement
还是 JSX runtime
底层都一样的渲染逻辑,只不过名字换了而已)这种事情肯定有它自己的理由,为此它给出的推出的新转换的优势如下
优势
- 使用全新的转换,你可以单独使用 JSX 而无需引入 React。
- 根据你的配置,JSX 的编译输出可能会略微改善 bundle 的大小。
- 它将减少你需要学习 React 概念的数量,以备未来之需。
说实话这三点里面一、三点都非常离谱,首先是
第三点我实在是想不出来有什么减少的,难道 JSX
编译成 React.createElement
的八股不会变成 JSX
编译成 JSX runtime
的八股吗?
无需引入 React
还有第一点,在博客里面的语境意思就是,我们不再需要去写组件的时候,写上很别扭的一行引用
// React 17 RC 以前
import React from "react";
function App() {
return <h1>Hello World</h1>;
}
// React 17 RC 及其以后
function App() {
return <h1>Hello World</h1>;
}
为什么需要 import React from "react";
是因为它的编译结果是需要 React.createElement
,虽然在源代码里面没有用到,但是编译后是需要的,否则你会收到一个报错
Uncaught ReferenceError: React is not defined
但也恰恰是因为源代码(编译前)没有用到,像一些编译器或者包会频繁给你提醒,提示存在一个未使用的引用,如果小白不懂,按提示操作就会喜提上面的报错
减小打包体积
根据你的配置,JSX 的编译输出可能会 略微改善 bundle 的大小。
这一点我认为是比较重要的(可以考察候选人对模块的理解)
为什么改变新转换以后还可以减小打包体积?举个例子
// React.createElement
// 手动引入 react
import React from 'react';
function App() {
return React.createElement('h1', null, 'Hello world');
}
// JSX runtime
// 由编译器引入(禁止自己引入!)
import {jsx as _jsx} from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}
上面对于渲染函数的引用分为了两种方式
_jsx
解构引用React.createElement
全量引用
直接引用 React
会将非 createElement
相关的 API
(比如上面例子中就没有用到 useState
)一并被打包进编译文件中,而使用解构,可以通过 tree-sharking
来排除掉项目中未使用到的 React API
,这就是为什么 JSX runtime
可以减小打包体积
看到这里你应该明白了,其实 _jsx
可以理解为 createElement
,如下
import { createElement } from 'react';
function App() {
return createElement('h1', { children: 'Hello world' });
}
但因为在 React 17 RC
以前,编译结果只能是 React.createElement(...)
,因此无法修改成上面这种写法,但不知道出于什么原因 React
在改进的时候直接选择了换掉 React.createElement
Tree Sharking
解构就能减小打包体积的原因是什么呢?怎么减的?这里可以给出 rollup
的 tree-sharking
编译结果的例子](https://rollupjs.org/repl/?version=3.7.5&shareable=JTdCJTIybW9kdWxlcyUyMiUzQSU1QiU3QiUyMm5hbWUlMjIlM0ElMjJtYWluLmpzJTIyJTJDJTIyY29kZSUyMiUzQSUyMiUyRiolMjBUUkVFLVNIQUtJTkclMjAqJTJGJTVDbmltcG9ydCUyME1hdGglMjBmcm9tJTIwJy4lMkZtYXRocy5qcyclM0IlNUNuJTVDbmNvbnNvbGUubG9nKCUyME1hdGguY3ViZSglMjA1JTIwKSUyMCklM0IlMjAlMkYlMkYlMjAxMjUlMjIlMkMlMjJpc0VudHJ5JTIyJTNBdHJ1ZSU3RCUyQyU3QiUyMm5hbWUlMjIlM0ElMjJtYXRocy5qcyUyMiUyQyUyMmNvZGUlMjIlM0ElMjIlMkYlMkYlMjBtYXRocy5qcyU1Q24lNUNuZXhwb3J0JTIwZGVmYXVsdCUyMCU3QiU1Q24lMjAlMjBzcXVhcmUoeCklMjAlN0IlNUNuJTVDdCUyMCUyMHJldHVybiUyMHglMjAqJTIweCUzQiU1Q24lMjAlMjAlN0QlMkMlNUNuJTIwJTIwY3ViZSUyMCh4KSUyMCU3QiU1Q24lNUN0JTIwJTIwcmV0dXJuJTIweCUyMColMjB4JTIwKiUyMHglM0IlNUNuJTIwJTIwJTdEJTVDbiU3RCUyMiUyQyUyMmlzRW50cnklMjIlM0FmYWxzZSU3RCU1RCUyQyUyMm9wdGlvbnMlMjIlM0ElN0IlMjJmb3JtYXQlMjIlM0ElMjJlcyUyMiUyQyUyMm5hbWUlMjIlM0ElMjJteUJ1bmRsZSUyMiUyQyUyMmFtZCUyMiUzQSU3QiUyMmlkJTIyJTNBJTIyJTIyJTdEJTJDJTIyZ2xvYmFscyUyMiUzQSU3QiU3RCU3RCUyQyUyMmV4YW1wbGUlMjIlM0FudWxsJTdE))
输入:(React.createElement
形式)
// main.js
import Math from './maths.js';
console.log( Math.cube( 5 ) ); // 125
// maths.js
const square = function (x) {
return x * x;
};
const cube = function (x) {
return x * x * x;
}
export { square, cube };
export default {
square,
cube
}
输出:
// maths.js
const square = function (x) {
return x * x;
};
const cube = function (x) {
return x * x * x;
};
var Math = {
square,
cube
};
/* TREE-SHAKING */
console.log( Math.cube( 5 ) ); // 125
输入:(JSX runtime
形式)
// main.js
import { cube } from './maths.js';
console.log( Math.cube( 5 ) ); // 125
// maths.js
const square = function (x) {
return x * x;
};
const cube = function (x) {
return x * x * x;
}
export { square, cube };
export default {
square,
cube
}
输出:
// maths.js
const cube = function (x) {
return x * x * x;
};
/* TREE-SHAKING */
console.log( cube( 5 ) ); // 125
在线查看完整代码](https://rollupjs.org/repl/?version=3.7.5&shareable=JTdCJTIybW9kdWxlcyUyMiUzQSU1QiU3QiUyMm5hbWUlMjIlM0ElMjJtYWluLmpzJTIyJTJDJTIyY29kZSUyMiUzQSUyMiUyRiolMjBUUkVFLVNIQUtJTkclMjAqJTJGJTVDbmltcG9ydCUyME1hdGglMjBmcm9tJTIwJy4lMkZtYXRocy5qcyclM0IlNUNuJTVDbmNvbnNvbGUubG9nKCUyME1hdGguY3ViZSglMjA1JTIwKSUyMCklM0IlMjAlMkYlMkYlMjAxMjUlMjIlMkMlMjJpc0VudHJ5JTIyJTNBdHJ1ZSU3RCUyQyU3QiUyMm5hbWUlMjIlM0ElMjJtYXRocy5qcyUyMiUyQyUyMmNvZGUlMjIlM0ElMjIlMkYlMkYlMjBtYXRocy5qcyU1Q24lNUNuY29uc3QlMjBzcXVhcmUlMjAlM0QlMjBmdW5jdGlvbiUyMCh4KSUyMCU3QiU1Q24lNUN0cmV0dXJuJTIweCUyMColMjB4JTNCJTVDbiU3RCUzQiU1Q24lNUNuY29uc3QlMjBjdWJlJTIwJTNEJTIwZnVuY3Rpb24lMjAoeCklMjAlN0IlNUNuJTVDdHJldHVybiUyMHglMjAqJTIweCUyMColMjB4JTNCJTVDbiU3RCU1Q24lNUNuZXhwb3J0JTIwJTdCJTIwc3F1YXJlJTJDJTIwY3ViZSUyMCU3RCUzQiU1Q24lNUNuZXhwb3J0JTIwZGVmYXVsdCUyMCU3QiU1Q24lMjAlMjBzcXVhcmUlMkMlNUNuJTIwJTIwY3ViZSU1Q24lN0QlMjIlMkMlMjJpc0VudHJ5JTIyJTNBZmFsc2UlN0QlNUQlMkMlMjJvcHRpb25zJTIyJTNBJTdCJTIyZm9ybWF0JTIyJTNBJTIyZXMlMjIlMkMlMjJuYW1lJTIyJTNBJTIybXlCdW5kbGUlMjIlMkMlMjJhbWQlMjIlM0ElN0IlMjJpZCUyMiUzQSUyMiUyMiU3RCUyQyUyMmdsb2JhbHMlMjIlM0ElN0IlN0QlN0QlMkMlMjJleGFtcGxlJTIyJTNBbnVsbCU3RA==))
略微改善是为什么?
React
在优势上的措辞非常谨慎,它说
可能会 略微改善 bundle 的大小
这是因为,无论是 ESM
还是 CJS
对同一模块的导入,都是存在缓存的,也就是说你在 A
组件全量引用了 React
,在 B
组件再引用一次是不会在打包文件中再加上一个 React
,并不是 1 + 1 的关系
// A
import React from 'react';
function App() {
return <h1>Hello World</h1>;
}
// B
import React from 'react';
function App() {
return <h1>Hello World</h1>;
}
// React 只会在磁盘被加载一次,往后都是缓存
性能优化和简化
其实 JSX runtime
对于 React.createElement
还有一定的性能优化和简化,但文章中没讲,所以我也不讲(绝对不是因为我菜而看不懂),相关链接 - 性能优化和简化