简单聊聊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 的目标了

相关文章
相关标签/搜索