• 前端工程化5js源码编译和ast


    写在前面

    前面几节大概了解了webpack的使用和执行过程,上一节我们知道了webpack的源码编译的库是acorn,那今天我们就来研究一下js编译以及抽象语法树(ast)。我们先来看一个笔试题

    问题
    将一个 html 字符串变成树的形式

    <div id="main" data-x="hello">Hello<span id="sub" /></div>
    

    这样的一串字符串变成如下的一棵树,考虑尽可能多的形式,比如自闭合标签等。

        {
          tag: "div",
          selfClose: false,
          attributes: {
            "id": "main",
            "data-x": "hello"
          },
          text: "Hello",
          children: [
            {
              tag: "span",
              selfClose: true,
              attributes: {
                "id": "sub"
              }
            }
          ]
        }
    

    先来分析一下题目,题意即将html树转化成对象树的表示形式,主要难点就是需要正确匹配到标签并进行转化成对象的属性。下面我们来开始写代码,首先我们要找到标签的匹配正则,我们参考html-parser.js,然后循环切割html字符串,再通过类似递归(在开始标签的时候入栈,在闭合标签出栈并构建)的方式构建树,具体实现如下:

    参考代码

    /**
     * 输入:'<div id="main" data-x="hello">Hello<span id="sub" /></div>'
     * 输出:
    {
      tag: "div",
      selfClose: false,
      attributes: {
        "id": "main",
        "data-x": "hello"
      },
      text: "Hello",
      children: [
        {
          tag: "span",
          selfClose: true,
          attributes: {
            "id": "sub"
          }
        }
      ]
    }
     * 
     */
    /**
     * 
      伪代码
        1. 通过正则匹配到开始标签,通过startTagOpen匹配,可以获取到开始标签tag,入栈
        2. 切割html字符串
        3. 匹配属性,通过attribute匹配,循环直至所有attribute都匹配完成,可以获取所有的attributes
        4. 切割html字符串
        5. 匹配开始标签的闭合, >或者/> ,通过startTagClose匹配,可以知道是否为自闭合selfClose
        6. 切割html字符串
        7. 匹配到子级标签的开始或者自己结束标签的第一个标示符, <, 可以获取到标签的内部文本text
        8. 切割字符串
        9. 如果是结束标签,出栈,构建对象树,可以获取到children,继续循环
        10. 如果是新的开始标签,继续循环
     */
    
    const html2Object = (htmlStr) => {
      const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
      const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`
      const qnameCapture = `((?:${ncname}\\:)?${ncname})`
      const startTagOpen = new RegExp(`^<${qnameCapture}`)
      const startTagClose = /^\s*(\/?)>/
      const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
      let stack = [];
      let root;
      const matchTagStart = (element) => {
        const tagStart = htmlStr.match(startTagOpen);
        if (tagStart) {
          element.tag = tagStart[1];
          stack.push(element);
          htmlStr = htmlStr.substring(tagStart[0].length);
        }
      }
      const matchTagAttribute = (element) => {
        while (htmlStr.match(attribute)) {
          let attr = htmlStr.match(attribute);
          element.attributes[attr[1]] = attr[3];
          if (attr) htmlStr = htmlStr.substring(attr[0].length);
        }
      }
      const matchTagClose = (element) => {
        const tagClose = htmlStr.match(startTagClose);
        if (tagClose) {
          if (tagClose[0].trim() === '/>') {
            element.selfClose = true;
            const c = stack.pop();
            const p = stack.pop();
            if (p) {
              p.children.push(c);
              stack.push(p);
            }
          }
          htmlStr = htmlStr.substring(tagClose[0].length);
        }
      }
      const matchTagEnd = () => {
        const et = htmlStr.match(endTag);
        if (et) {
          const c = stack.pop();
          const p = stack.pop();
          if (p) {
            p.children.push(c);
            stack.push(p);
            root = JSON.parse(JSON.stringify(stack));
          }
          htmlStr = htmlStr.substring(et[0].length);
        }
      }
      const matchTagText = (element) => {
        const index = htmlStr.indexOf('<');
        element.text = htmlStr.substring(0, index);
        htmlStr = htmlStr.substring(index);
      }
      while (htmlStr) {
        let element = {
          tag: '',
          text: '',
          selfClose: false,
          attributes: {},
          children: [],
        }
        matchTagStart(element);
        matchTagAttribute(element);
        matchTagClose(element);
        matchTagText(element);
        matchTagEnd(element);
      }
      return root;
    }
    

    以上我们已经实现了一个简易的html模版解析方法,相当于html模版的对象表示法。当然也可以实现逆向,将html模版对象转化成dom树,这个相对比较简单。有了这个我们就会更好理解抽象语法树ast,ast即是对我们js代码的对象描述,和上面的例子是一个道理,有了这么一颗树我们会很容易对我们的代码进行静态操作。

    ast

    抽象语法树,js代码词法树型结构的表示。js代码在编译的过程中会首先解析成抽象语法树的形式。我们可以在astexplorer网站上查看js代码的ast结构。我们可以看一个简单的例子

    const print = ()=>{
      console.lot('hello world');
    } 
    print();
    

    转化成ast之后的代码变成了

    {
      "type": "Program",
      "start": 0,
      "end": 62,
      "body": [
        {
          "type": "VariableDeclaration",
          "start": 1,
          "end": 52,
          "declarations": [
            {
              "type": "VariableDeclarator",
              "start": 7,
              "end": 52,
              "id": {
                "type": "Identifier",
                "start": 7,
                "end": 12,
                "name": "print"
              },
              "init": {
                "type": "ArrowFunctionExpression",
                "start": 15,
                "end": 52,
                "id": null,
                "expression": false,
                "generator": false,
                "async": false,
                "params": [],
                "body": {
                  "type": "BlockStatement",
                  "start": 19,
                  "end": 52,
                  "body": [
                    {
                      "type": "ExpressionStatement",
                      "start": 23,
                      "end": 50,
                      "expression": {
                        "type": "CallExpression",
                        "start": 23,
                        "end": 49,
                        "callee": {
                          "type": "MemberExpression",
                          "start": 23,
                          "end": 34,
                          "object": {
                            "type": "Identifier",
                            "start": 23,
                            "end": 30,
                            "name": "console"
                          },
                          "property": {
                            "type": "Identifier",
                            "start": 31,
                            "end": 34,
                            "name": "lot"
                          },
                          "computed": false,
                          "optional": false
                        },
                        "arguments": [
                          {
                            "type": "Literal",
                            "start": 35,
                            "end": 48,
                            "value": "hello world",
                            "raw": "'hello world'"
                          }
                        ],
                        "optional": false
                      }
                    }
                  ]
                }
              }
            }
          ],
          "kind": "const"
        },
        {
          "type": "ExpressionStatement",
          "start": 54,
          "end": 62,
          "expression": {
            "type": "CallExpression",
            "start": 54,
            "end": 61,
            "callee": {
              "type": "Identifier",
              "start": 54,
              "end": 59,
              "name": "print"
            },
            "arguments": [],
            "optional": false
          }
        }
      ],
      "sourceType": "module"
    }
    

    我们发现转化之后的代码对象和数组的嵌套的树形结构,每个对象都最少有type、start、end三个属性,他们分别代表的是类型,开始列,结束列,通过对象的形式来描述源码。

    acorn与babel

    acorn是一个js解析库,能帮助我们将js解析成ast,如果想将jsx解析成ast则需要使用acorn-jsx。如果要将typescript解析成ast则需要用到babel或者typescript。

    参考

  • 相关阅读:
    使用SecureCRTP 连接生产环境的web服务器和数据库服务器
    CSS之浮动
    CSS之定位
    Session的过期时间如何计算?
    浏览器关闭后,Session会话结束了么?
    Spring事务注意点
    Lucene 的索引文件锁原理
    Mysql数据库的触发程序
    记一次jar包冲突
    关于jvm的OutOfMemory:PermGen space异常的解决
  • 原文地址:https://www.cnblogs.com/xingguozhiming/p/15660014.html
Copyright © 2020-2023  润新知