热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

React系列:Babel编译JSX生成代码

上次我们总结了React代码构建后的webpack模块组织关系,今天来介绍一下Babel编译JSX生成目标代码的一些规则,并且写一个简单的解析器,模拟整个生成的过程。我们还是拿最简

上次我们总结了 React 代码构建后的 webpack 模块组织关系,今天来介绍一下 Babel 编译 JSX 生成目标代码的一些规则,并且写一个简单的解析器,模拟整个生成的过程。

我们还是拿最简单的代码举例:

import {greet} from './utils';

const App = 

{greet('scott')}

; ReactDOM.render(App, document.getElementById('root'));

这段代码在经过Babel编译后,会生成如下可执行代码:

var _utils = __webpack_require__(1);

var App = React.createElement(
  'h1',
  null,
  (0, _utils.greet)('scott')
);

ReactDOM.render(App, document.getElementById('root'));

看的出来,App 是一个 JSX 形式的元素,在编译后,变成了 React.createElement() 方法的调用,从参数来看,它创建了一个 h1 标签,标签的内容是一个方法调用返回值。我们再来看一个复杂一些的例子:

import {greet} from './utils';

const style = {
  color: 'red'
};

const App = (
  

This is a JSX demo

); ReactDOM.render(App, document.getElementById('root'));

编译之后,会生成如下代码:

var _utils = __webpack_require__(1);

var style = {
  color: 'red'
};

var App = React.createElement(
  'div',
  { className: 'container' },
  React.createElement(
    'h1',
    { style: style },
    (0, _utils.greet)('scott'),
    ' hah'
  ),
  React.createElement(
    'p',
    null,
    'This is a JSX demo'
  ),
  React.createElement(
    'div',
    null,
    React.createElement(
      'input',
      { type: 'button', value: 'click me' }
    )
  )
);

ReactDOM.render(App, document.getElementById('root'));

从上面代码可以看出,React.createElement 方法的签名大概是下面这个样子:

React.createElement(tag, attrs, ...children);

第一参数是标签名,第二个参数是属性对象,后面的参数是 0 到多个子结点。如果是自闭和标签,只生成前两个参数即可,如下:

// JSX
const App = ;

// 编译结果
var App = React.createElement('input', { type: 'button', value: 'click me' });

现在,我们大概了解了由 JSX 到目标代码这中间的一些变化,那么我们是不是能够模拟这个过程呢?

要模拟整个过程,需要两个步骤:首先将 JSX 解析成树状数据结构,然后根据这个树状结构生成目标代码。

下面我们就来实际演示一下,假如有如下代码片段:

const style = {
  color: 'red'
};

function greet(name) {
  return `hello ${name}`;
}

const App = (
  

this is jsx-like code

parsing it now

);

我们在 JSX 中引用到了 style 变量和 greet() 函数,对于这些引用,在后期生成可执行代码时,会保持原样输出,直接引用当前作用域中的变量或函数。注意,我们可能覆盖不到 JSX 所有的语法规则,这里只做一个简单的演示即可,解析代码如下:

