簡單聊聊React17-RC.2 新的 JSX 轉換邏輯

本文爲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 引擎沒法對這點進行優化,由於這段邏輯是高度復態的

  • .defaultPropsReact.lazy 不起做用。由於爲對組件 props 進行默認賦值的操做發生在React.createElement() 期間,而 lazy 須要等候異步組件 resolved 。這致使了 React 必需要在渲染時對 props 對象進行默認賦值,這使得 lazy 組件的 .defaultProps 的語義與其餘組件的不一致

  • Children 是做爲經過參數動態傳入,所以不能直接肯定它的形狀,因此必須在React.createElement() 內將其拼合在一塊兒

  • 調用 React.createElement() 是一個動態屬性查找的過程,而非侷限在模塊內部的變量查找,這須要額外的成原本進行查找操做

  • 沒法感知傳遞的 props 對象是否是用戶建立的可變對象,因此必須將其從新克隆下

  • keyref 都是從 props 對象中拿到的,若是咱們不克隆新的對象,就必須在傳遞的 props 對象上 deletekeyref 屬性,然而這會使得 props 對象變成 map-like ,不利於引擎優化

  • keyref 能夠經過 ... 擴展運算符進行傳遞,這使得若是不通過複雜的語法分析,就沒法判斷這種 <div {...props} /> 模式下,有沒有傳遞 keyref

  • jsx 轉譯函數依賴變量 React 存在做用域內,因此必須導入模塊的默認導出內容

除了性能上的考量以外,RFC-0000 使得在不遠的未來能夠將 React 的一些概念給簡化或剔除掉,好比 forwardRefdefaultProps ,減小開發者的理解上手成本

除此以外,爲了將來有一天標準化 jsx 語法,就必須將 jsx 與 React 耦合的地方解耦掉

JSX 轉換流程的變化

自動引入(已實裝)

再也不須要手動引入 React 到做用域,而是由編譯器自動引入

function Foo({
  return <div />;
}

複製代碼

會被轉譯爲

import {jsx} from "react";
function Foo({
  return jsx('div', ...);
}

複製代碼

將 key 看成參數傳入(已實裝)

爲了理解這點是什麼意思,咱們須要看看如今新的轉換函數 jsxjsxDev 的函數簽名:

  • function jsxDEV(type, config, maybeKey, source, self)

  • function jsx(type, config, key)

能夠看到,所謂的將 key 看成參數傳入的意思和字面意思同樣,爲了更好的理解,咱們再來看個例子🌰

// test.jsx

const props = {
  value0,
  key'bar'
};

<div key="foo" {...props}>     <span>hello</span>     <span>world</span> </div>;

<div {...propskey="foo">     <span>hello</span>     <span>world</span> </div>;

複製代碼

對於上述代碼,傳統轉換會轉譯成以下代碼

const props = {
    value0,
    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 = {
  value0,
  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」 中

將 Children 看成 props 傳遞(已實裝)

咱們先看看例子,而後再說說爲何要這麼作

對於下述代碼

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形狀 而不用再經歷一次昂貴的拼合操做

DEV 模式下的轉換邏輯

爲了幫助開發者調試,React 在 DEV 模式下有一些特殊的行爲,所以針對 DEV 模式實現了 function jsxDEV(type, config, maybeKey, source, self) 函數,從簽名上能夠看出來,區別就在於 DEV 模式下會多傳入兩個參數 sourceself

老是展開(已實裝)

傳統的轉換邏輯其實有一個特殊模式,大部分狀況下傳統的轉譯流程都會對 props 作一次克隆,可是對於 <div {...props} /> 模式,傳統模式會轉譯爲 React.createElement('div', props) 。由於 createElement 會在內部對 props 進行克隆,因此這種轉譯是無傷大雅的

React 官方不想在新的轉換函數 jsx 中實現對 props 的克隆邏輯,所以對於 <div {...props} /> 將老是會轉譯成 jsx('div', {...props})

結束

本文只是對 RFC-0000 有關 jsx 的部分作了說明,除此以外 RFC-0000 也說明有關 refforwardRef.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 的目標了

相關文章
相關標籤/搜索