【Vue源码】AST抽象语法树 - 指针- 递归 - 栈 - 正则表达式 - 手写实现parse

769 阅读2分钟

嗨!~ 大家好,我是YK菌 🐷 ,一个微系前端 ✨,喜欢分享自己学到的小知识 🏹,欢迎关注我呀 😘 ~ [微信号: yk2012yk2012,微信公众号:ykyk2012]

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

Vue源码系列好久没更新了,今天来介绍AST抽象语法树,在正式介绍之前,我们会做一些准备工作,学习一些基本的算法知识,还有正则的知识。

gitee.com/ykang2020/v…

1. 抽象语法树是什么

模板语法直接编译成正常的HTML语法是非常困难的

而通过抽象语法树进行过渡,可以让编译工作变得简单

在这里插入图片描述

抽象语法树(Abstract Syntax Tree)本质上就是一个JS对象 在这里插入图片描述

那抽象语法树与我们之前学的虚拟节点有什么关系呢

在这里插入图片描述

2. 知识储备

2.1 指针思想

试寻找字符串中,连续重复次数最多的字符 aaaaaabbbbbbbcccccccccccccdddddd

在这里插入图片描述

// 试寻找字符串中,连续重复次数最多的字符
let str = 'aaaaaaabbbbbbbbbbbccccccccccccccccccccccddddddddd'

let i = 0
let j = 1

let maxRepeatCount = 0
let maxRepeatChar = ''

while (i <= str.length - 1) {
  if (str[i] !== str[j]) {
    console.log(`${i}${j}之间的字母${str[i]}连续重复了${j-i}次`)
    if (j - i > maxRepeatCount) {
      maxRepeatCount = j - i
      maxRepeatChar = str[i]
    }
    i = j
  }
  j++
}
console.log(`字母${maxRepeatChar}连续重复次数最多,重复了${maxRepeatCount}次`)

在这里插入图片描述

2.2 递归

这里可以参考我之前关于斐波那契的博文

在这里插入图片描述 直接暴力递归

function fib(n) {
	return n === 0 || n === 1 ? 1 : fib(n-1) + fib(n-2)
}

加入缓存对象

let cache = {}
function fib(n) {
	if(cache.hasOwnProperty(n)){
		return cache[n]
	}
	let value =  n === 0 || n === 1 ? 1 : fib(n-1) + fib(n-2)
	cache[n] = value
	return value
}

在这里插入图片描述

let arr = [1, 2, 3, [4, 5, [6, 7], 8], 9, [10, 11]]

function convert(arr){
  let result = []
  arr.forEach(item => {
    if(typeof item === 'number'){
      result.push({
        value: item
      })
    }else if (Array.isArray(item)) {
      result.push({
        children: convert(item)
      })
    }
  })
  return result
}

let res = {children: convert(arr)}
console.log(res)

在这里插入图片描述

let arr = [1, 2, 3, [4, 5, [6, 7], 8], 9, [10, 11]]

function convert(item){
  if(typeof item === 'number'){
    return {value: item}
  }else if (Array.isArray(item)) {
    return {
      children: item.map(_item=>convert(_item))
    }
  }
}

console.log(convert(arr))

在这里插入图片描述

2.3 正则表达式

replace 删除字符串中数字

> 'awerfa453q5325fd5234rfsdf'.replace(/\d/g, '')
< "awerfaqfdrfsdf"

search 查找第一个匹配到的位置

> 'awerfa453q5325fd5234rfsdf'.search(/\d/)
< 6
> 'awerfa453q5325fd5234rfsdf'.search(/\d/g)
< 6

match 匹配

> 'awerfa453q5325fd5234rfsdf'.match(/\d/)
< ["4", index: 6, input: "awerfa453q5325fd5234rfsdf", groups: undefined]
> 'awerfa453q5325fd5234rfsdf'.match(/\d/g)
< (11) ["4", "5", "3", "5", "3", "2", "5", "5", "2", "3", "4"]

在这里插入图片描述

match 捕获 分组