// 解析JSX
const parseJSX = function () {
  const TAG_LEFT = '<';
  const TAG_RIGHT = '>';
  const CLOSE_SLASH = '/';
  const WHITE_SPACE = ' ';
  const ATTR_EQUAL = '=';
  const DOUBLE_QUOTE = '"';
  const LEFT_CURLY = '{';
  const RIGHT_CURLY = '}';

  let at = -1;        // 当前解析的位置
  let stack = [];     // 放置已解析父结点的栈
  let source = '';    // 要解析的JSX代码内容
  let parent = null;  // 当前元素的父结点

  // 寻找目标字符
  let seek = (target) => {
    let found = false;

    while (!found) {
      let ch = source.charAt(++at);

      if (ch === target) {
        found = true;
      }
    }
  };

  // 向前搜索目标信息
  let explore = (target) => {
    let index = at;
    let found = false;
    let rangeStr = '';

    while (!found) {
      let ch = source.charAt(++index);

      if (target !== TAG_RIGHT && ch === TAG_RIGHT) {
        return {
          at: -1,
          str: rangeStr,
        };
      }

      if (ch === target) {
        found = true;
      } else if (ch !== CLOSE_SLASH) {
        rangeStr += ch;
      }
    }

    return {
      at: index - 1,
      str: rangeStr,
    };
  };

  // 跳过空格
  let skipSpace = () => {
    while (true) {
      let ch = source.charAt(at + 1);

      if (ch === TAG_RIGHT) {
        at--;
        break;
      }

      if (ch !== WHITE_SPACE) {
        break;
      } else {
        at++;
      }
    }
  };

  // 解析标签体
  let parseTag = () => {
    if (stack.length > 0) {
      let rangeResult = explore(TAG_LEFT);

      let resultStr = rangeResult.str.replace(/^\n|\n$/, '').trim();
      
      if (resultStr.length > 0) {
        let exprPositiOns= [];

        resultStr.replace(/{.+?}/, function(match, startIndex) {
          let endIndex = startIndex + match.length - 1;
          exprPositions.push({
            startIndex,
            endIndex,
          });
        });

        let strAry = [];
        let currIndex = 0;

        while (currIndex 
    if (source.charAt(at + 1) === CLOSE_SLASH) {
      at++;

      let endResult = explore(TAG_RIGHT);

      if (endResult.at > -1) {
        // 栈结构中只有一个结点 当前是最后一个闭合标签
        if (stack.length === 1) {
          return stack.pop();
        }

        let completeTag = stack.pop();

        // 更新当前父结点
        parent = stack[stack.length - 1];

        parent.children.push(completeTag);

        at = endResult.at;

        parseTag();

        return completeTag;
      }
    }

    let tagResult = explore(WHITE_SPACE);

    let elem = {
      tag: tagResult.str,
      attrs: {},
      children: [],
    };

    if (tagResult.at > -1) {
      at = tagResult.at;
    }

    // 解析标签属性键值对
    while (true) {
      skipSpace();

      let attrKeyResult = explore(ATTR_EQUAL);

      if (attrKeyResult.at === -1) {
        break;
      }

      at = attrKeyResult.at + 1;

      let attrValResult = {};

      if (source.charAt(at + 1) === LEFT_CURLY) {
        // 属性值是引用类型

        seek(LEFT_CURLY);

        attrValResult = explore(RIGHT_CURLY);
        
        attrValResult = {
          at: attrValResult.at,
          info: {
            type: 'ref',
            value: attrValResult.str,
          }
        };
      } else {
        // 属性值是字符串类型

        seek(DOUBLE_QUOTE);

        attrValResult = explore(DOUBLE_QUOTE);

        attrValResult = {
          at: attrValResult.at,
          info: {
            type: 'str',
            value: attrValResult.str,
          }
        };
      }

      at = attrValResult.at + 1;

      skipSpace();

      elem.attrs[attrKeyResult.str] = attrValResult.info;
    }

    seek(TAG_RIGHT);

    // 检测是否为自闭合标签
    if (source.charAt(at - 1) === CLOSE_SLASH) {
      // 自闭合标签 追加到父标签children中 然后继续解析
      if (stack.length > 0) {
        parent.children.push(elem);

        parseTag();
      }
    } else {
      // 有结束标签的 入栈 然后继续解析
      stack.push(elem);

      parent = elem;

      parseTag();
    }

    return elem;
  };

  return function (jsx) {
    source = jsx;
    return parseTag();
  };
}();

在解析 JSX 时,有以下几个关键步骤:

1. 解析到 `<` 时,表明一个标签的开始,接下来开始解析标签名,比如 div。
2. 在解析完标签名之后,试图解析属性键值对,如果存在,则检测 `=` 前后的值,属性值可能是字符串,也可能是变量引用,所以需要做个区分。
3. 解析到 `>` 时,表明一个标签的前半部分结束,此时应该将当前解析到的元素入栈,然后继续解析。
4. 解析到 `/>` 时,表明是一个自闭合元素,此时直接将其追加到栈顶父结点的 children 中。
5. 解析到 `

接下来,我们调用上面的 parseJSX() 方法,来解析示例代码:

const App = (`
  

this is jsx-like code

parsing it now

`); let root = parseJSX(App); console.log(JSON.stringify(root, null, 2));

生成的树状数据结构如下所示:

{
  "tag": "div",
  "attrs": {
    "className": {
      "type": "str",
      "value": "container"
    }
  },
  "children": [
    {
      "tag": "p",
      "attrs": {
        "style": {
          "type": "ref",
          "value": "style"
        }
      },
      "children": [
        {
          "type": "str",
          "value": "saying "
        },
        {
          "type": "expr",
          "value": "greet('scott')"
        },
        {
          "type": "str",
          "value": " hah"
        }
      ]
    },
    {
      "tag": "div",
      "attrs": {},
      "children": [
        {
          "tag": "p",
          "attrs": {},
          "children": [
            {
              "type": "str",
              "value": "this is jsx-like code"
            }
          ]
        },
        {
          "tag": "i",
          "attrs": {
            "className": {
              "type": "str",
              "value": "icon"
            }
          },
          "children": []
        },
        {
          "tag": "p",
          "attrs": {},
          "children": [
            {
              "type": "str",
              "value": "parsing it now"
            }
          ]
        },
        {
          "tag": "img",
          "attrs": {
            "className": {
              "type": "str",
              "value": "icon"
            }
          },
          "children": []
        }
      ]
    },
    {
      "tag": "input",
      "attrs": {
        "type": {
          "type": "str",
          "value": "button"
        },
        "value": {
          "type": "str",
          "value": "i am a button"
        }
      },
      "children": []
    },
    {
      "tag": "em",
      "attrs": {},
      "children": []
    }
  ]
}

在生成这个树状数据结构之后,接下来我们要根据这个数据描述,生成最终的可执行代码,下面代码可用来完成这个阶段的处理:

// 将树状属性结构转换输出可执行代码
function transform(elem) {
  // 处理属性键值对
  function processAttrs(attrs) {
    let result = [];

    let keys = Object.keys(attrs);

    keys.forEach((key, index) => {
      let type = attrs[key].type;
      let value = attrs[key].value;

      // 需要区分字符串和变量引用
      let keyValue = `${key}: ${type === 'ref' ? value : '"' + value + '"'}`;

      if (index  {
      // 子结点是标签元素
      if (child.tag) {
        content += processElem(child, elem);
        return;
      }

      // 以下处理文本结点

      if (child.type === 'expr') {
        // 表达式
        content += child.value;
      } else {
        // 字符串字面量
        content += `"${child.value}"`;
      }

      if (index 

我们来调用一下 transform() 方法:

let root = parseJSX(App);

let code = transform(root);

console.log(code);

运行完上述代码,我们会得到一个目标代码字符串,格式化显示后代码结构是这样的:

React.createElement(
  'div',
  {className: "container"},
  React.createElement(
    'p',
    {style: style},
    "saying ",
    greet('scott'),
    " hah"
  ),
  React.createElement(
    'div',
    null,
    React.createElement(
      'p',
      null,
      "this is jsx-like code"
    ),
    React.createElement(
      'i',
      {className: "icon"}
    ),
    React.createElement(
      'p',
      null,
      "parsing it now"
    ),
    React.createElement(
      'img',
      {className: "icon"}
    )
  ),
  React.createElement(
    'input',
    {type: "button", value: "i am a button"}
  ),
  React.createElement(
    'em',
    null
  )
);

我们还需要将上下文代码拼接在一起,就像下面这样:

const style = {
  color: 'red'
};

function greet(name) {
  return `hello ${name}`;
}

const App = React.createElement(
  'div',
  {className: "container"},
  React.createElement(
    'p',
    {style: style},
    "saying ",
    greet('scott'),
    " hah"
  ),
  React.createElement(
    'div',
    null,
    React.createElement(
      'p',
      null,
      "this is jsx-like code"
    ),
    React.createElement(
      'i',
      {className: "icon"}
    ),
    React.createElement(
      'p',
      null,
      "parsing it now"
    ),
    React.createElement(
      'img',
      {className: "icon"}
    )
  ),
  React.createElement(
    'input',
    {type: "button", value: "i am a button"}
  ),
  React.createElement(
    'em',
    null
  )
);

看上去是有几分模样了哈,那么如何实现 React.createElement() 方法,将上面的代码运行起来并输出预期的效果呢,我们会在下一篇文章中介绍。


推荐阅读
  • js常用方法(1)startWithJava代码varstartsWithfunction(str,regex){if(regexundefined||strundefined|| ... [详细]
  • 详细指南:使用IntelliJ IDEA构建多模块Maven项目
    本文在前两篇文章的基础上,进一步指导读者如何在IntelliJ IDEA中创建和配置多模块Maven项目。通过详细的步骤说明,帮助读者掌握项目模块化管理的方法。 ... [详细]
  • 探索Squid反向代理中的远程代码执行漏洞
    本文深入探讨了在网站渗透测试过程中发现的Squid反向代理系统中存在的远程代码执行漏洞,旨在帮助网站管理者和开发者了解此类漏洞的危害及防范措施。 ... [详细]
  • XWiki 数据模型开发指南
    本文档不仅介绍XWiki作为一个增强版的wiki引擎,还深入探讨了其数据模型,该模型可在用户界面层面被充分利用。借助其强大的脚本能力,XWiki的数据模型支持从简单的应用到复杂的系统构建,几乎无需直接接触XWiki的核心组件。 ... [详细]
  • 本文探讨了浏览器的同源策略限制及其对 AJAX 请求的影响,并详细介绍了如何在 Spring Boot 应用中优雅地处理跨域请求,特别是当请求包含自定义 Headers 时的解决方案。 ... [详细]
  • 本文深入探讨了WebGL与Three.js在构建多样化3D场景中的应用,详细解析了两者如何协同工作以实现高性能的3D渲染,并提供了实践指南。 ... [详细]
  • 深入解析ES6至ES8的新特性与应用
    本文详细介绍了自2015年发布的ECMAScript 6.0(简称ES6)以来,JavaScript语言的多项重要更新,旨在帮助开发者更好地理解和利用这些新特性进行复杂应用的开发。 ... [详细]
  • 大数据基础:JavaSE_day06 ... [详细]
  • iTOP4412开发板QtE5.7源码编译指南
    本文详细介绍了如何在iTOP4412开发板上编译QtE5.7源码,包括所需文件的位置、编译器设置、触摸库编译以及QtE5.7的完整编译流程。 ... [详细]
  • 本文探讨了如何利用 Hibernate 进行高效的批量更新和删除操作,包括直接使用 Hibernate API 的方法及其局限性,以及如何通过 JDBC 或存储过程实现更优的性能。 ... [详细]
  • R语言基础入门指南
    本文介绍R语言的基本概念,包括其作为区分大小写的解释型语言的特点、主要的数据结构类型如向量、矩阵、数据框及列表等,并探讨了R语言中对象的灵活性与函数的应用。此外,文章还提供了关于如何使用R进行基本操作的示例,以及解决常见编程问题的方法。 ... [详细]
  • Web前端性能提升指南:简化JavaScript与消除重复脚本
    本文为Web前端性能优化系列的第七篇,重点探讨简化JavaScript代码及清除重复脚本的方法。通过这些技术,可以显著提高网页加载速度和用户体验。了解更多信息,请参阅我们的完整指南:Web前端性能优化。 ... [详细]
  • 力扣93:复原IP地址问题解析(Golang实现)
    本文探讨了力扣平台上的第93号问题——复原IP地址。该问题要求从给定的纯数字字符串中,通过添加分隔符‘.’来构建所有可能的有效IP地址。有效IP地址由四个介于0至255之间的整数组成,不允许出现前导零。 ... [详细]
  • 1Authenticator简介1.1层次结构图1.2作用职责是验证用户帐号,是ShiroAPI中身份验证核心的入口点;接口中声明的authenticate方法就是用来实现认证逻辑 ... [详细]
  • 本文详细介绍了Java中的`ByteArrayInputStream`和`ByteArrayOutputStream`,包括它们的基本概念、工作原理及具体应用实例。`ByteArrayInputStream`用于处理内存中的字节数组,而`ByteArrayOutputStream`则用于将数据写入内存中的字节数组。 ... [详细]
author-avatar
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有