blogs icon indicating copy to clipboard operation
blogs copied to clipboard

1. jsx-runtime

Open luokuning opened this issue 3 years ago • 0 comments

开始

React v17 增加了 jsx-runtime 机制,摒弃了过去通过 React.CreateElement 创建 React element 的方式。以 <h1>Poem</h1> 为例,在 React v16 及其之前,上面的 JSX 会被转译成:

import React from 'React';
const a = React.createElement("h1", null, "Poem");

而在 React v17 之后,通过正确的 transpiler 配置(以 tsc 为例,compilerOptions.jsx 的值需要是 ReactJSX),会被转译成:

import { jsx as _jsx } from "react/jsx-runtime";
const a = _jsx("h1", { children: "Poem" });

乍一看似乎只是换了个函数,但是有几点需要引起注意。

首先是两个函数的签名。React.createElement 接收的参数为 (type, props, ...children), _jsx 接收的参数为 (type, config, maybeKey)。新的 _jsx 函数在创建 React element 的时候,把 children 属性放进了 props (config) 里,而把 key 独立了出来。 第二个变化是,原先 createElement 是 React 的一个方法,因此在写 JSX 的时候,我们通常还需要通过 import React from 'react' 在当前模块中引入 React。而在 React v17 之后,配合正确的 transpiler 配置,不再需要显示引入 React,transpiler 会自动根据配置引入对应的 jsx 函数。

React.createElement 改成 react/jsx-runtime 带来的好处主要有两个:

  1. 不再显示引入 React 到当前模块。
  2. 因为 _jsxCreateElement 更简短而且更可压缩替换,打包完成之后 JavaScript bundle 的体积将会稍微减少一些。

Warning React.createElement 方法并没有从代码里去掉,React v17 可以继续向下兼容。更何况我们还可以在代码里手动调用 React.createElement 来创建 React Element。

_jsx 与 _jsxs

再来看一个在项目中非常常用的、动态创建 React element 的例子:

const App = () => {
  return (
    <div className="outer">
      <span>Header</span>
      <div className="inner">
        {[1, 2].map(num => (
          <p>{num}</p>
        ))}
      </div>
    </div>
  )
}

上面的代码会被转译成:

const App = () => {
  return _jsxs(
    'div',
    Object.assign(
      { className: 'outer' },
      {
        children: [
          _jsx('span', { children: 'Header' }),
          _jsx(
            'div',
            Object.assign({ className: 'inner' }, { children: [1, 2].map(num => _jsx('p', { children: num })) })
          ),
        ],
      }
    )
  )
}

不仔细看的话可能会忽略 div.outer 元素是通过 _jsxs 而不是 _jsx 创建的。_jsxs_jsx 的区别在于,前者应该只对 children 为“静态”数组的元素调用,后者则是对元素的 children 属性为非数组或者只为“动态”数组时调用。

这里所谓的静态与动态,指的是子元素是否会在每次渲染时,存在动态排序、增删的情况。很明显,div.outer 具有两个在源码中就有固定顺序的子元素。而 div.inner 的子元素则是通过表达式动态生成的,每次 App 组件渲染时,div.inner 的子元素与上次相比可能交换过了顺序,或者删除、增加了某些子元素。

校验 key

我们都知道 React 要求开发者为每个动态生成的子元素手动增加一个 key 属性,相当于给这些子元素赋予一个固定的 ID,以便能够让 React 能够在同级元素中检测到哪些元素被移位、删除和新增。之所以上面的代码里会需要区别静态与动态子元素,并使用两个不同的方法来生成元素,就是为了校验子元素的 key 属性。

下面简单分析下具体的代码细节。两个函数内部都只调用了 jsxWithValidation 函数,区别在于传入的参数。先看看 jsxWithValidation 函数能接收的参数:(type, props, key, isStaticChildren, source, self)_jsxs_jsx 区别只在于传入的 isStaticChildren 的值,前者是 true,后者是 falsejsxWithValidation 中只在一个地方使用了 isStaticChildren

