React 17.0.0-RC.2 was released recently and brings new features about JSX:

  • withjsx()Function to replaceReact.createElement()
  • Automatically is introduced intojsx()function

An example is the following code

// hello.jsx

const Hello = () = > <div>hello</div>;
Copy the code

Will be translated into

// hello.jsx

import {jsx as _jsx} from 'react/jsx-runtime'; // Automatically imported by the compiler

const Hello = () = > _jsx('div', { children: 'hello' }); / / not React. The createElement method
Copy the code

React provides instructions on how to use the new translation syntax and automatic migration tools

I’m all for this change. What interests me most, though, is why use the new conversion syntax? The reason can be found in the React RFC-0000 documentation.

motivation

React was originally designed around class components, but function components have become increasingly popular thanks to hooks. Some of the design considerations for class components are not appropriate for functional components, and new concepts must be introduced for developers to understand.

For example 🌰, the ref feature is normal for class components, and we can get an instance of a class component by using ref. It makes no sense to pass ref to function components that do not have hooks in front of them, as we all know function components have no instances. Function components behave almost exactly like class components after hooks are implemented, and React provides useImperativeHandle() hooks that allow function components to expose their own methods to their parent. However, we can’t easily do this because React handles refs.

React blocks the REF attribute in the Props object, and then mounts and unmounts the ref attribute itself. For functional components, however, this mechanism is a little out of place. Because of the blocking, you can’t get the ref from props, you have to somehow tell React I need the ref, so React introduces the forwardRef() function to do that.

// For function components, we want to do this

const Input = (props) = > <input ref={props.ref} /> // error props. Ref is undefined

// But now we have to write it this way

const Input = React.forwardRef((props, ref) = > <input ref={ref} />)
Copy the code

So RFC-0000 proposed to revisit some of the original designs to see if they could be simplified

React.createElement()The problem of

React. CreateElement () was a balanced choice when React implemented the JSX solution. At that point, it worked fine, and many alternatives didn’t show enough advantage to replace it

Creating ReactElement with reac.createElement () in a React application is a very frequent operation, because the corresponding ReactElement is recreated each time it is rerendered

As technology evolved, the react.createElement () design exposed a number of problems:

  • Each timeReact.createElement(), dynamically check whether a component exists.defaultPropsProperty, which causes the JS engine to be unable to optimize this because the logic is highly complex
  • .defaultProps 对 React.lazyIt doesn’t work. Because the default assignment to the component props occurs atReact.createElement()During the period,lazyNeed to wait for asynchronous componentsresolved. This causes React to have to assign the props object by default at render time, which makeslazyThe component’s.defaultPropsIs inconsistent with the semantics of other components
  • Children is passed in dynamically as a parameter, so its shape cannot be determined directly, so it must be inReact.createElement()And put it all together
  • callReact.createElement()Is a dynamic attribute lookup process, rather than a variable lookup confined to the module, which requires additional cost to perform the lookup operation
  • The props object passed could not be sensed if it was a mutable object created by the user, so it must be cloned again
  • key å’Œ refAll from the props object, so if we don’t clone a new object, we have to clone it on the props object we passeddelete 掉 key å’Œ refProperty, however, which makes the props object map-like, which is bad for engine optimization
  • key å’Œ refCan be achieved by.The extension operator is passed, which makes it impossible to determine this without complex parsing<div {... props} />Mode, there is no transferkey å’Œ ref
  • JSX translation functions depend on variablesReactExists in scope, so you must import the module’s default exports

In addition to performance considerations, RFC-0000 will make it possible to simplify or eliminate React concepts such as forwardRef and defaultProps in the near future, reducing the cost of getting started

In addition, in order to standardize JSX syntax one day, it is necessary to decouple JSX from React coupling

Changes to the JSX transformation process

Automatic introduction (already installed)

React is no longer introduced into the scope manually, but is introduced automatically by the compiler

function Foo() {
  return <div />;
}
Copy the code

Will be translated into

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

Pass key as a parameter (installed)

To understand what this means, we need to look at the function signatures of the new conversion functions JSX and jsxDev:

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

As you can see, passing key as a parameter means exactly what it means. For better understanding, let’s look at the example 🌰

// 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>;
Copy the code

For the above code, the traditional transformation would translate into the following code

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"));
Copy the code

For the new translation process, the following code is translated

import { createElement as _createElement } from "react";
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
// Note that all of the above code is automatically introduced
const props = {
  value: 0.key: 'bar'
};

_jsxs(
  "div",
  {
    ...props,
    children: [_jsx("span", {
      children: "hello"
    }), _jsx("span", {
      children: "world"}})],"foo".// as an argument
);

_createElement(
  "div",
  {
    ...props,
    key: "foo" // Still as part of props
  }, _jsx("span", {
    children: "hello"
  }), _jsx("span", {
    children: "world"}));Copy the code

You can see that for

In fact, the compatible code is very simple, so let’s look at it a little bit

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. 
      
// or
). We want to deprecate key spread,
// but as an intermediary step, we will use jsxDEV for everything except //
, 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}}Copy the code

The compatibility logic is very simple, and in the notes we can see the detailed explanation, and we are now in an “intermediary step”

Pass Children as props

Let’s take a look at some examples, and then we’ll talk about why we’re doing this. Okay

For the following code

const a = 1;
const b = 1;

<div>{a}{b}</div>;
Copy the code

The traditional transformation will translate to

const a = 1;
const b = 1;
React.createElement(
		"div".null,
    a,
    b,
);
Copy the code

New transformation logic

import { jsxs as _jsxs } from "react/jsx-runtime";
const a = 1;
const b = 1;

_jsxs("div", {
  children: [a, b] / / here
});
Copy the code

The traditional conversion process would be to pass Children as function parameters, but that would require the conversion function to assemble an array of parameters internally

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);
}
Copy the code

This step can be very costly, especially since creating ReactElement is a frequent operation, which makes the performance penalty even worse

By passing Children as props, we can know the shape of Children in advance! Without having to go through another expensive splicing operation

Transformation logic in DEV mode

Function jsxDEV(type, config, maybeKey, source, self) is implemented for DEV mode. Function jsxDEV(type, config, maybeKey, source, self) The difference is that DEV mode passes two more arguments source and self

Always open (loaded)

Traditional translation logic has a special pattern. In most cases, traditional translation processes clone props, but

mode. The traditional mode translates to React. CreateElement (‘div’, props). Because createElement internally clones props, this translation is harmless

React doesn’t want to clone props in the new JSX conversion function, so

will always translate to JSX (‘div’, {… props})

The end of the

In addition, rfC-0000 also describes the changes to ref, forwardRef,.defaultprops and other related concepts.

Even the latest JSX transformation logic is actually an intermediate process with a lot of compatibility code, and the ultimate goal of RFC-0000 is to implement JSX () functions as follows

function jsx(type, props, key) {
  return {
    $$typeof: ReactElementSymbol,
    type,
    key,
    props,
  };
}
Copy the code

Similarly, we can look at the implementation logic of the JSX () function in production mode

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 ina) 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 }
}
Copy the code

You can see that the implementation is now very close to the RFC-0000 goal