Vue源码-AST抽象语法树

AST 抽象语法树

抽象语法树的本质就是一个 JS 对象;
对象包含 tag attrs type children 等属性;
模板语法–>转成字符串–>转成 AST 语法树

模板语法—>正常的 HTML 语法(算法编写难度极大)

模板语法—>抽象语法树—->正常的 HTML 语法(算法编写难度较小)

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

模板语法–>抽象语法树–>渲染函数(h 函数)–>虚拟节点—经过 diff—>界面

学习内容

  1. 相关算法储备
  2. AST 形成算法
  3. 手写 AST 编译器
  4. 手写文本解析功能
  5. AST 优化
  6. 将 AST 生成 h 函数

算法储备

指针思想

寻找字符串中,连续重复次数最多的字符串 “aaaaaabbbbbbbbcccccccccccccccddddddd”;

/**



 * 寻找字符串中,连续重复次数最多的字符串 "aaaaaabbbbbbbbcccccccccccccccddddddd";
 * 指针就是下标(不是C语言的指针,c语言的指针可以操作内存)
 * i:0; j:0
 *
 * aaaaaabbbbbbbbcccccccccccccccddddddd
 * i     j
 * 指针移动规律: (不管怎么样j都要后移,)
 *  如果i和j指向的字一样,那么i不动,j后移
 *  如果i和j指向的字不一样,此时说明他们之间的字都是连续相同的(统计一下j和i的差距,就是当前差距的字符数量),让i追上j,j后移,直到i是否还在范围内,不在范围内就循环结束了
 *
 */
window.ZhiZhen = function (str) {
  // 指针
  var i = 0;
  var j = 1;
  // 字符串
  var str = "aaabbbbccccc";
  // 记录当前存的出现次数多的字符
  var word = "";
  // 记录当前存的出现次数多的字符的数量
  var count = 0;
  while (i < str.length - 1) {
    if (str[i] != str[j]) {
      if (count < j - i) {
        count = j - i;
        word = str[i];
      }
      i = j;
    }
    j++;
  }
  return word;
};

递归深入

斐波那契

试着输出斐波那契数列的前 10 项.即 1 1 2 3 5 8 13 21 34 55

思考代码是否有大量重复的计算?

应该如何的解决重复计算的问题?

/**



 * 试着输出斐波那契数列的前10项.即1 1 2 3 5 8 13 21 34 55
 * 思考代码是否有大量重复的计算?
 * 应该如何的解决重复计算的问题?
 */
var cache = {}; // 缓存对象
window.DiGuiShenRu = function fib(n) {
  // 方法1. 基本写法(存在大量的重复计算,每次计算都是要找到最底层的1)
  // return n == 0 || n == 1 ? 1 : fib(n - 1) + fib(n - 2); // 如果是0 1 那么就返回1; 如果不是就递归
  // 方法2. cache思想(把之前的计算都记下来)
  // {
  //   fib(0): 1,
  //   fib(1): 1,
  //   fib(2): 2,
  //   fib(3): 3,
  //   fib(4): 5,
  // }
  // 判断缓存对象是否有,有直接用,没有才计算
  if (cache.hasOwnProperty(n)) {
    return cache[n];
  } else {
    // 缓存对象没有这个值,看下标如果是0 1 那么就返回1; 如果不是就递归
    var v = n == 0 || n == 1 ? 1 : fib(n - 1) + fib(n - 2);
    cache[n] = v;
    return v;
  }
};

形式转换(高维数组)

写法 1 的递归次数是要远远大于写法 2 的,因为写法 1 遇到任何东西都要递归一次

// 形式转换
// 试着将高维数组[1, 2, [3, [4, 5], 6], 7, [8, 9]]变成下面所示的对象
// {
//   children: [
//     { value: 1 }, { value: 2 },
//     {children: [{ value: 3 },{ children: [{ value: 4 }, { value: 5 }] },{ value: 6 },],},
//     { value: 7 }, {children: [{ value: 8 }, { value: 9 }],},
//   ];
// }
window.XingShiZhuanHuan = function arrayMethod() {
  // 写法 1
  function convert1(arr) {
    // 准备一个结果数组
    var result = [];
    for (let i = 0; i < arr.length; i++) {
      const element = arr[i];
      // 如果是数字number,那么就直接推入
      if (typeof element == "number") {
        result.push({ value: element });
        // 如果是数组,那么就继续循环递归
      } else if (Array.isArray(arr)) {
        result.push({
          children: convert1(element),
        });
      }
    }
    return result;
  }
  // return convert1([1, 2, [3, [4, 5], 6], 7, [8, 9]])

  // 写法 2 (映射 map)
  // 参数不再是arr,而是item,意味着itme可能是数字也可能是数组
  function convert2(item) {
    // 如果传进来的是数字
    if (typeof item == "number") {
      return { value: item };
    } else if (Array.isArray(item)) {
      // 如果传进来的是数组
      return {
        children: item.map((_item) => convert2(_item)),
      };
    }
  }
  return convert2([1, 2, [3, [4, 5], 6], 7, [8, 9]]);
};

