关于react.js:翻译翻译什么叫JSX

前言

什么叫JSX?,咱们能够先从 React 官网 的定义窥见一二。其中讲到JSX 能够很好地形容 UI 应该呈现出它应有交互的实质模式,即能够让咱们写 React 组件像在写 HTML 一样。除了相似原生的HTML标签外,咱们还能够自定义本人的组件,进而一步步形成简单的页面。那么在这一过程中React是如何解决JSX的,咱们将在接下来一一拆解,透过景象来开掘背地的实质。

本文源码 feature/jsx ,在scripts中提供了三个命令:

## 须要import React,index.js
yarn dev or npm run dev
## 不须要import React,index-jsx-runtime.js
yarn runtime or npm run runtime
## 自定义jsx-runtime, index-feact-jsx-runtime.js
yarn feact or npm run feact

import React from ‘react’

React17 以前,如果咱们不显式调用import React from 'react'

那么在页面上必定会报如下谬误

当然在咱们刚开始用React的时候,前辈、书里都说过必须在顶部显示import React from 'react',长此以往,咱们都会条件反射式地写上这么一句。可是你有没有想过,为何必须这样做页面才不会报错正告呢?明明我上面都没用到React相干的啊!

而且你还发现,每次你写上import React from 'react',即便发现代码里都没有用到React相干的,然而vscode外面的import React就会高亮,而比照上面的import eslint就是暗色的,并且提醒申明了但没应用

诶,下面提到的两个问题为何那么诡异?上面,让咱们化身福尔摩斯来一一揭秘吧~

React.createElement

React 官网 JSX 示意对象 有一句话:Babel 会把 JSX 转译成一个名为  React.createElement()  函数调用。翻译翻译,就是说咱们写的组件中用到了JSX,那么转译的过程中Babel就会去找React.createElement,将JSX转译为相应的对象。

举个 🌰:

function App() {
  return <div className='.app'>app</div>
}

console.log('App: ', App)
console.log('<App/>: ', <App/>)

下面如果间接输入 App,那么 App 实质上就是一个函数,而如果是这样的用法<App/>,即调用了组件,咱们能够在控制台看到看到两种的区别

Babel会将<App/>转译为上面的代码:

React.createElement(
  ƒ App(),  // type, ƒ App()示意函数的意思
  {},       // config
  undefined // children
);

而之后转译div,也会转译为上面代码:

React.createElement(
  'div',                 // type
  { className: ".app" }, // config
  'app'                  // children
);

那咱们看下 createElement 的实现

export function createElement(type, config, children) {

  let propName;

  // Reserved names are extracted
  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  if (config != null) {
    // 1.这里验证了ref和key如果都符合要求,会拦挡这两者
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }
    // 以下省略了__self和__source
    ...
    // 2.解决除了ref和key的props
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }
  // 3.解决children
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }

    props.children = childArray;
  }

  // 4.解决defaultProps
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  // 5.最初调用ReactElement
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

咱们剖析下面的代码过程

1. 拦挡 ref 和 key

if (hasValidRef(config)) {
  ref = config.ref;
}
if (hasValidKey(config)) {
  key = '' + config.key;
}

如果refkey符合要求,那么将两者独自取出来,而不会放到上面的props,这也验证了咱们在父组件传给子组件refkey的 prop 时,在子组件是取不到的,因为一开始就被拦挡掉了

2. 解决除了 ref、key、__self 和__source 的 props

for (propName in config) {
  if (
    /**
     * hasOwnProperty.call(config, propName)的意思就是只取传给组件的prop,而不取继承而来的
     * RESERVED_PROPS即蕴含key、ref、__self、__source,后两个只用于开发环境,能够疏忽
     */
    hasOwnProperty.call(config, propName) &&
    !RESERVED_PROPS.hasOwnProperty(propName)
  ) {
    props[propName] = config[propName];
  }
}

3. 解决 children

const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
   props.children = children;
} else if (childrenLength > 1) {
   const childArray = Array(childrenLength);
   for (let i = 0; i < childrenLength; i++) {
       childArray[i] = arguments[i + 2];
   }
   props.children = childArray;
}

咱们看createElement(type,config,children)接管了三个参数,但实际上一个父节点能够有多个子节点,如上面的div有两个子节点

  <div className=".app">
    <span>span1</span>
    <span>span2</span>
  </div>

那么Babel会转化为如下代码

React.createElement(
  'div', // type
  { className: ".app" }, // config
  {
    $$typeof: Symbol(react.element),
    key: null,
    props: {children: 'span1'},
    ref: null,
    type: "span",
  },
  {
    $$typeof: Symbol(react.element),
    key: null,
    props: {children: 'span2'},
    ref: null,
    type: "span"
  }
);

