「这是我参与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的转化
。