vue 2.0 抽象语法树AST

1,227 阅读1分钟

先探究下语法树是什么,是做什么的

graph TD
模版语法 --> 抽象语法树AST --> 正常的HTML语法

它本质上是一个js对象,vue会字符串的视觉看待.vue文件中的内容。将它解析为AST,下面举个例子

<div class="box">
    <h3 class="title">我是一个标题</h3>
    <ul>
        <li v-for="item in arr" :key="index">
            {{item}}
        </li>
    </ul>
</div>

上面的内容会被解析为下面的对象

{
    tag:"div",
    attrs:[{name: "class", value: "box"}],
    type:1,
    children:[
        {
            tag: "h3",
            attrs:[{name: "class", value: "title"}],
            type:1,
            children:[{text: "我是一个标题", type: 3}]
        },
        {
            tag: "ul",
            attrs:[],
            type:1,
            children:[
                {
                    tag: "li",
                    for: "arr",
                    key: "index",
                    alias: "item",
                    type: 1,
                    children: []
                }
            ]
        }
    ]
}

抽象语法树和虚拟节点的关系

graph LR
模版语法 --> 抽象语法树AST -->渲染函数h函数 -->虚拟节点 -->界面

下面对AST进行探秘

大概步骤分为以下几步

  • 相关算法知识的准备。 如指针,递归, 栈
  • AST形成算法
  • 写AST编译器
  • 写文本解析功能
  • AST 优化
  • 将AST生成h()函数

这里准备了三个算法题,帮助理解后面 的内容 juejin.cn/column/6997…

下面是将 字符串转为AST语法的 代码笔记

微信截图_20210823193440.png

index.js

import parse from './parse.js'
var templateString = `
<div>
  <h3 class="box" id="box1">你好</h3>
  <ul>
    <li>a</li>
    <li>b</li>
    <li>c</li>
  </ul>
  <div>
    <div>大家好</div>
  </div>
</div>
`
const ast = parse(templateString)

console.log(ast)

parse.js

import parseAttrString from './parseAttrsString'
export default function (templateString) {
  // 创建遍历的指针
  var index = 0;
  // 剩余部分
  var rest = "";
  // 开始标记
  //var startRegExp = /^\<([a-z]+[1-6]?)\>/;
  // 添加上class 等attr 内容后的正则
  var startRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/;
  // 结束标签
  var endRegExp = /^\<\/([a-z]+[1-6]?)\>/;
  // 抓取结束标签前的文字
  var wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/;
  // 创建两个栈
  var stack1 = [];
  var stack2 = [{ children: [] }];
  while (index < templateString.length - 1) {
    rest = templateString.substring(index);
    //console.log(templateString[index])
    // 识别遍历到的标签是不是一个开始标签
    if (startRegExp.test(rest)) {
      let tag = rest.match(startRegExp)[1];
      let attrString = rest.match(startRegExp)[2];
      const attrLength = attrString != undefined? attrString.length: 0
      console.log(attrLength)
      // 将开始标签压入栈1
      stack1.push(tag);
      // 给栈2 压入一个数组
      stack2.push({ tag: tag, children: [] , attrs: parseAttrString(attrString)});
      console.log("检测到开始标签" + tag);
      index += tag.length + 2 +attrLength ;
      //console.log(stack1,stack2)
    } else if (endRegExp.test(rest)) {
      // 识别结束标签
      let tag = rest.match(endRegExp)[1];
      // 出栈的时候判断 栈顶是不是和当前标签一致

      if (tag == stack1[stack1.length - 1]) {
        let stack_pop = stack1.pop();
        let stack_pop2 = stack2.pop();
        stack2[stack2.length - 1].children.push(stack_pop2);
      } else {
        throw Error(tag + "标签没有封闭");
      }
      //console.log("检测到结束标签" + tag);
      index += tag.length + 3;
      //console.log(stack1,stack2)
    } else if (wordRegExp.test(rest)) {
      // 遍历到的字符是不是文字 并且不是全空
      let word = rest.match(wordRegExp)[1];
      if (!/^\s+$/.test(word)) {
        //console.log("检测到中间文字" + word);
        stack2[stack2.length - 1].children.push({ text: word, type: 3 });
      }

      index += word.length;
    } else {
      index++;
    }
  }
  return stack2[0].children[0];
}

parseAttrsSting.js

// 把字符串返回数组 格式为 [{name: '', value: ''}]
export default function (string) {
  if (string==undefined) return []
  // 不能简单的使用 split 的方法, 需要使用 遍历的方式
  // 创建开始的点
  let point = 0;
  // 创建一个索, 初始值是 false ,遇到第一个 " 后 变为 true  再次遇见 " 的时候 变为 false
  // 只有当遇到空格的时候,并且 索的值为false 的时候 才分割这段字符创
  let yiyinhao = false
  // 定义一个存放值的数组
  let arr = []

  for (let i=0; i< string.length; i++) {
    let char = string[i]
    if (char=='"') {
      yiyinhao = !yiyinhao
    }else if (char==' '&& !yiyinhao) {
      if (!/^\s*$/.test(string.substring(point,i))) {

        arr.push(string.substring(point,i).trim())
        point = i
      }
    }
  }
  arr.push(string.substring(point).trim())
  let result = arr.map(item => {
    // 根据等号拆分
    let o = item.match(/^(.+)="(.+)"$/)
    
    return {
      name: o[1],
      value: o[2]
    }
  })
  return result
}