-
抽象语法树介绍
vue在mount过程中,会将template编译成ast抽象语法树,是源代码的抽象语法结构的树状表现形式
虚拟dom的实现思路
vue 中使用了虚拟dom(vitrualdom),来模拟dom树,通过操作dom树数据来操作dom提高了dom操作的性能
具体流程
template 转换成 ast抽象语法树 ast抽象语法树再转换成 render函数 最终返回一个vnode
具体代码
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // aa-aa
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //aa:aa
/*
?: 匹配不捕获
*/
const defautTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 可以匹配到标签名 [1]
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); //[0] 标签的结束名字
// style="xxx" style='xxx' style=xxx
const attribute =
/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 匹配属性的 aaa="aaa" aa='aaa' a = aaa
const startTagClose = /^\s*(\/?)>/; //匹配标签结束的
// ast 语法树
// let obj = {
// tag: 'div',
// type: 1,//nodeType
// attrs: [
// {
// style:"color:red"
// }
// ],
// children: [
// {
// tag: 'span',
// type: 1,
// attrs: [],
// children,
// parent
// }
// ],
// parent:null
// }
//vue3里面支持多个根元素(外层加了一个空元素)vue2中只有一个根节点
export function parseHTML(html) {
function createASTElement(tag, attrs) {
return {
tag,
type: 1,
children: [],
attrs,
parent,
};
}
let root = null;
let currentParent;
let stack = [];// //这里和上面的parse函数一样用到stack这个数组 不过这里的stack只是为了了简单存放标签名 为了和结束标签进行匹配的作用
//根据开始标签,结束标签 ,文本内容 生成一个ast语法树
function start(tagName, attrs) {//tagName 标签名 attrs 传入的属性名
debugger;
let element = createASTElement(tagName, attrs);//创建ast 元素
if (!root) {
//没有树根 是最外层元素
root = element;
}
currentParent = element; //更新父元素
stack.push(element);
}
function end(tagName) {
debugger;
let element = stack.pop(); //遇到结束标签 删掉
currentParent = stack[stack.length - 1];
if (currentParent) {
element.parent = currentParent;
currentParent.children.push(element);
}
}
function chars(text) {
debugger;
text = text.replace(/\s/g, "");
if (text) {
currentParent.children.push({
type: 3,
text,
});
}
}
function advance(n) {
debugger;
html = html.substring(n);
}
function parseStartTag() {
debugger;
const start = html.match(startTagOpen);//通过正则匹配开始标签的具体信息
if (start.length>0) {
let match = {
tagName: start[1],//标签名
attrs: [],//属性
};
advance(start[0].length);//截取掉开始标签,即删除掉开始标签,然后在处理剩下的标签
console.log(html, match);
let end, attr;
while (
!(end = html.match(startTagClose)) &&//不是闭合标签
(attr = html.match(attribute))// 当前标签的属性赋值给 attr
) {
// advance(attr[0].length);
console.log("95", attr, html);
match.attrs.push({
name: attr[1],//attr[1] 是属性名
value: attr[3] || attr[4] || attr[5] || true, //属性值
});
console.log("attrs", match);
advance(attr[0].length);//将标签中的属性字符串删除掉
}
if (end) {//在处理完标签的开始标签和属性之后并删除之后,删除 闭合标签 >
advance(end[0].length);
return match;
}
}
}
while (html) {
// 传入的template 的html片段
debugger;
let textEnd = html.indexOf("<");
if (textEnd == 0) {
// textEnd的值为0 表示 是开始标签
// 先解析开始 然后解析结束 调用解析开始标签的 parseStartTag()
let startTagMatch = parseStartTag();
console.log("startTagMatch", startTagMatch);
console.log(html);
if (startTagMatch) {
// 开始标签
console.log("开始", startTagMatch.tagName);
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}
// 结束标签
let endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
end(endTagMatch[1]);
continue;
}
}
let text;
if (textEnd > 0) {
//开始解析文本
text = html.substring(0, textEnd);
}
if (text) {
advance(text.length);
chars(text);
console.log(text);
}
break;
}
return root;
}
while (html) {
// 传入的template 的html片段
debugger;
let textEnd = html.indexOf("<");
if (textEnd == 0) {
// textEnd的值为0 表示 是开始标签
// 先解析开始 然后解析结束 调用解析开始标签的 parseStartTag()
let startTagMatch = parseStartTag();
console.log("startTagMatch", startTagMatch);
console.log(html);
if (startTagMatch) {
// 开始标签
console.log("开始", startTagMatch.tagName);
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}
// 结束标签
let endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
end(endTagMatch[1]);
continue;
}
}
let text;
if (textEnd > 0) {
//开始解析文本
text = html.substring(0, textEnd);
}
if (text) {
advance(text.length);
chars(text);
console.log(text);
}
break;
}
return root;
}
- 判断传入的html字符串开始是不是 < 开始标签 如果是就调用 parseStartTag 方法解析
parseStartTag 解析开始标签
function parseStartTag() {
debugger;
const start = html.match(startTagOpen);//通过正则匹配开始标签的具体信息
if (start.length>0) {
let match = {
tagName: start[1],//标签名
attrs: [],//属性
};
advance(start[0].length);//截取掉开始标签,即删除掉开始标签,然后在处理剩下的标签
console.log(html, match);
let end, attr;
while (
!(end = html.match(startTagClose)) &&//不是闭合标签
(attr = html.match(attribute))// 当前标签的属性赋值给 attr
) {
// advance(attr[0].length);
console.log("95", attr, html);
match.attrs.push({
name: attr[1],//attr[1] 是属性名
value: attr[3] || attr[4] || attr[5] || true, //属性值
});
console.log("attrs", match);
advance(attr[0].length);//将标签中的属性字符串删除掉
}
if (end) {//在处理完标签的开始标签和属性之后并删除之后,删除 闭合标签 >
advance(end[0].length);
return match;
}
}
}
具体实现思路
通过html.match()解析开始标签的数据
- 如果start的长度大于0 说明开始标签解析成功
- 就去start的索引为1的数据作为 标签的名字,并且定义一个attrs属性数组
- 将调用advance字符串的开始标签删除,然继续处理剩下的属性和闭合标签字符串
- 如果剩下的字符串的开头不是闭合标签并且是属性,将属性解析
- 并将解析出来属性值组成一个属性对象并添加到match的属性数组中
- 并删除属性字符串,继续下边的处理
- 如果是闭合标签 删除掉
- 并将 match 对象返回
返回的match数据
start 方法
function start(tagName, attrs) {//tagName 标签名 attrs 传入的属性名
debugger;
let element = createASTElement(tagName, attrs);//创建ast 元素
if (!root) {
//没有树根 是最外层元素
root = element;
}
currentParent = element; //更新父元素
stack.push(element);
}
使用 createASTElement 创建ast元素
- 判断有没有根元素,如果没有就将最外层作为root根元素
- 并将创建的ast元素作为当前的父元素,也就是更新父元素
- 将当前元素添加到 stack 栈中
end 方法
function end(tagName) {
debugger;
let element = stack.pop(); //遇到结束标签 删掉
currentParent = stack[stack.length - 1];//更新当前的父亲节点
if (currentParent) {
element.parent = currentParent;
currentParent.children.push(element);
}
}
chars 处理文本的方法
function chars(text) {
debugger;
text = text.replace(/\s/g, "");
if (text) {
currentParent.children.push({
type: 3,
text,
});
}
}
入宫是文本 直接将 空格、制表符、换页符 替换问 空字符 并且设置元素类型为 文本节点添加到currentParent的 children 中
advance 删除字符串
function advance(n) {
debugger;
html = html.substring(n);
}