likes
comments
collection
share

想想react会怎么做(前传)之 jsx编译JSX 的全称是 Javasrcipt and XML,是一个 JavaSc

作者站长头像
站长
· 阅读数 83

这一节其实和react核心库的代码并没有关系,算是前传。

JSX是什么

const element = <h1>Hello, world!</h1>

这个有趣的标签语法既不是字符串也不是 HTML,它是JSX

JSX 的全称是 Javasrcipt and XML,是一个 JavaScript 的语法扩展,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式。

但是react其实不强制要求使用jsx

例如,使用jsx的代码如下:

class Hello extends React.Component {
  render() {
    return <div>Hello {this.props.toWhat}</div>;
  }
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Hello toWhat="World" />);

不使用jsx的代码如下:

class Hello extends React.Component {
  render() {
    return React.createElement('div', null, `Hello ${this.props.toWhat}`);
  }
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(Hello, {toWhat: 'World'}, null));

看起来确实jsx要更加生动形象一些,所以我们react一般还是会使用jsx的

JSX编译是什么

所以其实JSX是一种语法糖,它最后都会被编译成方法的调用(React.createElement,jsx,jsxs方法),而这些函数就是来自react,这样就让jsx和react关联了起来。

我们可以使用babel playground来看看jsx编译之后的效果,打开babel plaground,使用react的presets

React runtime:classisc

React runtime使用classic的效果:

想想react会怎么做(前传)之 jsx编译JSX 的全称是 Javasrcipt and XML,是一个 JavaSc

可以看到左边的就是jsx,右边的就是jsx编译的结果,左边的jsx编译成了React.createElement的调用以及嵌套调用

React runtime:Automatic

然后我们再使用React runtime为Automatic:

想想react会怎么做(前传)之 jsx编译JSX 的全称是 Javasrcipt and XML,是一个 JavaSc

可以看到左边的jsx编译成了jsxs和jsx的调用和嵌套调用,还在上方从react/jsx-runtime中引入了jsx和jsxs方法

可以认为在react17之前jsx都是编译成React.createElement,之后就是编译成jsx,jsxs

两个runtime的区别

老版本(classisc)

会将jsx编译成React.createElement的调用,举个例子

源代码:

import React from 'react';
function App() {
    return <div name="abc">
      <div>123</div>
      <div>456</div>
    </div>;
 }

编译后

import React from 'react';
function App() {
  return /*#__PURE__*/React.createElement("div", {
    name: "abc"
  }, /*#__PURE__*/React.createElement("div", null, "123"), 
    /*#__PURE__*/React.createElement("div", null, "456"));
}

React.createElement的第一个参数为元素名,第二个参数为元素的参数,如果没有则为null,第三个参数及其之后为元素的子元素(有几个子元素就有几个参数)

这个编译结果还是有一些缺点(来自React官方文档)

  • 由于jsx被编译成React.createElement,所有导致当你使用jsx的时候,你必须手动引入React
  • 有一些性能优化和代码精简是React.createElement不允许的(我理解就是不方便通过修改React.createElement来做优化和精简)

新版本(automatic)

为了解决这些问题,react17之后,就会使用新版本的runtime对jsx进行编译

源代码

function App() {
    return <div name="abc" key="key1">
      <div>123</div>
      <div>456</div>
    </div>;
 }

编译后

// 这行import是编译器自动把你引入的,不需要你手动引入
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
function App() {
  return /*#__PURE__*/_jsxs("div", {
    name: "abc",
    children: [/*#__PURE__*/_jsx("div", {
      children: "123"
    }), /*#__PURE__*/_jsx("div", {
      children: "456"
    })]
  }, "key1");
}

jsx模版编译后变成了jsx方法调用,可以看到新版本源代码不需要再import React来使用jsx了。

如果子元素为多个时使用jsxs方法,单个时使用jsx方法

jsx/jsxs方法的第一个参数还是元素名

第二个参数是一个对象,包含所有的属性和子元素,子元素使用children数组表示

第三个参数是元素的key值,单独拎出来了

编译具体过程

这个编译过程就是指上述jsx->函数调用的过程

编译过程使用插件:@babel/plugin-transform-react-jsx。

