Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1. jsx-runtime #19

Open
luokuning opened this issue Nov 16, 2022 · 0 comments
Open

1. jsx-runtime #19

luokuning opened this issue Nov 16, 2022 · 0 comments
Labels

Comments

@luokuning
Copy link
Owner

luokuning commented Nov 16, 2022

开始

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
Loading
  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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant