Vue2剥丝抽茧-模版编译之生成AST

开发 前端
因为 dom 是一对一对出现的,一对中可能又包含多对,因此和括号匹配一样,这里就缺少不了栈了。遇到开始标签就入栈,遇到结束标签就出栈,这样就可以保证栈顶元素始终是后续节点的父节点。

​AST 结构

AST​ 即抽象语法树,在 虚拟dom、eslint、babel​ 都有接触过了,简单来说就是一种描述 dom​ 的数据结构。通过 AST​ 可以还原 dom​ ,也可以把 dom​ 转为 AST 。

因为是树的结构,所以肯定有一个 children​ 字段来保存子节点,同时有 parent​ 来保存父节点。其他的话还有 tag​ 名,节点类型,type = 1​ 代表普通节点类型,type=3​ 表示普通文本类型,type=2 代表有插值的的文本类型。

提供一个函数 createASTElement​ 来生成 dom 节点,后续会用到。

export function createASTElement(tag, parent) {
return {
type: 1,
tag,
parent,
children: [],
};
}

因为 dom 是一对一对出现的,一对中可能又包含多对,因此和括号匹配一样,这里就缺少不了栈了。

遇到开始标签就入栈,遇到结束标签就出栈,这样就可以保证栈顶元素始终是后续节点的父节点。

举个例子,对于 <div><span>3<5吗</span><span>?</span></div> 。

1
div span 3<5/span span ? /span /div
^
stack:[div],当前栈顶 div,后续节点为 div 的子节点

2
div span 3<5/span span ? /span /div
^
stack:[div, span],当前栈顶 span,后续节点为 span 的子节点

3
div span 3<5/span span ? /span /div
^
stack:[div, span],当前栈顶 span,3<5吗 属于 span

4
div span 3<5/span span ? /span /div
^
stack:[div],遇到结束节点 span,栈顶的 span 去掉,后续节点为 div 的子节点

5
div span 3<5/span span ? /span /div
^
stack:[div, span],当前栈顶 span,后续节点为 span 的子节点

6
div span 3<5/span span ? /span /div
^
stack:[div, span],当前栈顶 span,? 属于 span

7
div span 3<5/span span ? /span /div
^
stack:[div],遇到结束节点 span,栈顶的 span 去掉,后续节点为 div 的子节点

8
div span 3<5/span span ? /span /div
^
stack:[],遇到结束节点 div,栈顶的 div 去掉,遍历结束

整体算法

添加 stack​ 变量、添加 currentParent​ 保存当前的父节点、添加 root 保存根节点。

let root;
let currentParent;
let stack = [];

接下来完善 模版编译之分词​ 中遗留的 start​ 、end、chars 三个回调函数。

parseHTML(template, {
start: (tagName, unary, start, end) => {
console.log("开始标签:", tagName, unary, start, end);
},
end: (tagName, start, end) => {
console.log("结束标签:", tagName, start, end);
},
chars: (text, start, end) => {
console.log("文本:", text, start, end);
},
});

start 函数

start: (tagName, unary, start, end) => {
let element = createASTElement(tagName, currentParent);
if (!root) {
root = element;
}

if (!unary) {
currentParent = element;
stack.push(element);
} else {
closeElement(element);
}
},

先调用 createASTElement​ 生成一个 AST​ 节点,如果当前是第一个开始节点,就将 root 赋值,接下来判断是否是出一元节点。

如果是一元节点直接调用 closeElement ,将当前节点加入到父节点中。

function closeElement(element) {
if (currentParent) {
currentParent.children.push(element);
element.parent = currentParent;
}
}

end 函数

end: (tagName, start, end) => {
const element = stack[stack.length - 1];
// pop stack
stack.length -= 1;
currentParent = stack[stack.length - 1];
closeElement(element);
},

出栈,更新当前父节点,并且将出栈的元素加入到父节点中。

chars 函数

chars: (text, start, end) => {
if (!currentParent) {
return;
}
const children = currentParent.children;
if (text) {
let child = {
type: 3,
text,
};
children.push(child);
}
},

这里只考虑了 type​ 为 3 的普通文本节点。

整体代码

