AST 抽象構文木
シリーズノート:
- 【フロントエンドソースコード解析】口ひげテンプレートエンジンの核心原理
- 【フロントエンドのソースコード解析】仮想DOMの核心原理
- 【フロントエンドソースコード解析】データレスポンシブの原則
- 【フロントエンドソースコード解析】AST抽象構文木
- 【フロントエンドソースコード解析】命令とライフサイクル
概要:
- 関連するアルゴリズム予約
- AST形成アルゴリズム
- 手書きの AST コンパイラ
- 手書き文字解析機能
- AST最適化
- AST から h() 関数を生成する
この章のソースコード: https://gitee.com/szluyu99/vue-source-learn/tree/master/AST_Study
抽象構文木の紹介
抽象構文木とは?
- テンプレート構文からHTML 構文に直接コンパイルするのは非常に困難です。
- 抽象構文ツリーを介して移行すると、これが簡単になります
抽象構文木は基本的にJS オブジェクトです:
- 一連のルールによりHTML文法に対応したJSオブジェクトとして理解される
抽象構文木と仮想ノードの関係:
関連するアルゴリズム予約
関連コード: https://gitee.com/szluyu99/vue-source-learn/tree/master/AST_Study/pre
ポインター
質問: 文字列内で最も連続した繰り返しを持つ文字を見つけてみてください。
function findMaxChar(str) {
let i = 0, j = 1
let maxChar = str[i], maxRepeat = -1
while (i < str.length) {
if (str[i] != str[j]) {
if (j - i > maxRepeat) {
maxRepeat = j - i
maxChar = str[i]
}
i = j
}
j++
}
console.log(`最多的字母是 ${
maxChar},重复了 ${
maxRepeat} 次`);
}
let str = 'aaaabbbbbcccccccccccccdddddd'
findMaxChar(str)
再帰
トピック 1: フィボナッチ数列の最初の 10 項目、つまり 1、1、2、3、5、8、13、21、34、55 を出力してみてください。それでは、考えてみてください、コードには繰り返し計算がたくさんありますか? ダブルカウントの問題を解決する方法
キャッシュされていない再帰:
function fib(n) {
return n <= 1 ? n : fib(n - 1) + fib(n - 2)
}
キャッシングによる再帰:
let cache = {
}
function fib(n) {
if (n in cache) return cache[n]
return cache[n] = (n <= 1 ? n : fib(n - 1) + fib(n - 2))
}
関数
console.count()
に
トピック 2: 高次元配列を以下のオブジェクト[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 }
]
};
// 转换函数1
function convert(arr) {
return {
children: help(arr)}
}
function help(arr) {
let res = [];
for (let i = 0; i < arr.length; i++) {
if (typeof arr[i] === "number") {
res.push({
value: arr[i] });
} else if (Array.isArray(arr[i])) {
res.push({
children: help(arr[i]) });
}
}
return res;
}
// 转换函数2
function convert(item) {
if (typeof item == 'number')
return {
value: item }
if (Array.isArray(item))
return {
children: item.map(_item => convert(_item)) }
}
スタック
JavaScript では、
push()
と をpop()
トピック: 「スマート リピート」smartRepeat 関数を作成して、次のことを達成してみてください。
3[abc]
になりますabcabcabc
3[2[a]2[b]]
になりますaabbaabbaabb
2[1[a]3[b]2[3[c]4[d]]]
になりますabbbcccddddcccddddabbbcccddddcccdddd
次のような入力文字列が不正な場合は考慮しないでください。
2[a3[b]]
は間違っています。1 を入力する必要があります。つまり、2[1[a]3[b]]
[abc]
は間違っています。1 を入力する必要があります。つまり、1[abc]
一般に、括弧に遭遇して字句解析を行う必要がある場合、スタックがよく使用されます。
アイデア: 各文字をトラバースする
- 文字が数字の場合、数字をスタック A にプッシュし、ヌル文字をスタック B にプッシュします。
- 文字が文字の場合、スタック B の一番上を文字に変更します
- 文字が の場合
]
、スタック A とスタック B が別々にポップされ、結合され、結合結果がスタック B の一番上の文字列の後ろに結合されます
function smartRepeact(templateStr) {
let stackA = [] // 存放数字
let stackB = [] // 存放临时字符串
let rest = templateStr // 剩余字符串
let idx = 0 // 指针
while (idx < templateStr.length - 1) {
rest = templateStr.substring(idx) // 更新剩余字符串
if (/^\d+\[/.test(rest)) {
// 判断是否以数字和 [ 开头
// 取出开头的数字
let times = Number(rest.match(/^(\d+)\[/)[1])
stackA.push(times)
stackB.push('')
idx += times.toString().length + 1
} else if(/^\w+\]/.test(rest)) {
// 判断是否以字母和 ] 开头
// 如果这个字符是字母,那么就把B栈顶这项改为这个字母
// 取出开头的字母
let word = rest.match(/^(\w+)\]/)[1]
stackB[stackB.length - 1] = word
idx += word.length
} else if (rest[0] === ']') {
// 如果这个字符是 ],组合两个栈的结果并放入栈B
let times = stackA.pop() // 取出栈顶的数字
let word = stackB.pop() // 取出栈顶的字符串
stackB[stackB.length - 1] += word.repeat(times)
idx++
}
}
return stackB[0].repeat(stackA[0])
}
正規表現
JS で一般的に使用される正規表現について少し知識を追加します。
// replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串
'abc666def123'.replace(/\d/g, '') // abcdef
// search() 方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串
// 找不到则返回 -1
'abc666def123'.search(/\d/g) // 3
// match() 方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配
// 找不到则返回 null
'abc666def123'.match(/\d/g) // ['6', '6', '6', '1', '2', '3']
// test() 方法用于检测一个字符串是否匹配某个模式
/^\d/.test('5abc') // true
/^\d/.test('abc') // false
'555[abc]'.match(/^\d+\[/)
// ['555[', index: 0, input: '555[abc]', groups: undefined]
// () 表示捕获
'555[abc]'.match(/(^\d+)\[/)
// ['555[', '555', index: 0, input: '555[abc]', groups: undefined]
手書きの AST 抽象構文木
この章のソースコード: https://gitee.com/szluyu99/vue-source-learn/tree/master/AST_Study
開始タグと終了タグを識別する
関連する知識については、上記の「Algorithm Reserve - Stack」の内容を参照してください。
parse.js
:
export default function parse(templateStr) {
let rest = ''
// 开始标签的正则
const startRegExp = /^\<([a-z]+[1-6]?)\>/
// 结束标签的正则
const endRegExp = /^\<\/([a-z]+[1-6]?)\>/
// 结束标签前文字的正则(注意开头不含 <)
const wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/
let stackA = [], stackB = []
let index = 0
while (index < templateStr.length - 1) {
rest = templateStr.substring(index)
if (startRegExp.test(rest)) {
// 识别遍历到的字符,是 开始标签
let tag = rest.match(startRegExp)[1];
// console.log(`检测到开始标记:<${tag}>`);
stackA.push(tag) // 将开始标记推入 栈A
stackB.push([]) // 将空数组推入 栈B
// 移动开始标签的长度,由于 <> 是两个字符,所以需要 + 2
index += tag.length + 2
} else if (endRegExp.test(rest)) {
// 识别遍历到的字符,是 结束标签
let tag = rest.match(endRegExp)[1];
// console.log(`检测到结束标记:</${tag}>`);
// 此时,tag 一定和 栈A 顶部是相同的
if (tag === stackA[stackA.length - 1]) {
stackA.pop()
} else {
throw new Error(`${
stackA[stackA.length - 1]} 标签没有封闭`)
}
// 移动结束标签的长度,由于 </.> 是两个字符,所以需要 + 3
index += tag.length + 3
} else if (wordRegExp.test(rest)) {
// 识别到遍历的字符,是 文字(并且不能为全空)
let word = rest.match(wordRegExp)[1];
if (!/^\s+$/.test(word)) {
console.log(`检测到文字:${
word}`);
}
// 指针移动到文字的末尾
index += word.length
} else {
index++
}
// 未考虑文字在标签后面的情况,如:
// <p>123</p> hello
}
}
スタックを使用して AST を形成する
目的の効果を達成します。
let htmlStr = `<div>
<h1>Hello</h1>
<ul>
<li>111</li>
<li>222</li>
<li>333</li>
</ul>
</div>`;
const ast = parse(htmlStr)
console.log(ast);
parse.js
:
export default function parse(templateStr) {
let rest = "";
// 开始标签的正则
const startRegExp = /^\<([a-z]+[1-6]?)\>/;
// 结束标签的正则
const endRegExp = /^\<\/([a-z]+[1-6]?)\>/;
// 结束标签前文字的正则(注意开头不含 <)
const wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/;
let stackA = [];
let stackB = [{
children: [] }];
let index = 0;
while (index < templateStr.length - 1) {
rest = templateStr.substring(index);
if (startRegExp.test(rest)) {
// 识别遍历到的字符,是 开始标签
let tag = rest.match(startRegExp)[1];
console.log(`检测到开始标记:<${
tag}>`);
stackA.push(tag); // 将开始标记推入 栈A
stackB.push({
tag: tag, children: [] }); // 将空数组推入 栈B
// 移动开始标签的长度,由于 <> 是两个字符,所以需要 + 2
index += tag.length + 2;
// console.log(stackA, stackB);
} else if (endRegExp.test(rest)) {
// 识别遍历到的字符,是 结束标签
let tag = rest.match(endRegExp)[1];
console.log(`检测到结束标记:</${
tag}>`);
let pop_tag = stackA.pop();
// 此时,tag 一定和 栈A 顶部是相同的
if (tag === pop_tag) {
let pop_arr = stackB.pop();
if (stackB.length > 0) {
stackB[stackB.length - 1].children.push(pop_arr);
}
} else {
throw new Error(`${
stackA[stackA.length - 1]} 标签没有封闭`);
}
// 移动结束标签的长度,由于 </.> 是两个字符,所以需要 + 3
index += tag.length + 3;
// console.log(stackA, stackB);
} else if (wordRegExp.test(rest)) {
// 识别到遍历的字符不是 文字(并且不能为全空)
let word = rest.match(wordRegExp)[1];
// 文字不能全是空
if (!/^\s+$/.test(word)) {
console.log(`检测到文字:${
word}`);
// 改变此时 stackB 中的栈顶元素
stackB[stackB.length - 1].children.push({
text: word, type: 3 });
console.log(stackB);
}
// 指针移动到文字的末尾
index += word.length;
} else {
index++;
}
// 未考虑文字在标签后面的情况,如:
// <p>123</p> hello
}
console.log(stackB);
return stackB[0].children[0]
}
属性の特定
parseAttrsString.js
: attrsString を attrs オブジェクトの配列に解析します
- 解析前の文字列:
class="box red" id="mybox"
- オブジェクトの解析済み配列:
[
{
name: 'class',
value: 'box red'
},
{
name: 'id',
value: 'mybox'
}
]
parseAttrsString アルゴリズムの核となる考え方:
- attrsStr をトラバースし、スペースが検出され、それが引用符で囲まれていない場合は、前のブレークポイントから現在のブレークポイントに文字列を追加して結果を出します
/**
* 把 attrsString 解析成 attrs 对象数组
*/
export default function parseAttrsString(attrsStr) {
if (!attrsStr) return []
let inFlag = false // 当前是否处于引号内
let point = 0 // 断点处
let result = []
// 遍历 attrsStr,不能直接用 split(),有如下情况 class="aa bb cc" id="gg"
for (let i = 0; i < attrsStr.length; i++) {
let c = attrsStr[i]
if (c === '"') inFlag = !inFlag // 遇到 双引号,切换 inFlag 状态
else if (c === ' ' && !inFlag) {
// 遇见 空格,且不在引号中
if (!/^\s*$/.test(attrsStr.substring(point, i))) {
// 不全为空格
result.push(attrsStr.substring(point, i).trim())
point = i
}
}
}
// 循环结束后,还剩一个属性
result.push(attrsStr.substring(point).trim())
// 将 ["k1=v1", "k2=v2"] 变为 [{name: k1, value: v1}, {name: k2, value: v2}]
result = result.map(item => {
const o = item.match(/^(.+)="(.+)"$/)
return {
name: o[1], value: o[2] }
})
return result
}
parse.js
:
export default function parse(templateStr) {
let rest = "";
// 开始标签的正则
const startRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/;
// 结束标签的正则
const endRegExp = /^\<\/([a-z]+[1-6]?)\>/;
// 结束标签前文字的正则(注意开头不含 <)
const wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/;
let stackA = [];
let stackB = [{
children: [] }];
let index = 0;
while (index < templateStr.length - 1) {
rest = templateStr.substring(index); // 更新剩余字符串
if (startRegExp.test(rest)) {
// 识别遍历到的字符,是 开始标签
let tag = rest.match(startRegExp)[1]; // 标签内容
let attrsString = rest.match(startRegExp)[2] // attr 内容
// console.log(`检测到开始标记:<${tag}>`);
stackA.push(tag); // 将开始标记推入 栈A
stackB.push({
tag: tag,
children: [],
attrs: parseAttrsString(attrsString) // 解析属性字符串
}); // 将空数组推入 栈B
// 移动开始标签的长度,由于 <> 是两个字符,所以需要 + 2,还需加上 attrs 的长度
index += tag.length + 2 + (attrsString?.length || 0);
// console.log(stackA, stackB);
} else if (endRegExp.test(rest)) {
// 识别遍历到的字符,是 结束标签
let tag = rest.match(endRegExp)[1]; // 标签内容
// console.log(`检测到结束标记:</${tag}>`);
let pop_tag = stackA.pop(); // 栈A 顶部元素
// 此时,tag 一定和 栈A 顶部是相同的
if (tag === pop_tag) {
let pop_arr = stackB.pop();
if (stackB.length > 0) {
stackB[stackB.length - 1].children.push(pop_arr);
}
} else {
throw new Error(`${
pop_tag} 标签没有封闭`);
}
// 移动结束标签的长度,由于 </> 是两个字符,所以需要 + 3
index += tag.length + 3;
// console.log(stackA, stackB);
} else if (wordRegExp.test(rest)) {
// 识别到遍历的字符,是 文字(并且不能为全空)
let word = rest.match(wordRegExp)[1];
// 文字不能全是空
if (!/^\s+$/.test(word)) {
// console.log(`检测到文字:${word}`);
// 改变此时 stackB 中的栈顶元素
stackB[stackB.length - 1].children.push({
text: word, type: 3 });
// console.log(stackB);
}
// 指针移动到文字的末尾
index += word.length;
} else {
index++;
}
// 未考虑文字在标签后面的情况,如:
// <p>123</p> hello
}
return stackB[0].children[0]
}