git源码:github.com/babel/babel…

整体过程如图:

想想react会怎么做(前传)之 jsx编译JSX 的全称是 Javasrcipt and XML,是一个 JavaSc

createPlugin

文件目录:packages/babel-plugin-transform-react-jsx

可以看到src/index.ts导出了createPlugin方法,参数为name(插件名),development(运行环境)

想想react会怎么做(前传)之 jsx编译JSX 的全称是 Javasrcipt and XML,是一个 JavaSc

所以编译的关键逻辑就在create-plugin.ts文件的createPlugin方法之中

想想react会怎么做(前传)之 jsx编译JSX 的全称是 Javasrcipt and XML,是一个 JavaSc

declare

babel使用declare方法定义一个bable插件,declare方法的执行结果就是createPlugin的返回值

import { declare } from "@babel/helper-plugin-utils";

export default function createPlugin({
  name,
  development,
}: {
  name: string;
  development: boolean;
}) {
    return declare((_, options: Options) => {
        //...
    }
}

初始化

对插件的配置进行初始化,包括我们上述的runtime配置

declare((_, options: Options) => {
    // 配置初始化
    const {
      // defaults to true, Enables @babel/plugin-transform-react-pure-annotations. It will mark top-level React method calls as pure for tree shaking.
      pure: PURE_ANNOTATION,
      // defaults to true, 是否在使用XML名称空间标记名时抛出错误,虽然JSX规范允许这样做,但React的JSX目前不支持,所有要抛出错误
      throwIfNamespace = true,
      // 文档上没看到这个options,源码中说新版本runtime已经支持这个option,所以不管它了
      filter,
      // runtime配置:automatic | classic,在babel8之后默认值从classic改成了automatic
      runtime: RUNTIME_DEFAULT = process.env.BABEL_8_BREAKING
        ? "automatic"
        : development
          ? "automatic"
          : "classic",
      // automatic模式下配置,导入函数时替换的导入源,默认"react"
      importSource: IMPORT_SOURCE_DEFAULT = DEFAULT.importSource,
      // classic模式下配置,编译jsx时替换的函数,默认"React.createElement"
      pragma: PRAGMA_DEFAULT = DEFAULT.pragma,
      // classic模式下配置,当编译JSX fragments替换的组件,默认"React.Fragment"
      pragmaFrag: PRAGMA_FRAG_DEFAULT = DEFAULT.pragmaFrag,
    } = options;

    // babel版本判断处理
    if (process.env.BABEL_8_BREAKING) {
      // useSpread,useBuiltIns,filter参数不让使用
      // ...
    }else{
      // useSpread,useBuiltIns不是boolean报错,
      // useSpread,useBuiltIns不能同时存在
      // ...  
    }

}

解析与处理

declare方法返回的对象中我们可以看到解析与转化的具体逻辑了:

// 也是Babel的一个插件,主要作用是允许Babel解析JSX语法
import jsx from "@babel/plugin-syntax-jsx";

return declare((_, options: Options) => {
    // ...
    return {
      // 插件名
      name,
      //  继承@babel/plugin-syntax-jsx插件的功能
      inherits: jsx
      // 当前插件的遍历逻辑(通过visitor处理ast节点)
      visitor:{
      }
    }
}

为什么需要inherits来继承@babel/plugin-syntax-jsx库的能力?

  1. 语法解析: @babel/plugin-syntax-jsx 负责让 Babel 能够解析 JSX 语法。从而,任何对 JSX 进行转换的插件在能够转换之前都需要能解析 JSX。通过继承这个插件,babel-plugin-transform-react-jsx 获得了解析 JSX 的能力,不需要重新实现这个解析逻辑。
  2. 依赖性管理: inherits 排除了在 babel-plugin-transform-react-jsx 中直接包含和管理 JSX 语法解析逻辑的需要。对于 Babel 插件开发者来说,这意味着他们可以专注于转换逻辑本身,而不是如何解析 JSX。、

visitor作用?

我理解这里的visitor就是通过一个个回调和参数来获取和处理ast节点,其实和通过@babel/traverse工具来遍历某个代码片段的ast节点是一模一样的

处理ast节点

这里的要讲的就是visitor的具体逻辑了,这里列出来所有使用到的回调和参数

return declare((_, options: Options) => {
    // ...
    return {
      // ...
      // 当前插件的遍历逻辑(通过visitor处理ast节点)
      visitor:{
         JSXNamespacedName(path){
            //...
         },
         JSXSpreadChild(path){
            // ...
         },
         Program: {
            enter(path, state) {
              // ...
            }
         },
        JSXFragment: {
          exit(path, file) {
            // ... 
          },
        },
        JSXElement: {
          exit(path, file) {
            // ...
          },
        },
        JSXAttribute(path) {
          // ...
        },
      }
    }
}

JSXNamespacedName

JSXNamespacedName是 ast 节点的类型之一,指的是那些使用了命名空间命名的jsx节点

命名空间名称由冒号分隔,例如<my:component />,这里my是命名空间,component是实际的组件名称。

github.com/facebook/js…

但是react中不支持命名空间,所以在这里默认是直接抛出错误 React's JSX doesn't support namespace tags

JSXNamespacedName(path) {
  if (throwIfNamespace) {
    throw path.buildCodeFrameError(
      `Namespace tags are not supported by default. React's JSX doesn't support namespace tags. \
You can set `throwIfNamespace: false` to bypass this warning.`,
    );
  }
}

JSXSpreadChild

JSXSpreadChild 是 Babel AST 中用于描述 JSX 元素里的扩展子节点的类型,这个节点类型表示了在 JSX 元素的子节点中使用了扩展操作符(即 {...})来扩展表达式,例如:

// JSX 代码
<div>{...myVar}</div>

// 对应的 Babel AST
{
  type: "JSXSpreadChild",
  expression: {
    type: "Identifier",
    name: "myVar"
  }
}

而react中是不允许使用扩展子节点的,所以这个地方babel直接就抛出错误:"Spread children are not supported in React "

JSXSpreadChild(path) {
  throw path.buildCodeFrameError(
    "Spread children are not supported in React.",
  );
},

实际项目中使用报错如下: 想想react会怎么做(前传)之 jsx编译JSX 的全称是 Javasrcipt and XML,是一个 JavaSc

这里应该直接使用 {listItems}

但是不知道为什么,直播工程中使用SpreadChild就没有报错,可能进行了兼容处理?这个后续我再看看

Program

  • Program 对象代表了整个文件的 AST(抽象语法树)节点,通常在这里处理文件级别的转换逻辑

  • enter是插件进入 Program 节点时执行的回调

  • path:是一个包含了当前AST节点信息的对象,提供了操作当前节点和其子节点的方法

  • state: 是一个跨插件传递信息的对象,允许插件访问和共享整个转换过程中的状态。

    • 可以使用state.set(key, value)将值存在state中
    • 可以使用state.get(key)获取通过state.set(key,value)存储的值
    import { types as t } from "@babel/core";
    import { addNamed, addNamespace, isModule } from "@babel/helper-module-imports";

    // 匹配的正则
    const JSX_SOURCE_ANNOTATION_REGEX =
      /^\s**?\s*@jsxImportSource\s+([^\s]+)\s*$/m;
    const JSX_RUNTIME_ANNOTATION_REGEX = /^\s**?\s*@jsxRuntime\s+([^\s]+)\s*$/m;
    const JSX_ANNOTATION_REGEX = /^\s**?\s*@jsx\s+([^\s]+)\s*$/m;
    const JSX_FRAG_ANNOTATION_REGEX = /^\s**?\s*@jsxFrag\s+([^\s]+)\s*$/m;

    const get = (pass: PluginPass, name: string) =>
      pass.get(`@babel/plugin-react-jsx/${name}`);
    const set = (pass: PluginPass, name: string, v: any) =>
      pass.set(`@babel/plugin-react-jsx/${name}`, v);

    // ...
    Program:{
      enter(path, state) {
        const { file } = state;
        // 一些状态初始化
        let runtime: string = RUNTIME_DEFAULT;
        let source: string = IMPORT_SOURCE_DEFAULT;
        let pragma: string = PRAGMA_DEFAULT;
        let pragmaFrag: string = PRAGMA_FRAG_DEFAULT;
        let sourceSet = !!options.importSource;
        let pragmaSet = !!options.pragma;
        let pragmaFragSet = !!options.pragmaFrag;
        // file.ast.comments是当前文件中的所有注释节点
        if (file.ast.comments) {
          // 遍历注释节点数组
          for (const comment of file.ast.comments) {
            // 通过正则匹配 @jsxImportSource 注释
            const sourceMatches = JSX_SOURCE_ANNOTATION_REGEX.exec(
              comment.value,
            );
            if (sourceMatches) {
              source = sourceMatches[1];
              sourceSet = true;
            }
            // 通过正则匹配 @jsxRuntime 注释
            const runtimeMatches = JSX_RUNTIME_ANNOTATION_REGEX.exec(
              comment.value,
            );
            if (runtimeMatches) {
              runtime = runtimeMatches[1];
            }
            // 通过正则匹配 @jsx 注释
            const jsxMatches = JSX_ANNOTATION_REGEX.exec(comment.value);
            if (jsxMatches) {
              pragma = jsxMatches[1];
              pragmaSet = true;
            }
            // 通过正则匹配 @jsxFrag 注释
            const jsxFragMatches = JSX_FRAG_ANNOTATION_REGEX.exec(
              comment.value,
            );
            if (jsxFragMatches) {
              pragmaFrag = jsxFragMatches[1];
              pragmaFragSet = true;
            }
          }
        }

        // 将当前的runtime值存到state之中,babel8之后默认automatic
        set(state, "runtime", runtime);
        // 如果运行时是classic
        if (runtime === "classic") {
          // 如果importSource被设置,抛出错误
          if (sourceSet) {
            throw path.buildCodeFrameError(
              // 运行时是classic时不能设置importSourve
              `importSource cannot be set when runtime is classic.`,
            );
          }

          // 这里pragma默认React.createElement, pragmaFrag默认React.Fragment
          // 将React.createElement转换成MemberExpression节点
          const createElement = toMemberExpression(pragma);
          // 将React.Fragment转换成MemberExpression节点
          const fragment = toMemberExpression(pragmaFrag);

          // 将节点信息存进state中
          set(state, "id/createElement", 
              () => t.cloneNode(createElement));
          set(state, "id/fragment", () => t.cloneNode(fragment));

          // 保存defaultPure配置
          set(state, "defaultPure", pragma === DEFAULT.pragma);
          // 运行时为automatic时的逻辑
        } else if (runtime === "automatic") {
          // 运行时为automatic时不能设置pragma和pragmaSet
          if (pragmaSet || pragmaFragSet) {
            throw path.buildCodeFrameError(
              `pragma and pragmaFrag cannot be set when runtime is automatic.`,
            );
          }

          // 将创建好的导入语句的MemberExpression节点保存到state
          const define = (name: string, id: string) =>
            set(state, name, createImportLazily(state, path, id, source));

          define("id/jsx", development ? "jsxDEV" : "jsx");
          define("id/jsxs", development ? "jsxDEV" : "jsxs");
          define("id/createElement", "createElement");
          define("id/fragment", "Fragment");

          set(state, "defaultPure", source === DEFAULT.importSource);
        } else {
          throw path.buildCodeFrameError(
            `Runtime must be either "classic" or "automatic".`,
          );
        }
      },
    }

    // 将字符串形式的标识符转换为 ast 中的 MemberExpression 节点
    /** 
    * 具体步骤:
    * 1.分割传入的 id 字符串,使用点 . 作为分隔符,得到标识符数组。
    * 2.使用 map 函数遍历这个数组,并为每个标识符创建一个 Identifier 节点。
    * 3.使用 reduce 函数组合这些 Identifier 节点,构造一个嵌套的 
    * MemberExpression。
    * 示例:
    * 输入:React.creatElement
    * 输出:
    * {
    *  "type": "MemberExpression",
    *  "object": {
    *    "type": "Identifier",
    *   "name": "React"
    *  },
    *  "property": {
    *    "type": "Identifier",
    *    "name": "createElement"
    *  }
    * }
    */
    function toMemberExpression(id: string): 
      Identifier | MemberExpression {
      return (
        id.split(".")
          .map(name => t.identifier(name))
          .reduce((object, property) => 
            t.memberExpression(object, property))
      );
    }

    function createImportLazily(
        /** babel state */
        pass: PluginPass,
        /** babel path */
        path: NodePath<Program>,
        /** 需要导入的方法名,比如:jsx, jsxs, createElement, Fragment */
        importName: string,
        /** 导入源,默认react */
        source: string,
      ): () => Identifier | MemberExpression {
        return () => {
          // 获取实际导入源,默认是react/jsx-runtime
          const actualSource = getSource(source, importName);
          // 如果是Esmodule
          if (isModule(path)) {
            let reference = get(pass, `imports/${importName}`);
            // 如果已经有reference state,直接return出去
            if (reference) return t.cloneNode(reference);
            // 没有reference就计算,然后存入state
            /**
            * 这个reference代表对importName的引用
            * addNamed举个例子:
            * 当需要引用 useState 时,创建一个延迟执行的函数
            * const importUseState = createImportLazily(
            *   pass, path, "useState", "react"
            * );
            * 在插件的某个部分需要使用 useState 时,调用该函数
            * const useStateReference = importUseState();
            */
            reference = addNamed(path, importName, actualSource, {
              importedInterop: "uncompiled",
              importPosition: "after",
            });
            set(pass, `imports/${importName}`, reference);

            return reference;
          } else {
            // 如果不是Esmodule的导入逻辑
            let reference = get(pass, `requires/${actualSource}`);
            if (reference) {
              reference = t.cloneNode(reference);
            } else {
              reference = addNamespace(path, actualSource, {
                importedInterop: "uncompiled",
              });
              set(pass, `requires/${actualSource}`, reference);
            }

            return t.memberExpression(reference, t.identifier(importName));
          }
        };
      }

      // 通过拼接得到实际导入源,默认是react/jsx-runtime
      function getSource(source: string, importName: string) {
        switch (importName) {
          case "Fragment":
            return `${source}/${development ? "jsx-dev-runtime" : "jsx-runtime"}`;
          case "jsxDEV":
            return `${source}/jsx-dev-runtime`;
          case "jsx":
          case "jsxs":
            return `${source}/jsx-runtime`;
          // 如果是createElement直接return react
          case "createElement":
            return source;
        }
      }

所以在这里Program中的逻辑就是做一些数据准备,为后面的实例转换逻辑提供计算好的节点数据

JSXFragment

    JSXFragment: {
      // 插件即将离开 JSXFragment 节点时执行的逻辑
      exit(path, file) {
        let callExpr;
        // 生成编译Fragment元素的回调方法
        if (get(file, "runtime") === "classic") {
          callExpr = buildCreateElementFragmentCall(path, file);
        } else {
          callExpr = buildJSXFragmentCall(path, file);
        }
        // 执行该回调来对<></>进行替换
        path.replaceWith(t.inherits(callExpr, path.node));
      },
    },

    /** 生成将 <></> 编译成
    * React.createElement(React.Fragment, null, ...children)的回调
    */
    function buildCreateElementFragmentCall(
      path: NodePath<JSXFragment>,
      file: PluginPass,
    ) {
      return call(
        file, 
        // 获取state的key
        "createElement", 
        // 回调的参数
        [
          get(file, "id/fragment")(),
          t.nullLiteral(),
          ...t.react.buildChildren(path.node),
        ]
      );
    }

    /** 生成将 <></> 编译成
    * React.jsx(type, arguments)的回调
    */
    function buildJSXFragmentCall(
      path: NodePath<JSXFragment>,
      file: PluginPass,
    ) {
      const args = [get(file, "id/fragment")()];
      const children = t.react.buildChildren(path.node);
      args.push(
        t.objectExpression(
          children.length > 0
            ? [
                buildChildrenProperty(
                  children,
                ),
              ]
            : [],
        ),
      );

      // 如何子元素>1就编译成jsxs方法,反之编译成jsx
      return call(file, children.length > 1 ? "jsxs" : "jsx", args);
    }

    // 根据name得到对应的函数调用表达式
    function call(
          pass: PluginPass,
          name: string,
          args: CallExpression["arguments"],
        ) {
          // get(pass, `id/${name}`)()可以获取之前存储的函数引用
          const node = t.callExpression(get(pass, `id/${name}`)(), args);
          return node;
        }

JSXElement

    JSXElement: {
      exit(path, file) {
        let callExpr;
        // 得到编译jsx元素的回调
        if (
          get(file, "runtime") === "classic" ||
          shouldUseCreateElement(path)
        ) {
          callExpr = buildCreateElementCall(path, file);
        } else {
          callExpr = buildJSXElementCall(path, file);
        }
        // 执行回调进行替换
        path.replaceWith(t.inherits(callExpr, path.node));
      },
    },

    /** 
    *  生成将jsx元素编译成React.createElement(type,args,children)的回调
    */
    function buildCreateElementCall(
      path: NodePath<JSXElement>,
      file: PluginPass,
    ) {
      // openingElement 属性代表了一个 JSX 元素的开始标签(例如 <div>)
      const openingPath = path.get("openingElement");

      return call(file, "createElement", [
        // 根据元素开始标签获取tag名,比如说<div> -> "div"
        getTag(openingPath),
        // 获取元素props对象,比如<div a="1" b="2"> -> {a:"1",b:"2"}
        buildCreateElementOpeningElementAttributes(
          file,
          path,
          openingPath.get("attributes"),
        ),
        // 元素的children数组
        ...t.react.buildChildren(path.node),
      ]);
    }

    /** 
    *  生成将jsx元素编译成jsx(type,args,key)的回调
    */
    function buildJSXElementCall(
      path: NodePath<JSXElement>, 
      file: PluginPass) 
    {
      // 获取元素开始标签
      const openingPath = path.get("openingElement");
      // 参数列表,第一项为元素tag名
      const args: t.Expression[] = [getTag(openingPath)];
      // 属性列表
      const attribsArray = [];
      // 提取参数
      const extracted = Object.create(null);
          for (const attr of openingPath.get("attributes")) {
            if (attr.isJSXAttribute() && t.isJSXIdentifier(attr.node.name)) {
              const { name } = attr.node.name;
              switch (name) {
                case "__source":
                case "__self":
                  if (extracted[name]) throw sourceSelfError(path, name);
                // 如果是key,放入extracted参数中
                case "key": {
                  const keyValue = 
                    convertAttributeValue(attr.node.value);
      
                  extracted[name] = keyValue;
                  break;
                }
                // 默认把元素属性放入属性列表
                default:
                  attribsArray.push(attr);
              }
            } else {
              attribsArray.push(attr);
            }
          }

          // 把子元素也放入和属性列表合并,放到一起,例如:
          //{a:"1",b:"2",children:[]}
          const children = t.react.buildChildren(path.node);
          let attribs: t.ObjectExpression;
          if (attribsArray.length || children.length) {
            attribs = buildJSXOpeningElementAttributes(
              attribsArray,
              children,
            );
          } else {
            // attributes should never be null
            attribs = t.objectExpression([]);
          }
          args.push(attribs);
      
          // 如何子元素>1就编译成jsxs方法,反之编译成jsx
          return call(file, children.length > 1 ? "jsxs" : "jsx", args);
        }

JSXAttribute

本质上就是对于属性为jsx元素的时候做兼容处理,加上花括号

    JSXAttribute(path) {
      // 如果属性是一个jsxELement
      if (t.isJSXElement(path.node.value)) {
        // 将属性用 {} 包裹一下
        // 比如:<div title=<div a="1"/> > --> <div title={<div a="1"/>} >
        path.node.value = t.jsxExpressionContainer(path.node.value);
      }
    },

写在最后

这样我们就完整的过完了babel编译jsx的整体,希望这个解读能让你对jsx编译时有更多的理解,也能学习到其中的一些解决问题的思路

参考

Introducing the New JSX Transform:legacy.reactjs.org/blog/2020/0…

@babel/plugin-transform-react-jsx:babeljs.io/docs/babel-…

转载自:https://juejin.cn/post/7410347763898875916
评论
请登录