开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第21天,点击查看活动详情
之前的一篇 基于 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 编译结果的[例子](rollup.js (rollupjs.org))
输入:(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
[在线查看完整代码](rollup.js (rollupjs.org))
略微改善是为什么?
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 还有一定的性能优化和简化,但文章中没讲,所以我也不讲(绝对不是因为我菜而看不懂),相关链接 - 性能优化和简化