栈(stack)又名堆栈,他是运算受限的线性表,仅在表尾能进行插入和删除操作。这一端被称为栈顶。相对的把另一端称为栈底。

向一个栈插入新元素。又称作进栈,入栈或压栈。从一个栈删除元素又称作出栈或退栈。

后进先出(LIFO)特点,栈中的元素最先进入栈的必定是最后出栈的后进栈的一定会先出栈。

应用

JS 中,栈可以用数组模拟,需要限制只能使用 push() pop() ,不能使用 unshift()和 shift(),即数组尾是栈顶

题目

/**



 * 智能重复
 * 试着编写,智能重复,smartRepeat函数.实现
 * 3[abc] ---> abcabcabc
 * 3[2[a]2[b]] ---> aabbaabbaabb
 * 2[1[a]3[b]2[3[c]]4[d]]] ---> abbbcccddddcccddddabbbcccddddcccdddd
 *
 * 实现思路:
 *  两个栈: 一个数字栈,一个字符串栈
 *  遍历每一个字符,
 *  1. 如果这个字符是个数字,那么就把数字压栈,把空字符压栈
 *  2. 如果这个字符是个字母,那么此时就把栈顶这项改为这个字母
 *  3. 如果这个字符是个],那么就将这个数字弹栈,就把字符串这个栈的栈顶的元素,重复刚刚的这个次数,拼接到栈的新栈顶上
 * 正则表达式的相关方法
 */
window.smartRepeat = function (templateStr) {
  // 指针
  var index = 0;
  // 栈1 存放数字
  var stack1 = [];
  // 栈2 存放临时字符串
  var stack2 = [];
  // 剩余部分
  var rest = templateStr;
  while (index < templateStr.length - 1) {
    // 剩余部分
    rest = templateStr.substring(index);
    // 看当前的剩余部分是不是数字和[开头
    if (/^\d+\[/.test(rest)) {
      // 得到这个数字
      let times = rest.match(/^(\d+)\[/)[1] - 0;
      // 把数组和空字符压栈
      stack1.push(times);
      stack2.push("");

      // 让指针后移,times是多少位往后移动多少位加1位
      // 加一位是为了让[也移动一位
      index += times.toString().length + 1;
    } else if (/^\w+\]/.test(rest)) {
      // 如果这个字符是字母,那么此刻就把栈顶这项改为这个字母
      let word = rest.match(/^(\w+)\]/)[1];
      // console.log(word);
      stack2[stack2.length - 1] = word;
      // 让指针后移,word 是多少位往后移动多少位
      index += word.length;
    } else if (rest[0] == "]") {
      // 如果这个字符是],
      // 1. 那么就将 stack1 的最后一项 A 弹栈,
      // 2. 把 stack2 的最后一项 B  弹栈
      // 3. 把 A 重复 B 次数 得到 C , 然后把 stack2 新栈顶的数据拼上 C
      let times = stack1.pop();
      let word = stack2.pop();
      stack2[stack2.length - 1] += word.repeat(times);
      index++;
    }
    // while结束之后,stack1 和 stack2 中肯定还剩余一项,如果剩余的个数不对,那么就是用户的问题,方括号没有闭合等原因
  }
  return stack2[0].repeat(stack1[0]);
};

正则表达式的相关方法

search 加 g 不加都一样

// 替换所有数字为空字符
'123大中小abc'.replace(/\d/g, '')  // 返回 '大中小abc'
// 返回了第一个数字的下标
'123大中小abc'.search(/\d/)   // 返回0
// 返回了第一个数字的下标
'123大中小abc'.search(/\d/g)  // 返回0

'123大中小abc'.match(/\d/)    // ['1', index: 0, input: '123大中小abc', groups: undefined]

'123大中小abc'.match(/\d/g)   // ['1', '2', '3']

/^\d/.test('abc') // false

/^\d/.test('123abc') // true

