本文已参与「新人创作礼」活动,一起开启掘金创作之路。
上一节我们已经介绍了start函数解析开始标签的过程,初始化了AST树并解析了开始标签上的各种指令丰富了AST树,不清楚可以点击这里。本节我们来分析结束标签的end函数与文本字符串的char函数以及注释节点的comment函数的解析过程,关于结束标签与文本标签的编译大致流程之前已经分析过,不清楚的可以点击这里。
Vue 编译(compile)核心流程之parse
end函数
end () {
// remove trailing whitespace
// 移除尾随的最后一个空格
const element = stack[stack.length - 1]
const lastNode = element.children[element.children.length - 1]
// 如果最后一个节点存在且类型为文本节点,且文本内容是空格,且不是预编译的条件下
if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
// 移除这个空格节点
element.children.pop()
}
// pop stack
// 栈推出
// stack长度减1,stack是管理ast树而创建的一个栈数组,当匹配上了stack这个栈之后,长度会减1
stack.length -= 1
currentParent = stack[stack.length - 1]
closeElement(element)
},
function closeElement (element) {
// check pre state
// 检查pre属性,当开始标签有pre属性的话,pre为true,结束标签匹配上后,
// 开始标签pop出stack栈,inVPre恢复false状态
if (element.pre) {
inVPre = false
}
// 原理同pre标签
if (platformIsPreTag(element.tag)) {
inPre = false
}
// apply post-transforms
// 遍历执行postTransforms函数数组,web平台这个函数数组为空
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
}
end函数主要是进行stack栈的管理,每匹配到结束标签,会将stack数组pop出一个与结束标签匹配的开始标签,直到完成template的扫描,stack被清空。
char函数
chars(text: string) {
// (没有父节点,纯文本的情况下),(有父节点,文本定义在外面的情况下)会报错
if (!currentParent) {
if (process.env.NODE_ENV !== 'production') {
// 没有父节点,纯文本的情况下,报错
if (text === template) {
warnOnce(
'Component template requires a root element, rather than just text.'
)
// 有父节点,文本定义在外面的情况下,报错
} else if ((text = text.trim())) {
warnOnce(
`text "${text}" outside root element will be ignored.`
)
}
}
return
}
// IE textarea placeholder bug
// IE placeholder bug 的处理的逻辑,不太重要
/* istanbul ignore if */
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
const children = currentParent.children
// 对text进行处理
text = inPre || text.trim()
// 存在inPre,执行下面逻辑
? isTextTag(currentParent) ? text : decodeHTMLCached(text)
// only preserve whitespace if its not right after a starting tag
// 不存在inpre
: preserveWhitespace && children.length ? ' ' : ''
// text存在
if (text) {
let res
// 没有v-pre的节点,text不为空,且解析出的text文本有结果,这儿生成的是表达式节点
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
children.push({
type: 2,
expression: res.expression,
tokens: res.tokens,
text
})
// text不是空节点,为纯文本节点
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
// children push一个纯文本节点
children.push({
type: 3,
text
})
}
}
},
如果是含有插值表达式的文本节点,会执行parseText:
export function parseText (
text: string,
delimiters?: [string, string]
): TextParseResult | void {
// 默认的分隔符 是{{}},也可以自己配置分隔符
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
// 如果没有匹配到分隔符,说明是一个纯文本节点,直接返回
if (!tagRE.test(text)) {
return
}
const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
// 循环匹配表达式,如{{name}}
// match的结果:['{{name}}', 'name', index: 0, input: '{{name}}', groups: undefined]
while ((match = tagRE.exec(text))) {
index = match.index
// push text token
// 当index大于lastIndex 说明匹配上了纯文本,纯文本的位置在index与lastIndex中间
if (index > lastIndex) {
// 进入这个逻辑,将纯文本推入rawTokens和tokens
rawTokens.push(tokenValue = text.slice(lastIndex, index))
tokens.push(JSON.stringify(tokenValue))
}
// tag token
// 有filter的时候,解析filter
const exp = parseFilters(match[1].trim())
// 将解析的表达式拼接成`_s(name)`这种形式,推入tokens
tokens.push(`_s(${exp})`)
// 将解析的表达式转换成对象{ '@binding': name }这种形式,推入rawTokens
rawTokens.push({ '@binding': exp })
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex))
tokens.push(JSON.stringify(tokenValue))
}
return {
// expression是一个拼接的表达式
expression: tokens.join('+'),
// 这个就是含有对象{ '@binding': exp }的数组
tokens: rawTokens
}
}
char会将文本分成两种情况来解析,一种情况是含有变量的插值表达式,这种情况下会在AST element上面生成expression和tokens属性,expression属性是一段拼接的属性字符串(例如_s(name)+":<"+_s(age));tokens是一个数组(例如[{@binding:"name"},":<",{@binding:"age"}])。
另一种情况是纯文本的表达式,这种情况直接生成纯文本节点。
comment函数
// 生成注释节点
comment (text: string) {
currentParent.children.push({
type: 3,
text,
isComment: true
})
}
commnet函数就是给AST element上面添加isComment:true的属性,生成一个注释节点。
到此为止,parse的整体流程大致已经清楚了,接下来我们来看看optimize(优化ast树)的流程是怎么样的。