Vue源码学习之实现模板转化成ast语法树(下)
接上文解析模板参数完成以后,我们就成功拿到模板可以开始进行处理了,那么接下来就开始对compileToFcuntion()
开始耕种。
第一步 编写正则表达式
在我们拿到模板之后,我们需要对标签、文本、属性、表达式、字符串等进行匹配,那就需要用到我们的正则表达式,如下:
// compiler/index.js
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`);
console.log(startTagOpen);
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const startTagClose = /^\s*(\/?)>/;
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
输出一下,用正则可视化工具来看一下规则如下:
startTagOpen
:开始为<
,然后里面是匹配名字div
或者带命名空间的div:xxx
等这样的开始标签的名字
endTag
:结束标签</xxx>
,最终匹配到的分组就是结束标签的名字
attribute
:用来匹配属性的,属性前面可以有一些空白white space
,不能是None of
内的符号,中间是=
,左右于两边可以有空白字符,左右单引号中间不是单引号,左右双引号中间不是双引号,也就是说第一个分组就是属性的key,value是分组3/4/5
startTagClose
:匹配到的就是一个反斜杠/
,可能是</div>
或者<br/>
defaultTagRE
:匹配的是双大括号{{}}
,两边是大括号,中间是换行或者回车以及任何字符
第二步 将模板转换成ast语法树
有了上面的正则之后,我们就需要去对模板进行匹配,然后将匹配到的字符串转换成ast语法树。
// compiler/index.js
// 对模板进行编译处理
......
function parseHTML(html) {
const ELEMENT_TYPE = 1
const TEXT_TYPE = 3
const stack = [] // 用于存放元素
let currentParent; // 指向栈中最后一个
let root
function createdASTElement(tag, attrs) {
return {
tag,
type: ELEMENT_TYPE,
children: [],
attrs,
parent: null
}
}
// 开始标签
function start(tag, attrs) {
let node = createdASTElement(tag, attrs) // 创造一个ast节点
if (!root) { // 看一下是否为空树
root = node // 如果为空则表示当前是树的根节点
}
if (currentParent) {
node.parent = currentParent // 只赋了parent属性
currentParent.children.push(node) // 还需要让父亲记住自己
}
stack.push(node)
currentParent = node //currentParent为栈中的最后一个
}
// 文本
function chars(text) { // 文本直接放到当前指向的节点中
text = text.replace(/\s/g,'')
&& currentParent.children.push({
type: TEXT_TYPE,
text,
parent: currentParent
})
}
// 结束标签
function end(tag) {
stack.pop() // 弹出最后一个
currentParent = stack[stack.length - 1]
}
function advance(n) {
html = html.substring(n)
}
function parseStartTag() {
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1], // 标签名
attrs: []
}
advance(start[0].length)
// 如果不是开始标签的结束就一直匹配下去
let attr, end
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length)
match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] || true })
}
if (end) {
advance(end.length)
}
return match
}
return false // 不是开始标签
}
while (html) { // html最开始肯定是一个 <
// 如果textEnd为0说明是一个开始标签或者结束标签
// 如果textEnd大于0则说明是文本结束的位置
let textEnd = html.indexOf('<') // 如果indexOf的索引是0则说明是个标签
if (textEnd === 0) {
const starttagMatch = parseStartTag() //开始标签的匹配
if (starttagMatch) { // 解析到的开始标签
start(starttagMatch.tagName, starttagMatch.attrs)
continue
}
let endTagmatch = html.match(endTag)
if (endTagmatch) {
advance(endTagmatch[0].length)
end(endTagmatch[1])
continue
}
}
if (textEnd > 0) {
let text = html.substring(0, textEnd) // 文本内容
if (text) {
chars(text)
advance(text.length) // 解析到的文本
}
}
}
console.log(root);
return root
}
export function compileToFcuntion(template) {
// 1.将template转换为ast语法树
let ast = parseHTML(template)
// 2.生成render方法 render方法执行后的结果就是虚拟DOM
console.log(template);
}
思路:
- 通过
parseHTML(template)
方法传入模板,将模板转换成ast语法树,解析开始标签、结束标签等内容,通过这个方法就会返回一个ast语法树。 - 在
parseHTML()
方法中,每解析一个标签则在模板的字符串中将其截取掉,直到为空,所以可以写一个while
循环来完成 - html肯定是
<
开头的,所以我们直接用indexOf
方法来进行判断:如果textEnd为0说明是一个开始标签或者结束标签,如果textEnd大于0则说明是文本结束的位置。 - 如果
textEnd
等于0就认为他是开始标签,那就用parseStartTag
方法来解析开始标签。如下图接下来就可以塞到match
里面。 - 追加完成之后就需要去解析属性,那我们就需要先将已经匹配完成的标签进行删除,通过
advance(start[0].length)
来进行截取,如下图就说明截取成功了,截取成功之后就可以去匹配属性
- 在匹配属性的过程中,只要不是开始标签的结束就可以一直进行匹配,所以可以通过
while
,如果不是开始标签的结束就一直匹配下去,并且将每次拿到的属性进行保存,每次匹配完在将其截掉,最后还有一个>符号,所以在匹配结束以后end可能就会有值,所以再把end删掉
7. 紧接着我们需要将属性解析出来放到attr
里面通过match.attrs.push
,最后再将match
return出去
8. 然后就可以处理文本内容,当textEnd>0
说明有文本,继续进行截取。
9. 整个过程就是遇到开始标签处理开始标签,遇到文本处理文本,遇到结束标签处理结束标签,当最后循环能够终止输出为空,就说明处理成功了 9. 现在还没有替换文本只是把字符串删掉了,那就还需要几个方法将它们暴露出去,也可以用htmlparser2去解析html
9. 最终我们想要转换成一棵抽象语法树,树肯定需要有层级关系,那我们就需要构建父子关系,我们可以做一个栈形结构,刚开始匹配到标签的时候将这个标签放进去,之后当匹配下一个标签的时候,就知道这个标签是栈中最后一个标签的儿子,当遇到结束标签的时候就将栈中最后一个弹出去,通过这种方式来模拟出父子关系:
- 知道方法之后,就开始定义元素类型
ELEMENT_TYPE
、文本类型TEXT_TYPE
,栈stack
,指针currentParent
,根节点root
- 当遇到开始标签的时候就创建一个AST元素,用
createdASTElement
方法,然后还需要去判断产生的节点是否是根节点root,如果没有根节点这个节点就是根节点。然后将这个节点放进栈中,同时还需要当前这个指针指向栈中的最后一个。如果currentParent
有值就需要让当前节点的父亲等于currentParent
,并将node值赋给currentParent.children
- 当遇到文本的时候,这个文本就是当前元素的孩子,直接找到
currentParent.children
放进去 - 当遇到结束标签的时候,直接弹出栈中最后一个,然后更新
currentParent
指针,在end中也可以进行校验标签是否合法。
这个时候输出发现children是空的,可以在开始的时候chars
执行的时候将空格去掉text = text.replace(/\s/g,'')
再次执行输出如下:
最终
到此,利用栈形结构,我们这棵树就构建完成了。
转载自:https://juejin.cn/post/7148735327081857037