本文为IVWEB团队成员所做, 原做者掘金帐号指路 -> 塔希html
React 17.0.0-rc.2 不久前发布,并带来了有关 JSX 新的特性:react
用 jsx()
函数替换 React.createElement()
git
自动引入 jsx()
函数github
举个例子就是如下代码typescript
// hello.jsx
const Hello = () => <div>hello</div>;
复制代码
会被转译为数组
// hello.jsx
import {jsx as _jsx} from 'react/jsx-runtime'; // 由编译器自动引入
const Hello = () => _jsx('div', { children: 'hello' }); // 不是 React.createElement
复制代码
React 官方提供了如何使用新的转译语法的说明和自动迁移的工具,详见文档markdown
对于这种变更,我举双手同意。不过,我最感兴趣的仍是,为何要用新的转换语法呢?在React 的 RFC-0000 文档中,咱们能够找到详细的解释。异步
React 最开始的设计是围绕着 class 组件来的,而随着 hooks 的流行,使得函数组件也变得愈来愈流行了。其中一些主要考虑 class 组件的设计放到函数组件上就变得不那么合适了,必须引入新的概念让开发者理解。函数
举个栗子🌰,好比 ref 这个特性面对 class 组件显得很正常,咱们经过 ref 可以拿到一个 class 组件的实例。对于出现 hooks 前的函数组件来说,咱们传递 ref 是没有意义的,众所周知,函数组件是没有实例的。可是在有了 hooks 以后,函数组件的行为和 class 组件几乎没区别了,而且 react 官方也提供了useImperativeHandle()
hook 让函数组件一样具有暴露自身方法到父组件的能力。可是咱们并不能很容易作到这点,这个和 React 处理 ref 的机制有关系。工具
React 关于 ref 的机制是这样的,React 会拦截掉 props 对象中 的 ref 属性,而后由 React 自己来完成相应挂载和卸载操做。可是对于函数组件来说,这个机制就显得有点不适宜了。由于拦截,你没法从props拿到ref,你必须以某种方式告诉react 我须要ref 才行,所以React 引入了 forwardRef()
函数来完成相关的操做。
// 对于函数组件,咱们想作到这样
const Input = (props) => <input ref={props.ref} /> // error props.ref 是 undefined
// 但咱们如今必须这样写
const Input = React.forwardRef((props, ref) => <input ref={ref} />)
复制代码
基于上述缘由,RFC-0000 提议从新审视当初的一些设计,看看可否进行一些简化
React.createElement()
的问题React.createElement()
是 React 当初实现 jsx 方案的一个相对平衡选择。在那个时候,它能够很好工做运行,而不少备选方案并无显示出足够的优点替换它
在一个 React 应用中经过React.createElement()
建立 ReactElement
是很是频繁的操做,由于每次重渲染时都要从新建立对应的ReactElement
随着技术的发展 React.createElement()
设计暴露出了大量的问题:
每次执行React.createElement()
时,都要动态的检测一个组件上是否存在.defaultProps
属性,这致使 js 引擎没法对这点进行优化,由于这段逻辑是高度复态的
.defaultProps
对 React.lazy
不起做用。由于为对组件 props 进行默认赋值的操做发生在React.createElement()
期间,而 lazy
须要等候异步组件 resolved
。这致使了 React 必需要在渲染时对 props 对象进行默认赋值,这使得 lazy
组件的 .defaultProps
的语义与其余组件的不一致
Children 是做为经过参数动态传入,所以不能直接肯定它的形状,因此必须在React.createElement()
内将其拼合在一块儿
调用 React.createElement()
是一个动态属性查找的过程,而非局限在模块内部的变量查找,这须要额外的成原本进行查找操做
没法感知传递的 props 对象是否是用户建立的可变对象,因此必须将其从新克隆下
key
和 ref
都是从 props 对象中拿到的,若是咱们不克隆新的对象,就必须在传递的 props 对象上 delete
掉 key
和 ref
属性,然而这会使得 props 对象变成 map-like ,不利于引擎优化
key
和 ref
能够经过 ...
扩展运算符进行传递,这使得若是不通过复杂的语法分析,就没法判断这种 <div {...props} />
模式下,有没有传递 key
和 ref
jsx 转译函数依赖变量 React
存在做用域内,因此必须导入模块的默认导出内容
除了性能上的考量以外,RFC-0000 使得在不远的未来能够将 React 的一些概念给简化或剔除掉,好比 forwardRef
和 defaultProps
,减小开发者的理解上手成本
除此以外,为了将来有一天标准化 jsx 语法,就必须将 jsx 与 React 耦合的地方解耦掉
再也不须要手动引入 React 到做用域,而是由编译器自动引入
function Foo() {
return <div />;
}
复制代码
会被转译为
import {jsx} from "react";
function Foo() {
return jsx('div', ...);
}
复制代码
为了理解这点是什么意思,咱们须要看看如今新的转换函数 jsx
和 jsxDev
的函数签名:
function jsxDEV(type, config, maybeKey, source, self)
function jsx(type, config, key)
能够看到,所谓的将 key 看成参数传入的意思和字面意思同样,为了更好的理解,咱们再来看个例子🌰
// test.jsx
const props = {
value: 0,
key: 'bar'
};
<div key="foo" {...props}> <span>hello</span> <span>world</span> </div>;
<div {...props} key="foo"> <span>hello</span> <span>world</span> </div>;
复制代码
对于上述代码,传统转换会转译成以下代码
const props = {
value: 0,
key: 'bar'
};
React.createElement("div", Object.assign({ key: "foo" }, props),
React.createElement("span", null, "hello"),
React.createElement("span", null, "world")
);
React.createElement("div", Object.assign({}, props, { key: "foo" }),
React.createElement("span", null, "hello"),
React.createElement("span", null, "world")
);
复制代码
而对于新的转译流程,会转译成以下代码
import { createElement as _createElement } from "react";
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
// 注意,上述代码全是自动引入的
const props = {
value: 0,
key: 'bar'
};
_jsxs(
"div",
{
...props,
children: [_jsx("span", {
children: "hello"
}), _jsx("span", {
children: "world"
})]
},
"foo", // 看成参数
);
_createElement(
"div",
{
...props,
key: "foo" // 依然做为 props 的一部分
}, _jsx("span", {
children: "hello"
}), _jsx("span", {
children: "world"
})
);
复制代码
能够看到对于 <div {...props} key="foo">
这种形式的新旧转换逻辑是一致的。jsx
函数同时支持这两种传入 key 的方式,这里主要是为了兼容考虑,React 提倡渐进式升级,因此如今暂时处于过渡阶段,最终将只会支持把 key
看成参数的传入方式
其实兼容的代码也很简单,这里咱们稍微看一下
function jsxDEV(type, config, maybeKey, source, self) {
{
var propName; // Reserved names are extracted
var props = {};
var key = null;
var ref = null; // Currently, key can be spread in as a prop. This causes a potential
// issue if key is also explicitly declared (ie. <div {...props} key="Hi" />
// or <div key="Hi" {...props} /> ). We want to deprecate key spread,
// but as an intermediary step, we will use jsxDEV for everything except
// <div {...props} key="Hi" />, because we aren't currently able to tell if
// key is explicitly declared to be undefined or not.
if (maybeKey !== undefined) {
key = '' + maybeKey;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
// ... codes
}
}
复制代码
兼容逻辑很简单,而且在注释中咱们能够看到详细的解释,已经咱们如今处于一个 “intermediary step” 中
咱们先看看例子,而后再说说为何要这么作
对于下述代码
const a = 1;
const b = 1;
<div>{a}{b}</div>;
复制代码
传统转换将转译成
const a = 1;
const b = 1;
React.createElement(
"div",
null,
a,
b,
);
复制代码
新的转换逻辑
import { jsxs as _jsxs } from "react/jsx-runtime";
const a = 1;
const b = 1;
_jsxs("div", {
children: [a, b] // 这里
});
复制代码
传统的传递 children 的流程是经过函数参数,可是这样就须要转换函数内部将参数拼合成一个数组
function createElement(type, config, children) {
// ... codes
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
{
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
} // Resolve default props
// ... codes
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}
复制代码
这一步操做实际上是很耗费性能的,特别是前文咱们提到建立 ReactElement
是一个很频繁的操做,这使得其中的性能损失变得更加严重
经过将 Children
做为 props 传递,咱们能够提早知道 Children
的 形状 而不用再经历一次昂贵的拼合操做
为了帮助开发者调试,React 在 DEV 模式下有一些特殊的行为,所以针对 DEV 模式实现了 function jsxDEV(type, config, maybeKey, source, self)
函数,从签名上能够看出来,区别就在于 DEV 模式下会多传入两个参数 source
和 self
传统的转换逻辑其实有一个特殊模式,大部分状况下传统的转译流程都会对 props 作一次克隆,可是对于 <div {...props} />
模式,传统模式会转译为 React.createElement('div', props)
。由于 createElement
会在内部对 props
进行克隆,因此这种转译是无伤大雅的
React 官方不想在新的转换函数 jsx
中实现对 props
的克隆逻辑,所以对于 <div {...props} />
将老是会转译成 jsx('div', {...props})
本文只是对 RFC-0000 有关 jsx 的部分作了说明,除此以外 RFC-0000 也说明有关 ref
, forwardRef
, .defaultProps
等相关概念的变动。
即便是最新 jsx 转换逻辑,其实也是处于一个中间态的过程,其实现依然有不少兼容性的代码,而 RFC-0000 的最终目标是将 jsx()
函数实现为以下逻辑
function jsx(type, props, key) {
return {
$$typeof: ReactElementSymbol,
type,
key,
props,
};
}
复制代码
一样的,咱们能够看下 production 模式下 jsx()
函数的实现逻辑
function q(c, a, k) {
var b, d = {}, e = null, l = null;
void 0 !== k && (e = "" + k);
void 0 !== a.key && (e = "" + a.key);
void 0 !== a.ref && (l = a.ref);
for (b in a) n.call(a, b) && !p.hasOwnProperty(b) && (d[b] = a[b]);
if (c && c.defaultProps) for (b in a = c.defaultProps, a) void 0 === d[b] && (d[b] = a[b]);
return { $$typeof: g, type: c, key: e, ref: l, props: d, _owner: m.current }
}
复制代码
能够看到如今实现已经很接近 RFC-0000 的目标了