> '23[abc]'.match(/^\d+\[/)
< ["23[", index: 0, input: "23[abc]", groups: undefined]
> '23[abc]'.match(/(^\d+)\[/)
< (2) ["23[", "23", index: 0, input: "23[abc]", groups: undefined]

test 在这里插入图片描述

2.4 栈的思想

在这里插入图片描述

准备两个栈 一个用来存放数字 一个用来存放字符串

【解题思路】在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

function smartRepeat(templateStr) {
  let i = 0
  let stackNumber = []
  let stackString = []

  let rest = templateStr

  while (i < templateStr.length - 1) {

    // 剩余部分
    rest = templateStr.substring(i);

    // 看剩余部分是不是以数字和[开头
    if (/^\d+\[/.test(rest)) {
      // 得到数字
      let times = Number(rest.match(/^(\d+)\[/)[1])
      // 将数字和空字符串分别入对应的栈
      stackNumber.push(times)
      stackString.push('')

      // 指针向后移动数字的位数 ,再加上[这一位
      i += times.toString().length + 1
    } else if (/^\w+\]/.test(rest)) {
      // 剩余部分是字母数字下划线和]开头的
      // 捕获[]中的内容
      let word = rest.match(/^(\w+)\]/)[1]
      // 将字符栈顶改为当前字母
      stackString[stackString.length - 1] = word
      // 指针右移,移动到]的位置
      i += word.length
    } else if (rest[0] === ']') {
      let word = stackString.pop()
      let times = stackNumber.pop()

      // 将word重复times次,插入到栈顶字符串后面
      let newStr = word.repeat(times)
      stackString[stackString.length - 1] += newStr
      i++
    }
    console.log(i, stackNumber, stackString)
  }
  return stackString[0].repeat(stackNumber[0])
}

let result = smartRepeat('2[4[abc]3[b]]')
console.log(result)

在这里插入图片描述

3. 实现AST

  • 学习源码时,源码思想要借鉴,而不是抄袭,要能够发现源码中书写的精彩的地方
  • 将独立的功能拆写为独立的js文件,通常是一个独立的类,每个独立的功能必须能独立的单元测试
  • 应该围绕中心功能,先把主干完成,然后修剪枝叶
  • 功能不需要一步到位,功能的拓展要一步一步完成,有的非核心功能甚至不需要实现

源码中的parse函数就是将HTML变成AST

index.js

import parse from "./parse";

let templateString = `<div>
    <h3 class="aa bb cc" v-on="xxx" id="mybox">你好</h3>
    <ul>
      <li>A</li>
      <li>B</li>
      <li>C</li>
    </ul>
</div>`;

const AST = parse(templateString);
console.log(AST);

parse.js

import parseAttrsString from "./parseAttrsString";

/**
 * parse函数
 * @param {*} templateString
 * @returns
 */
export default function parse(templateString) {
  // 定义一个指针
  let i = 0;
  // 剩余部分
  let rest = "";

  // 开始标记正则
  let startRegExp = /^\<([a-z1-6]+)(\s[^\<]+)?\>/;
  // 结束标记正则
  let endRegExp = /^\<\/([a-z1-6]+)\>/;
  // 结束标记之前的文字【前面不是是<】
  let wordRegExp = /^([^\<]+)\<\/[a-z0-9]+\>/;

  // 准备两个栈
  let stack1 = []; // 辅助栈,存标签名
  let stack2 = [{ chilren: [] }];

  while (i < templateString.length - 1) {
    rest = templateString.substring(i);

    // 识别开始标签
    if (startRegExp.test(rest)) {
      let tag = rest.match(startRegExp)[1];
      let attrsString = rest.match(startRegExp)[2];
      console.log(i, "检测到开始标记", tag);

      const attrsStringLength = attrsString == null ? 0 : attrsString.length;
      // 将开始标记 入栈1
      stack1.push(tag);
      // 处理标签属性
      const attrsArray = parseAttrsString(attrsString);
      // 将空数组 入栈2
      stack2.push({ tag: tag, chilren: [], attrs: attrsArray });

      i += tag.length + 2 + attrsStringLength;
    } else if (endRegExp.test(rest)) {
      // 遇见结束标签
      let tag = rest.match(endRegExp)[1];
      console.log(i, "检测到结束标记", tag);

      let pop_tag = stack1.pop();
      // 此时tag一定与stack1的栈顶相同
      if (tag === pop_tag) {
        let pop_arr = stack2.pop();

        if (stack2.length > 0) {
          stack2[stack2.length - 1].chilren.push(pop_arr);
        }
      } else {
        throw new Error(pop_tag + "标签没有封闭好");
      }

      i += tag.length + 3;
    } else if (wordRegExp.test(rest)) {
      let word = rest.match(wordRegExp)[1];
      // 看word是不是全空
      if (!/^\s+$/.test(word)) {
        // 不为空
        console.log(i, "检测到文字", word);
        // 改变stack2的栈顶元素
        stack2[stack2.length - 1].chilren.push({ text: word, type: 3 });
      }
      i += word.length;
    } else {
      i++;
    }
  }
  return stack2[0].chilren[0];
}

处理 标签属性 parseAttrsString.js

/**
 * 将attrsString变为数组返回
 * @param {*} attrsString
 */
export default function parseAttrsString(attrsString) {
  let result = [];
  // 当前是否在引号内
  let isQuote = false;
  let point = 0;
  if (attrsString === undefined) return [];

  for (let i = 0; i < attrsString.length; i++) {
    let char = attrsString[i];
    if (char === '"') {
      isQuote = !isQuote;
    } else if (char === " " && !isQuote) {
      // 是空格且不在引号之中
      let str = attrsString.substring(point, i);
      // 不是纯空格
      if (!/^\s*$/.test(str)) {
        result.push(str.trim());
        point = i;
      }
    }
  }
  // 循环结束之后,最后一个也要加进去
  result.push(attrsString.substring(point).trim());

  result = result.map((item) => {
    // 根据等号拆分
    const o = item.match(/^(.+)="(.+)"$/);
    return {
      name: o[1],
      value: o[2],
    };
  });

  return result;
}

在这里插入图片描述

gitee.com/ykang2020/v…

最后,欢迎关注我的专栏,和YK菌做好朋友~