「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」
前言
在上篇文章我们分析了编译中parse
的部分代码,也就是parseHTML
的实现。在parseHTML
中通过逐字匹配将template
进行了初步解析。现在我们继续分析在parseHTML
中输出的结果是如何被parse
进行使用的。以此结束完整parse
流程的分析。
parse
我们依然从入口文件开始
const ast = parse(template.trim(), options)
我们来看看parse
的实现
const stack = []
let root
let currentParent
parseHTML(template, {
expectHTML: options.expectHTML,
// ...
start (tag, attrs, unary, start, end) {
// 解析开始标签调用的钩子
},
end (tag, start, end) {
// 解析结束标签调用的钩子
},
chars (text: string, start: number, end: number) {
// 解析文本节点调用的钩子
},
comment (text: string, start, end) {
// 解析注释节点调用的钩子
}
})
return root
可以发现,parse的实现主要是初始化一些钩子函数然后将其作为参数传递给parseHTML
。我们上篇分析了parseHTML
的实现,就是通过正则提取teamplate
的信息,将其标签属性提取出来,然后再调用parse
中的钩子。所以本篇文章的重点在钩子函数中是如何进行进一步处理的。
我们通过实例来分析
<div>
<div v-if="isShow" @click="doSomething" :class="activeClass">text{{name}}{{value}}text</div>
<div v-for="item in 10"></div>
</div>
start
start (tag, attrs, unary, start, end) {
// 1
let element: ASTElement = createASTElement(tag, attrs, currentParent)
// 2
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
// ...
// 3
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
processFor(element)
processIf(element)
processOnce(element)
}
// ...
// 4
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
}
简化后的satrt并不复杂,我们梳理下其实现
- 通过标签及属性数据创建节点AST
{
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []
}
-
执行
preTransforms
中暴露的函数,而preTransforms其实是收集了baseOptions
中modules
中相关的函数,这点和以前分析的vue渲染中的节点更新是相似的。 -
执行不同的
process
函数,通过函数名其实就可以发现,process是对一些指令如for
,if
之类的做进一步处理的。 -
前面我们定义了stack用于保存当前创建的节点栈,在创建之后将进其推入,并且将currentParent指向节点。
对于单个节点,我们来看看start
前后的数据对比
end
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
closeElement(element)
end的主代码很简单,就是将刚才start中推入的节点推出,同时更新currentParent,此时表示当前节点标签已经闭合且处理完毕。closeElement则是会做一些额外的校验及调用之类的,这边不作分析。
chars
chars (text: string, start: number, end: number) {
const children = currentParent.children
//...
if (text) {
let res
let child: ?ASTNode
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
}
if (child) {
children.push(child)
}
}
}
chars用于处理文本信息,主要的是调用parseText
对文本中的字符串进行解析,提取其中的变量。我们来看看其实现
export function parseText (
text: string,
delimiters?: [string, string]
): TextParseResult | void {
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
if (!tagRE.test(text)) {
return
}
// 1
const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
// 2
while ((match = tagRE.exec(text))) {
index = match.index
// push text token
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index))
tokens.push(JSON.stringify(tokenValue))
}
// tag token
const exp = parseFilters(match[1].trim())
tokens.push(`_s(${exp})`)
rawTokens.push({ '@binding': exp })
lastIndex = index + match[0].length
}
// 3
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex))
tokens.push(JSON.stringify(tokenValue))
}
// 4
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
parseText
的实现也并不复杂,有点像parseHTML
,实际就是将文本节点进一步token
化处理
-
定义了一些输出变量以及遍历文本需要的临时变量
-
循环匹配文本,通过匹配
{{}}
(当然如果传递其它delimiters会有所不同)来提取遍历,同时通过匹配位置判断其前面是否还有字符串,有则一并提取。当然匹配的变量会添加_s()
。不用多想_s()
实际是会在后面用于渲染时执行的函数。 -
进行结尾处理,也就是
{{name}}xxxx
这样情况下的xxx
文本。 -
将提取的表达式及token返回,我们来看看其输入输出值的区别。
comment
最后再看看注释节点的处理,就是使用对应的节点变量存储text,非常简单
comment (text: string, start, end) {
if (currentParent) {
const child: ASTText = {
type: 3,
text,
isComment: true
}
currentParent.children.push(child)
}
}
其它
我们在前面分析了parse
的主要流程,感觉内容不算复杂。但实际parse
中是包含很多内容的,因为我们跳过了很多指令处理的逻辑如v-if
,v-for
,v-pre
,v-slot
,v-esle
,v-elseif
,v-model
等。它们的处理逻辑主要在各自的process
函数中,我们将v-for
作为例子来分析下其处理
processFor
export function processFor (el: ASTElement) {
let exp
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
const res = parseFor(exp)
if (res) {
extend(el, res)
}
}
}
processFor的主要逻辑就是通过节点的attrsMap
判断是否存在v-for
指令,如果存在就进一步解析其值。将解析结果合并到element
。
我们再来看看parseFor
export function parseFor (exp: string): ?ForParseResult {
const inMatch = exp.match(forAliasRE)
if (!inMatch) return
const res = {}
res.for = inMatch[2].trim()
const alias = inMatch[1].trim().replace(stripParensRE, '')
const iteratorMatch = alias.match(forIteratorRE)
if (iteratorMatch) {
res.alias = alias.replace(forIteratorRE, '').trim()
res.iterator1 = iteratorMatch[1].trim()
if (iteratorMatch[2]) {
res.iterator2 = iteratorMatch[2].trim()
}
} else {
res.alias = alias
}
return res
}
parseFor的逻辑实际就是解析开发者定义的如item in list
将其分割处理并返回其对象,内容存在属性alias
及for
等属性中。
我们来看看其处理前后的节点
AST
我们再拉看看我们的模板最终生成的AST代码
<div>
<div v-if="isShow" @click="doSomething" :class="activeClass">text{{name}}{{value}}text</div>
<div v-for="item in 10"></div>
</div>
const ast = parse(template.trim(), options)
总结
本篇文章分析了vue编译的第一步,将template
编译成AST
。发现对比babel
那种对于JS代码的AST
生成实际是简单不少的,其原因在于将模板的解析主要是按顺序匹配标签及属性即可,而对于代码的解析要考虑的东西就特别多,尤其是得考虑语法逻辑的处理。后面我们将继续分析编译的第二步AST的转化
。