Vue 的编译模块包含 4 个目录:
compiler-core
compiler-dom // 浏览器
compiler-sfc // 单文件组件
compiler-ssr // 服务端渲染
其中 compiler-core 模块是 Vue 编译的核心模块,并且是平台无关的。而剩下的三个都是在 compiler-core 的基础上针对不同的平台作了适配处理。
Vue 的编译分为三个阶段,分别是:parse、transform、codegen。
其中 parse 阶段将模板字符串转化为语法抽象树 AST。transform 阶段则是对 AST 进行了一些转换处理。codegen 阶段根据 AST 生成对应的 render 函数字符串。
Parse
Vue 在解析模板字符串时,可分为两种情况:以 <
开头的字符串和不以 <
开头的字符串。
不以 <
开头的字符串有两种情况:它是文本节点或 {
{ exp }}
插值表达式。
而以 <
开头的字符串又分为以下几种情况:
- 元素开始标签
<div>
- 元素结束标签
</div>
- 注释节点
<!-- 123 -->
- 文档声明
<!DOCTYPE html>
用伪代码表示,大概过程如下:
while (s.length) {
if (startsWith(s, '{
{')) {
// 如果以 '{
{' 开头
node = parseInterpolation(context, mode)
} else if (s[0] === '<') {
// 以 < 标签开头
if (s[1] === '!') {
if (startsWith(s, '<!--')) {
// 注释
node = parseComment(context)
} else if (startsWith(s, '<!DOCTYPE')) {
// 文档声明,当成注释处理
node = parseBogusComment(context)
}
} else if (s[1] === '/') {
// 结束标签
parseTag(context, TagType.End, parent)
} else if (/[a-z]/i.test(s[1])) {
// 开始标签
node = parseElement(context, ancestors)
}
} else {
// 普通文本节点
node = parseText(context, mode)
}
}
在源码中对应的几个函数分别是:
parseChildren()
,主入口。parseInterpolation()
,解析双花插值表达式。parseComment()
,解析注释。parseBogusComment()
,解析文档声明。parseTag()
,解析标签。parseElement()
,解析元素节点,它会在内部执行parseTag()
。parseText()
,解析普通文本。parseAttribute()
,解析属性。
每解析完一个标签、文本、注释等节点时,Vue 就会生成对应的 AST 节点,并且会把已经解析完的字符串给截断。
对字符串进行截断使用的是 advanceBy(context, numberOfCharacters)
函数,context 是字符串的上下文对象,numberOfCharacters 是要截断的字符数。
我们用一个简单的例子来模拟一下截断操作:
<div name="test">
<p></p>
</div>
首先解析 <div
,然后执行 advanceBy(context, 4)
进行截断操作(内部执行的是 s = s.slice(4)
),变成:
name="test">
<p></p>
</div>
再解析属性,并截断,变成:
<p></p>
</div>
同理,后面的截断情况为:
></p>
</div>
</div>
<!-- 所有字符串已经解析完 -->
AST 节点
所有的 AST 节点定义都在 compiler-core/ast.ts 文件中,下面是一个元素节点的定义:
export interface BaseElementNode extends Node {
type: NodeTypes.ELEMENT // 类型
ns: Namespace // 命名空间 默认为 HTML,即 0
tag: string // 标签名
tagType: ElementTypes // 元素类型
isSelfClosing: boolean // 是否是自闭合标签 例如 <br/> <hr/>
props: Array<AttributeNode | DirectiveNode> // props 属性,包含 HTML 属性和指令
children: TemplateChildNode[] // 字节点
}
一些简单的要点已经讲完了,下面我们再从一个比较复杂的例子来详细讲解一下 parse 的处理过程。
<div name="test">
<!-- 这是注释 -->
<p>{
{ test }}</p>
一个文本节点
<div>good job!</div>
</div>
上面的模板字符串假设为 s,第一个字符 s[0] 是 &l