结合 模版编译之分词 中实现的分词,整体代码如下:

const unicodeRegExp =
/a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`);
const startTagClose = /^\s*(\/?)>/;
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
export function createASTElement(tag, parent) {
return {
type: 1,
tag,
parent,
children: [],
};
}
export function parseHTML(html, options) {
let index = 0;
while (html) {
let textEnd = html.indexOf("<");
if (textEnd === 0) {
// Start tag:
const startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
continue;
}
// End tag:
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1], curIndex, index);
continue;
}
}

let text, rest, next;
if (textEnd >= 0) {
rest = html.slice(textEnd);
while (!endTag.test(rest) && !startTagOpen.test(rest)) {
// < in plain text, be forgiving and treat it as text
next = rest.indexOf("<", 1);
if (next < 0) break;
textEnd += next;
rest = html.slice(textEnd);
}
text = html.substring(0, textEnd);
}

if (textEnd < 0) {
text = html;
}

if (text) {
advance(text.length);
}

if (options.chars && text) {
options.chars(text, index - text.length, index);
}
}

function advance(n) {
index += n;
html = html.substring(n);
}

function parseStartTag() {
const start = html.match(startTagOpen);
if (start) {
const match = {
tagName: start[1],
attrs: [],
start: index,
};
advance(start[0].length);
let end = html.match(startTagClose);
if (end) {
match.unarySlash = end[1];
advance(end[0].length);
match.end = index;
return match;
}
}
}

function handleStartTag(match) {
const tagName = match.tagName;
const unarySlash = match.unarySlash;
const unary = !!unarySlash;
options.start(tagName, unary, match.start, match.end);
}

function parseEndTag(tagName, start, end) {
options.end(tagName, start, end);
}
}
const template = "<div><span>3<5吗</span><span>?</span></div>";
console.log(template);

function parse(template) {
let root;
let currentParent;
let stack = [];
function closeElement(element) {
if (currentParent) {
currentParent.children.push(element);
element.parent = currentParent;
}
}
parseHTML(template, {
start: (tagName, unary, start, end) => {
let element = createASTElement(tagName, currentParent);
if (!root) {
root = element;
}

if (!unary) {
currentParent = element;
stack.push(element);
} else {
closeElement(element);
}
},
end: (tagName, start, end) => {
const element = stack[stack.length - 1];
// pop stack
stack.length -= 1;
currentParent = stack[stack.length - 1];
closeElement(element);
},
chars: (text, start, end) => {
if (!currentParent) {
return;
}
const children = currentParent.children;
if (text) {
let child = {
type: 3,
text,
};
children.push(child);
}
},
});
return root;
}
const ast = parse(template);
console.log(ast);

输入 <div><span>3<5吗</span><span>?</span></div> 。

最终生成的 AST 结构如下:

图片

总结

这篇文章实现了最简单情况的 AST​ 生成,了解了整个的结构,下一篇文章会通过 AST​ 生成 render 函数。

责任编辑:武晓燕 来源: windliang
相关推荐

2022-04-06 07:28:47

数组响应式系统

2022-03-29 09:59:58

响应式系统Vue2

2022-04-02 09:56:41

Vue2响应式系统

2022-04-14 08:46:46

响应式系统js

2022-04-03 19:27:35

Vue2响应式系统

2022-04-12 10:05:18

响应式系统异步队列

2022-03-31 10:15:10

分支切换响应式系统

2022-04-10 11:04:40

响应式系统setdelete

2023-07-12 13:25:17

Vue 2模版编译

2024-03-07 12:54:06

数据分析师企业

2023-03-02 11:51:00

数据分析师企业

2019-04-25 14:20:56

数据分析套路工具

2024-03-15 11:47:19

Vue2前端权限控制

2023-11-19 18:53:27

Vue2MVVM

2016-10-19 20:47:55

vuevue-cli移动端

2020-09-25 07:40:39

技术开发选型

2022-12-07 10:34:45

AST前端编译

2022-06-29 16:59:21

Vue3Vue2面试

2024-03-01 08:38:34

WebpackVue2sass

2021-07-06 09:29:38

Cobar源码AST
点赞
收藏

51CTO技术栈公众号