if (isStaticChildren) {
  if (isArray(children)) {
    for (let i = 0; i < children.length; i++) {
      validateChildKeys(children[i], type);
    }

    if (Object.freeze) {
      Object.freeze(children);
    }
  } else {
    console.error(
      'React.jsx: Static children should always be an array. ' +
        'You are likely explicitly calling React.jsxs or React.jsxDEV. ' +
        'Use the Babel transform instead.',
    );
  }
} else {
  validateChildKeys(children, type);
}

这段代码意图很清晰:如果 children 是静态数组,对 children 中的每个元素进行 key 的校验;如果 children 不是数组,或者是动态数组,那么对整个 children 做校验。validateChildKeys 函数会判断传入的 children/child 是否是数组,如果是数组则会校验数组中的每个元素是否有合法的 key 属性。

Note 在生产环境中,_jsxs_jsx 其实都指向同一个函数:jsxProd, 省略了几乎所有的校验。

React Element

_jsxs_jsx 函数其实更多的只是在开发环境中做一些校验,真正重要的是它的返回值。

前面我提到过几次 React element,这个概念似乎有点抽象又有点跟其他概念混淆。简单来讲的话,React element 就是 React component 的调用返回值。以上面的代码为例,App 是 React component,a 是 React element。

_jsxs_jsx 除了校验一些参数之外,还调用了一个关键函数 jsx (开发环境的话是 jsxDEV。没错,jsxDEVjsx 的主要区别也是会做更多的校验),而 jsx 的返回值就是 React element。

每个 React element 都只是一个带有几个特殊属性的字面量对象而已:

// packages/shared/ReactElementType.js 里的 flow 类型定义

export type ReactElement = {
  $$typeof: any,
  type: any,
  key: any,
  ref: any,
  props: any,
  // ReactFiber
  _owner: any,

  // __DEV__
  _store: {validated: boolean, ...},
  _self: React$Element<any>,
  _shadowChildren: any,
  _source: Source,
};

下面这张图里表示的是 <button onClick={addCount}>Add</button> 对应的 React element 对象: image

总结

  1. _jsx_jsxs 的区别在于能够校验动态生成的 childrenkey 属性,在生产环境版本的 React 中,两者指向同一个函数。
  2. 理一下调用关系:
graph TD;
    _jsx/_jsxs --> jsxWithValidation --> jsx --> ReactElement
  1. 开发环境调用的函数版本包含很多的校验和错误提示,React 中其他地方的很多函数也是如此,这也是为什么 dev 环境的 React 应用性能比 prod 环境的要差很多。
  2. 函数式组件的 defaultProps 是在 jsx 函数中 merge 到 props 中的:
// Resolve default props
if (type && type.defaultProps) {
  const defaultProps = type.defaultProps;
  for (propName in defaultProps) {
    if (props[propName] === undefined) {
      props[propName] = defaultProps[propName];
    }
  }
}

Quiz

判断下面 console.log 语句输出内容的顺序

const Button = (props) => {
  console.log(2)
  return <button onClick={props.onClick}>Add</button>
}

const App = () => {
  const [count, setCount] = useState(0)
  const addCount = () => {
    setCount(count + 1)
  }
  return (
    <div>
      {console.log(1)}
      Count: {count}
      <Button onClick={addCount} />
      {console.log(3)}
    </div>
  )
}
Answer

顺序为: 1 3 2

其实只要能想象到 JSX 被转译成普通的 JavaScript 代码的样子,就能知道答案。App 组件被转译成:

const App = () => {
  const [count, setCount] = useState(0)
  const addCount = () => {
    setCount(count + 1)
  }
  return _jsxs('div', {
    children: [console.log(1), 'Count: ', count, _jsx(Button, { onClick: addCount }), console.log(3)],
  })
}

这里 _jsx(Button, { onClick: addCount }) 调用完之后只会返回一个 React element 对象,并没有执行 Button 函数。

内容不错或者比较美观的文章

React 17 introduces new JSX transform JSX.Element vs ReactElement vs ReactNode

luokuning avatar Nov 16 '22 08:11 luokuning