本文爲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 的目標了