上篇文章中我们提到 Vue 调用 compile 方法编译 template 模板得到 render code (而不是 render function),然后 rumtime 执行 render code 得到 render function 的过程。现在让我们具体看一下 Vue3 如何编译 template 得到 render code。
1.template 的编译流程
Vue3 的模板编译的流程和 babel 是类似的,都大致可以分为 parse、transform、generate 三个阶段。
- parse: 解析阶段,将模板解析成原始AST,此时的AST只是相当于对模板进行了傻瓜式翻译,并没有实际的可用性
- transform: 转换阶段,将原始AST通过各转换插件转换出可用于渲染的目标AST,同时为各节点创建代码生成器,同时注入运行时优化信息
- generate: 生成阶段,通过代码生成器生成最终的可运行代码段和其他运行时信息
TODO:compiler编译流程.jpeg
2.编译系统的入口
上篇文章中我们提到,runtime 最终调用的是 packages/compiler-dom/src/index.ts#compile 来编译 template。
import {
baseCompile,
baseParse,
CompilerOptions,
CodegenResult,
ParserOptions,
RootNode,
noopDirectiveTransform,
NodeTransform,
DirectiveTransform
} from '@vue/compiler-core'
// ...
export function compile(
template: string,
options: CompilerOptions = {}
): CodegenResult {
return baseCompile(
template,
extend({}, parserOptions, options, {
nodeTransforms: [
// ignore <script> and <tag>
// this is not put inside DOMNodeTransforms because that list is used
// by compiler-ssr to generate vnode fallback branches
ignoreSideEffectTags,
// 传入适用于 DOM 环境的 DOMNodeTransforms,用于 node 节点及属性的转换
...DOMNodeTransforms,
...(options.nodeTransforms || [])
],
directiveTransforms: extend(
{},
// 传入适用于 DOM 环境的 DOMDirectiveTransforms,用于指令的编译
DOMDirectiveTransforms,
options.directiveTransforms || {}
),
// __BROWSER__ 判断,这是因为有些优化只能在 node 进行,对于浏览器运行时进行编译是没办法做到的。
transformHoist: __BROWSER__ ? null : stringifyStatic
})
)
}
packages/compiler-dom/src/index.ts#compile 又调用了 @vue/compiler-core#baseCompile 并传入适用于相应平台的默认的 transform 方法来进行模板编译。
// ...
// 获取预置的 transformer 插件集
export function getBaseTransformPreset(
prefixIdentifiers?: boolean
): TransformPreset {
return [
// 生成目标节点结构的转换插件
[
transformOnce,
transformIf,
transformFor,
...(!__BROWSER__ && prefixIdentifiers
? [
// order is important
trackVForSlotScopes,
transformExpression
]
: __BROWSER__ && __DEV__
? [transformExpression]
: []),
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
// 指令转换插件
{
on: transformOn,
bind: transformBind,
model: transformModel
}
]
}
// we name it `baseCompile` so that higher order compilers like
// @vue/compiler-dom can export `compile` while re-exporting everything else.
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
const onError = options.onError || defaultOnError
const isModuleMode = options.mode === 'module'
/* istanbul ignore if */
if (__BROWSER__) {
if (options.prefixIdentifiers === true) {
onError(createCompilerError(ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED))
} else if (isModuleMode) {
onError(createCompilerError(ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED))
}
}
const prefixIdentifiers =
!__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)
if (!prefixIdentifiers && options.cacheHandlers) {
onError(createCompilerError(ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED))
}
if (options.scopeId && !isModuleMode) {
onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
}
// 解析
// 如果 template 是 string 类型,解析 template html,转化为 ast
// 如果 template 是 ast (RootNode),则不需要解析了
const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
prefixIdentifiers
)
// 转换
// 遍历 AST 节点树,对上面生成的 AST 进行指令转换,生成可用节点,同时根据 compiler
// 传入的配置(如是否做静态节点提升等)对 AST 节点树进行优化处理,为 rootNode 及
// 下属每个节点挂载 codegenNode
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)
// 生成代码
// 将 ast 转化为可执行代码
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
我们可以通过 options 传入自定义的 nodeTransforms 和 directiveTransforms,实现对自定义的节点、指令的编译规则。
模板编译的流程 parse、transform、generate 三个阶段,接下来我们就来看看 parse 阶段,也就是解析 ast 语法树。
parse 阶段主要是调用 baseParse 解析 template string 为 ast。RootNode 就是一个 dom 树,也即一个 Vnode 树。
3.baseParse
// packages/compiler-core/src/parse.ts
// 解析 template html 生成抽象语法树 (AST)
export function baseParse(
content: string,
options: ParserOptions = {}
): RootNode {
// rootNode:
// 根节点是一个临时容器,真正在运行时映射成具体内容的是rootNode下的children,
// 说白了rootNode只是个用来存放实际节点的空壳子,假如parse AST节点时template string中是多根节点,
// 那么没有一个抽象出来的根节点就无法表述完整的树结构,这也是为什么vue3.0能够允许多根模版的原因所在。
// 生成解析阶段的执行上下文,用于寄存解析过程中产生的状态
const context = createParserContext(content, options)
// 获取模板解析的起始位置
const start = getCursor(context)
// 创建出AST根节点,将模板解析成AST节点,并将生成的AST子代节点挂载到根节点上
return createRoot(
parseChildren(context, TextModes.DATA, []), // 解析生成子节点
getSelection(context, start) // 获取成功解析为AST节点的子模板及其起始位置信息
)
}
我们看下上面调用的 createParserContext
4.createParserContext
/**
创建解析过程的上下文对象,用于对过程中产生的状态进行集中统一的存储和管控
{
options: extend({}, defaultParserOptions, options), // parser配置项
// column、line、offset均是相对template string的全局位置信息
column: 1, // parser解析到的列数
line: 1, // 解析到的行数
offset: 0, // 解析到相对于template string开始的位置
originalSource: content, // 初始template string,即用户定义的完整模版字符串
source: content, // parser处理后的最新template string
inPre: false,
inVPre: false
}
* @returns
*/
function createParserContext(
content: string,
rawOptions: ParserOptions
): ParserContext {
const options = extend({}, defaultParserOptions)
for (const key in rawOptions) {
// @ts-ignore
options[key] = rawOptions[key] || defaultParserOptions[key]
}
return {
options,
column: 1,
line: 1,
offset: 0,
originalSource: content,
source: content,
inPre: false,
inVPre: false
}
}
可以看到,baseParse 的核心在下面这步:
5.parseChildren
return createRoot(
parseChildren(context, TextModes.DATA, []),
getSelection(context, start)
)
我们先来具体看下 parseChildren
// 用于解析一段“完整”的模版串
function parseChildren(
context: ParserContext,
// 节点的命名空间是,html、xml等情况
mode: TextModes,
// 祖先节点,是一个栈结构,用于维护节点嵌套关系,越靠后的节点在dom树中的层级越深
ancestors: ElementNode[]
): TemplateChildNode[] {
const parent = last(ancestors)
const ns = parent ? parent.ns : Namespaces.HTML
// 存储解析出来的AST子节点
const nodes: TemplateChildNode[] = []
// 遇到闭合标签结束解析
while (!isEnd(context, mode, ancestors)) {
__TEST__ && assert(context.source.length > 0)
const s = context.source
let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
// '{{'
// 解析以‘{{’开头的模版,parseInterpolation为核心方法,下面重点讲解
node = parseInterpolation(context, mode)
} else if (mode === TextModes.DATA && s[0] === '<') {
// https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
if (s.length === 1) {
emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
} else if (s[1] === '!') {
// https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state
if (startsWith(s, '<!--')) {
// 解析注释节点
node = parseComment(context)
} else if (startsWith(s, '<!DOCTYPE')) {
// Ignore DOCTYPE by a limitation.
node = parseBogusComment(context)
} else if (startsWith(s, '<![CDATA[')) {
if (ns !== Namespaces.HTML) {
node = parseCDATA(context, ancestors)
} else {
emitError(context, ErrorCodes.CDATA_IN_HTML_CONTENT)
node = parseBogusComment(context)
}
} else {
// 错误处理,省略
emitError(context, ErrorCodes.INCORRECTLY_OPENED_COMMENT)
node = parseBogusComment(context)
}
} else if (s[1] === '/') {
// 解析结束标签错误的逻辑,此处省略
// https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state
if (s.length === 2) {
emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 2)
} else if (s[2] === '>') {
emitError(context, ErrorCodes.MISSING_END_TAG_NAME, 2)
advanceBy(context, 3)
continue
} else if (/[a-z]/i.test(s[2])) {
emitError(context, ErrorCodes.X_INVALID_END_TAG)
parseTag(context, TagType.End, parent)
continue
} else {
emitError(
context,
ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
2
)
node = parseBogusComment(context)
}
} else if (/[a-z]/i.test(s[1])) {
// 解析正常的html开始标签,获得解析到的AST节点
// parseElement是核心方法,下面重点讲解
node = parseElement(context, ancestors)
} else if (s[1] === '?') {
emitError(
context,
ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
1
)
node = parseBogusComment(context)
} else {
emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 1)
}
}
}
if (!node) {
node = parseText(context, mode)
}
if (isArray(node)) {
for (let i = 0; i < node.length; i++) {
pushNode(nodes, node[i])
}
} else {
pushNode(nodes, node)
}
}
// Whitespace management for more efficient output
// (same as v2 whitespace: 'condense')
let removedWhitespace = false
if (mode !== TextModes.RAWTEXT && mode !== TextModes.RCDATA) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (!context.inPre && node.type === NodeTypes.TEXT) {
if (!/[^\t\r\n\f ]/.test(node.content)) {
const prev = nodes[i - 1]
const next = nodes[i + 1]
// If:
// - the whitespace is the first or last node, or:
// - the whitespace is adjacent to a comment, or:
// - the whitespace is between two elements AND contains newline
// Then the whitespace is ignored.
if (
!prev ||
!next ||
prev.type === NodeTypes.COMMENT ||
next.type === NodeTypes.COMMENT ||
(prev.type === NodeTypes.ELEMENT &&
next.type === NodeTypes.ELEMENT &&
/[\r\n]/.test(node.content))
) {
removedWhitespace = true
nodes[i] = null as any
} else {
// Otherwise, condensed consecutive whitespace inside the text
// down to a single space
node.content = ' '
}
} else {
node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
}
}
// also remove comment nodes in prod by default
if (
!__DEV__ &&
node.type === NodeTypes.COMMENT &&
!context.options.comments
) {
removedWhitespace = true
nodes[i] = null as any
}
}
if (context.inPre && parent && context.options.isPreTag(parent.tag)) {
// remove leading newline per html spec
// https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
const first = nodes[0]
if (first && first.type === NodeTypes.TEXT) {
first.content = first.content.replace(/^\r?\n/, '')
}
}
}
return removedWhitespace ? nodes.filter(Boolean) : nodes
}
一起看下 parseChildren 用到的核心方法之一 parseElement
// 解析dom节点
function parseElement(
context: ParserContext,
ancestors: ElementNode[]
): ElementNode | undefined {
__TEST__ && assert(/^<[a-z]/i.test(context.source))
// Start tag.
const wasInPre = context.inPre
const wasInVPre = context.inVPre
const parent = last(ancestors)
// 解析开始标签生成AST节点
const element = parseTag(context, TagType.Start, parent)
const isPreBoundary = context.inPre && !wasInPre
const isVPreBoundary = context.inVPre && !wasInVPre
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
return element
}
// Children.
// 根据开始标签解析出的节点入栈,并解析它的子节点,子节点解析完毕后,父节点出栈
ancestors.push(element)
const mode = context.options.getTextMode(element, parent)
// 递归解析子节点,子节点解析过程中遇到父节点的结束标签,即解析完成并返回解析结果
const children = parseChildren(context, mode, ancestors)
ancestors.pop()
// 为当前节点注入children子节点
element.children = children
// End tag.
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.End, parent)
} else {
emitError(context, ErrorCodes.X_MISSING_END_TAG, 0, element.loc.start)
if (context.source.length === 0 && element.tag.toLowerCase() === 'script') {
const first = children[0]
if (first && startsWith(first.loc.source, '<!--')) {
emitError(context, ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT)
}
}
}
element.loc = getSelection(context, element.loc.start)
if (isPreBoundary) {
context.inPre = false
}
if (isVPreBoundary) {
context.inVPre = false
}
return element
}
TODO:parseTag、parseAttributes