也就是说,如果有多个 child,那么会在第二个参数后按程序传入每个 child(这里会先用createElement解决子节点,而后再解决父节点,所以咱们能够看到传入的 child 曾经解决好了)

所以上面的代码才用arguments的长度减 2 来获取真正传入 child 的数量

const childrenLength = arguments.length - 2;

当然这里能够用 es6 的...children来代替

之后判断 child 数量,如果只有一个,间接就赋值给props.children,否则将所有的child放入数组,再放到props.children

if (childrenLength === 1) {
   // child数量只有一个,间接就赋值给props.children
   props.children = children;
} else if (childrenLength > 1) {
   // 否则将所有的`child`放入数组
   const childArray = Array(childrenLength);
   for (let i = 0; i < childrenLength; i++) {
       childArray[i] = arguments[i + 2];
   }
   props.children = childArray;
}

4. 解决 defaultProps

如果有传入了defaultProps,比方

MyComponent.defaultProps = { prop1: 'x', prop2: 'xx' ...}

如果没传给组件prop,或者传了但值为undefined,那么判断到有传defaultProps,会去取defaultProps上对应的prop

if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

咱们看到下面是用 === 来判断的,则如果咱们给组件传prop1为 null,那么组件外面失去的会是 null,而不是defaultProps上的prop1: 'x'

5. 最初调用 ReactElement

ReactElement 代码不多,就是将传入参数组合成一个element对象并返回

const ReactElement = function(type, key, ref, self, source, owner, props) {
  // self, source用于DEV,这里略过
  const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
  };

  return element;
};
  • $$typeof: REACT_ELEMENT_TYPE代表 ReactElement 类型,本质是一个Symbol值,为Symbol.for('react.element')
  • type即为组件类型,对于原生 dom,type则是对应的tag,如div, span等,对于函数组件FnComp、类组件ClassComp则是对应的函数或类,如ƒ FnComp()class ClassA

import React为何为高亮

这是因为在(j|t)sconfig.json外面默认有个配置:

如果你改为本人写的如Feact.createElement,如"jsxFactory": "Feact.createElement",那么咱们能够看到原先的import React就不会高亮了

不过以上只是在编辑器层面上的显示作用,要真正起作用还是得配置Babel

jsx-runtime

React17 提供一个全新版本的 JSX 转换,即不必再显式import React,起因是ReactBabel单干推出了 @babel/plugin-transform-react-jsx,该plugin会默认到react目录下的 jsx-runtime.js 文件读取相应的jsxjsxs等函数来转译相应的JSX

jsx 函数根本与createElement雷同,但有一点要留神,在createElement外面要解决children,而在jsx外面,会间接在config.children外面失去children,省去了解决环节

function App() {
  return <div className=".app">
    <span>span1</span>
    <span>span2</span>
  </div>
}

自定义你本人的 jsx-runtime

比方你之后感觉本人行了,想写一个react-like库,比方feact,那你能够在 feact/jsx-runtime.js 下提供对应的jsx, jsxs

而后必须在 webpack.config.js 外面给@babel/plugin-transform-react-jsx加上importSourcefeact

plugins: [
    [require.resolve('@babel/plugin-transform-flow-strip-types')],
    ['@babel/plugin-transform-react-jsx',{runtime:'automatic', importSource: 'feact'}]
]

总结

  1. 😯,原来在 React17 之前必须显式 import React 是因为 Babel 会默认将 JSX 通过 React.createElement 转译,如果不引入 React ,那天然就取不到 createElement ,也就天然会报错了
  2. 😯,原来JSX 会被 Babel 转译为如下对象
{
    $$typeof: Symbol(react.element),
    key: null,
    props: {children: 'span1'},
    ref: null type: "span",
 }
  1. 😯,原来Babel 会先转译子节点,再转译父节点
  2. 😯,原来 React17 不必 import React 是因为通过 @babel/plugin-transform-react-jsx 去主动引入 react/jsx-runtime.js 外面的 jsx、jsxs
  3. 😯,原来要自定义 jsx-runtime ,能够加上配置 importSource: 'YourPackage' ,并在 YourPackage/jsx-runtime 下 export 出 jsx、jsxs

6) 😯,原来在 createElement 外面要解决 children,而在 jsx 外面,会间接在 config.children 外面失去 children

最初

感激留下脚印,如果您感觉文章不错 😄😄,还请动动手指 😋😋,点赞+珍藏+转发 🌹🌹

往期让 React 飞系列

翻译翻译,什么叫 ReactDOM.createRoot

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理