'34[abc]'.match(/^\d+\[/) // ['34[', index: 0, input: '34[abc]', groups: undefined]
// 加个括号可以捕获,显示在返回的第一项
'34[abc]'.match(/^(\d+)\[/)  //  ['34[', '34', index: 0, input: '34[abc]', groups: undefined]

手写 AST 抽象语法树源码

  1. 独立功能拆分为 js 文件,独立类,每个模块可以单元测试

  2. 先完成主干,然后修剪枝叶

思路: 和上一个思路相似,

  1. 就是两个栈,一个 栈 A 存标签,一个 栈 B 存标签里面的内容,

  2. 遇到标签进 A, 同时 B 也需要一个进一个数组([]是了放内容,是 AST 语法树的格式,内容放到数组内,我们上个例子是搞个字符串,这里和哪里用法一样),

  3. 遇到内容, 就把这个内容放到进 B 的最后一个数组内 组成 C([我是内容]),

  4. 遇到结束标签,就把 C 和 A 的最后一起拿出来,把 C 以 children 的形式标记到 A 的最后一个标签上组成 D,然后把 D 存到 B 的最后一个数组内

parse.js

import parseAttrsString from "./parseAttrsString.js";
export default function (templateStr) {
  // 指针
  var index = 0;
  // 剩余部分
  var rest = "";
  // 开始标记
  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 < templateStr.length - 1) {
    rest = templateStr.substring(index);
    // console.log(templateStr[index]);
    // 识别遍历到的这个字符是不是开始标签
    if (startRegExp.test(rest)) {
      let tag = rest.match(startRegExp)[1];
      let attrsString = rest.match(startRegExp)[2]; // 也就是获取属性的内容 class="box" id="mybox"
      //   console.log("检测到 Start 标记", tag);
      //   将开始标记推入 stack1
      stack1.push(tag);
      //   将空数组推入 stack2
      stack2.push({
        tag: tag,
        children: [],
        attrs: parseAttrsString(attrsString),
      });
      // 指针移动标签的长度+2,因为<>也占了两位, 再加上 attrsString 的长度
      const attrsStringLength = attrsString != null ? attrsString.length : 0;
      index += tag.length + 2 + attrsStringLength;
    } else if (endRegExp.test(rest)) {
      // 识别遍历到的这个标签是不是一个结束标签
      let tag = rest.match(endRegExp)[1];
      // 指针移动标签的长度+2,因为</>也占了三位
      //   console.log("检测到 End 标记", 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].children.push(pop_arr);
        }
      } else {
        throw new Error(pop_tag + " 标签没有封闭");
      }
      index += tag.length + 3;
    } else if (wordRegExp.test(rest)) {
      // 识别遍历到的这个标签是不是文字,并且不能是全空的文字
      let word = rest.match(wordRegExp)[1];
      //   看word是不是全是空,
      if (!/^\s+$/.test(word)) {
        // 不是全是空
        // console.log("检测到 Word 内容", word);
        // 改变此时 stack2 的栈顶的数据
        stack2[stack2.length - 1].children.push({ text: word, type: 3 });
      }
      index += word.length;
    } else {
      // 这里就是标签中的文字
      index++;
    }
  }
  return stack2[0].children;
}

parseAttrsString.js

/**



 *把 attrsString 变成数组返回
 * class="box id="mybox
 * 返回成
 * [{name: class, value: box},{name: id, value: mybox]
 *
 * 关于空格删除的问题,我们需要考虑这个是没用的空格,还是在引号内的空格,引号内的需要留下,外的需要干掉
 * class="aa bb cc" id="dd"
 * 1. 需要一个变量 isInYinHao = false 遍历上面的字符,一直往下走,
 * 2. 走到奇数个数的引号,就把 isInYinHao = true, 在走到偶数个数的引号,就 isInYinHao = false,
 * 3. 然后当 true的时候遇到了空格,那说明是属性里的,留下,false时候遇到了就删除这个空格
 *
 */
export default function (attrsString) {
  if (attrsString === undefined) {
    return [];
  }
  console.log(attrsString);
  let isInYinHao = false; // 当前是否在引号内
  let ponit = 0; // 断点
  let resultArr = [];
  for (let i = 0; i < attrsString.length; i++) {
    const char = attrsString[i];
    if (char === '"') {
      isInYinHao = !isInYinHao;
    } else if (char === " " && !isInYinHao) {
      // 遇到了空格,并且不在引号内
      console.log(i);
      if (!/^\s*$/.test(attrsString.substring(ponit, i))) {
        resultArr.push(attrsString.substring(ponit, i).trim());
        ponit = i;
      }
    }
  }
  //   循环结束还会剩一个的
  resultArr.push(attrsString.substring(ponit));

  //   *把 attrsString 变成数组返回
  //   * class="box id="mybox
  //   * 返回成
  //   * [{name: class, value: box},{name: id, value: mybox]
  //   *
  resultArr = resultArr.map((item) => {
    // 根据等号拆分
    const o = item.match(/^(.+)="(.+)"$/);
    return {
      name: o[1],
      value: o[2],
    };
  });
  return resultArr;
}

用法

import parse from "./parse.js";

var templateStr = `<div>
            <h3 class="aa bb" id="mybox">你好</h3>
            <ul>
                <li>A</li>
            </ul>
        </div>`;
const ast = parse(templateStr);
console.log(ast);
//   [
//     {
//         "tag": "div",
//         "children": [
//             {
//                 "tag": "h3",
//                 "children": [
//                     {
//                         "text": "你好",
//                         "type": 3
//                     }
//                 ],
//                 "attrs": [
//                     {
//                         "name": "class",
//                         "value": "aa bb"
//                     },
//                     {
//                         "name": " id",
//                         "value": "mybox"
//                     }
//                 ]
//             },
//             {
//                 "tag": "ul",
//                 "children": [
//                     {
//                         "tag": "li",
//                         "children": [
//                             {
//                                 "text": "A",
//                                 "type": 3
//                             }
//                         ],
//                         "attrs": []
//                     }
//                 ],
//                 "attrs": []
//             }
//         ],
//         "attrs": []
//     }
// ]

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYsK5uAz' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片