一、baseCompile
baseCompile:
编译器的总入口,是编译器的一个基础骨架(概念上可以理解为基类),然后不同平台的编译系统都是基于baseCompile来进行扩展的,如dom编译、服务端渲染编译、sfc,都是基于baseCompile进行了对应平台下处理场景的扩展。
function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
// 省略无关代码...
// 生成AST节点树
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
})
)
}
二、parser模版解析器
parser的作用就是将我们传入的template string转化为AST节点树,供后面的transform使用。
rootNode:
根节点是一个临时容器,真正在运行时映射成具体内容的是rootNode下的children,说白了rootNode只是个用来存放实际节点的空壳子,假如parse AST节点时template string中是多根节点,那么没有一个抽象出来的根节点就无法表述完整的树结构,这也是为什么vue3.0能够允许多根模版的原因所在。
Position:
包含offset、line、column三个属性,offset记录parser解析到的位置相对原始template string开头的位置,line记录parser解析到的行数,column为列数,因为parse过程中会遇到\n\t\f之类的转义字符。
baseParse:
将template string解析成AST,AST是vue对节点的一种表述形式,和平时JS生成的抽象语法树是两码事。
export function baseParse(
content: string, // 原始的模版字符串
options: ParserOptions = {}
): RootNode {
// 获得parser上下文,相当于class实例化的产物,用来存储parser的一些信息
const context = createParserContext(content, options)
// 获取parser开始位置
const start = getCursor(context)
// 生成AST节点
return createRoot(
parseChildren(context, TextModes.DATA, []), // 生成AST子节点
getSelection(context, start) // 获取根节点位置信息、对应string信息:loc
)
}
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
}
createRoot:
创建一个虚拟根节点容器,根据模版解析出children和对应的位置信息,透传给根节点对象
function createRoot(
children: TemplateChildNode[],
loc = locStub
): RootNode {
// 生成AST根节点的结构
return {
type: NodeTypes.ROOT, // 节点类型
children, // 子节点
helpers: [],
components: [], // 组件节点
directives: [], // 指令节点
hoists: [],
imports: [],
cached: 0,
temps: 0,
codegenNode: undefined, // 用于后续generate阶段生成vnode创建物料
// 节点在template string中所处的位置,结构{ source, start, end }
// source对应节点在模版中对应的string部分,start、end对应节点起始标签
// 对应template string中的位置
loc
}
}
createChildren:
创建AST根节点容器的所属子节点,即模版中实际的节点。该方法为parser核心处理逻辑,用于解析一段“完整”的模版串,比如<div><p>test</p></div>
,<div>...</div>
和<p>...</p>
都是“完整”的,因此会递归的执行parseChildren解析子节点,将解析出的子节点插入父节点中。
过程中两个比较重要的解析方法是parseInterpolation(处理插值)、parseElement(解析dom节点),将重点介绍。
function parseChildren(
context: ParserContext,
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)) {
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] === '!') {
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 {
// 错误处理,省略
}
} else {
// 错误处理省略
}
} else if (s[1] === '/') {
// 解析结束标签错误的逻辑,此处省略
} else if (/[a-z]/i.test(s[1])) {
// 解析正常的html开始标签,获得解析到的AST节点
// parseElement是核心方法,下面重点讲解
node = parseElement(context, ancestors)
} else if (s[1] === '?') {
// 解析错误处理,省略
} else {
// 解析错误处理,省略
}
}
}
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) {
if (!context.inPre) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (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, ' ')
}
} else if (!__DEV__ && node.type === NodeTypes.COMMENT) {
// remove comment nodes in prod
removedWhitespace = true
nodes[i] = null as any
}
}
} else if (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
}
那么入参里的这个mode是什么呢,其实就是vue对节点进行了一个分类,大概看一下:
getTextMode({ tag, ns }: ElementNode): TextModes {
// 节点命名空间是html的情况
if (ns === DOMNamespaces.HTML) {
if (tag === 'textarea' || tag === 'title') {
// textarea和title标签属于RCDATA类型
return TextModes.RCDATA
}
if (isRawTextContainer(tag)) {
// 文本容器,包括style、script、noscript、iframe,归类到RAWTEXT
return TextModes.RAWTEXT
}
}
// 非html命名空间的节点归类到DATA,如svg
return TextModes.DATA
}
parseElement:
解析dom元素生成AST节点
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
// 如果是自我闭合节点或者空标签,直接返回解析出的AST节点
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
return element
}
// 根据开始标签解析出的节点入栈,并解析它的子节点,子节点解析完毕后,父节点出栈
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
}
parseTag:
function parseTag(
context: ParserContext,
type: TagType,
parent: ElementNode | undefined
): ElementNode {
// 标签开始<.
const start = getCursor(context)
// 正则匹配开始 / 结束标签,\/?表示‘/’可有可无,因为此处匹配的是开始或者结束标签
// 所以有的有‘/’有的没有
const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
// exec返回的数组第一个是正则匹配文本,后面是子表达式匹配的内容,也就是括号里匹配到的
// 内容,此处子表达式匹配到的是除\t\r\n\f />这些字符外的内容,也就是节点的标签名、
// 各种属性集
const tag = match[1]
const ns = context.options.getNamespace(tag, parent)
// parser解析完开始标签,推进模版字符串,位数为整个开始标签的长度
advanceBy(context, match[0].length)
advanceSpaces(context)
// save current state in case we need to re-parse attributes with v-pre
const cursor = getCursor(context)
const currentSource = context.source
// 解析属性props,parseAttributes内部主要调用了parseAttribute,下面会讲到
let props = parseAttributes(context, type)
// <pre>标签和v-pre指令的解析逻辑,此处省略...
// 标签结束/>
let isSelfClosing = false
if (context.source.length === 0) {
emitError(context, ErrorCodes.EOF_IN_TAG)
} else {
// 判断标签是否自身闭合
isSelfClosing = startsWith(context.source, '/>')
if (type === TagType.End && isSelfClosing) {
emitError(context, ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS)
}
// 根据是否是自闭合标签向前推进对应的位数,普通标签>:1位,自闭合标签/>:2位
advanceBy(context, isSelfClosing ? 2 : 1)
}
let tagType = ElementTypes.ELEMENT
const options = context.options
if (!context.inVPre && !options.isCustomElement(tag)) {
// 是否有由v-is属性(动态组件)
const hasVIs = props.some(
p => p.type === NodeTypes.DIRECTIVE && p.name === 'is'
)
if (options.isNativeTag && !hasVIs) {
// 判断是否是html原生标签,如果不是则标记为组件类型的标签
if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
} else if (
hasVIs ||
isCoreComponent(tag) ||
(options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
/^[A-Z]/.test(tag) ||
tag === 'component'
) {
// 被解析为组件标签的case
tagType = ElementTypes.COMPONENT
}
// 插槽标签处理
if (tag === 'slot') {
tagType = ElementTypes.SLOT
} else if (
tag === 'template' &&
props.some(p => {
return (
p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
)
})
) {
tagType = ElementTypes.TEMPLATE
}
}
// 由tag解析出的AST节点结构
return {
type: NodeTypes.ELEMENT,
ns,
tag, // 标签名
tagType, // 标签类型,表明是原生还是某种组件类型
props, // 属性,解析后的表达式类型(包含属性的类型、名称、值表达式),具体看parseAttribute
isSelfClosing,
children: [], // 子节点,由外部的parseChildren生成并注入
loc: getSelection(context, start), // 对应模版字符串中的位置信息
codegenNode: undefined // to be created during transform phase
}
}
parseAttribute:
function parseAttribute(
context: ParserContext,
nameSet: Set<string>
): AttributeNode | DirectiveNode {
// Name.
const start = getCursor(context)
// 匹配属性名称
const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
const name = match[0]
// 保证属性名称唯一性
if (nameSet.has(name)) {
emitError(context, ErrorCodes.DUPLICATE_ATTRIBUTE)
}
nameSet.add(name)
if (name[0] === '=') {
emitError(context, ErrorCodes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME)
}
{
const pattern = /["'<]/g
let m: RegExpExecArray | null
while ((m = pattern.exec(name))) {
emitError(
context,
ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
m.index
)
}
}
// 推进属性名称
advanceBy(context, name.length)
// 解析属性值
let value:
| {
content: string // 引号间(不一定在引号里)的原生文本内容
isQuoted: boolean // 值是否包含在引号里
loc: SourceLocation // 对应位置
}
| undefined = undefined
if (/^[\t\r\n\f ]*=/.test(context.source)) {
// 推进‘=’
advanceSpaces(context)
advanceBy(context, 1)
advanceSpaces(context)
// 解析出属性值,结构见value声明处
value = parseAttributeValue(context)
if (!value) {
emitError(context, ErrorCodes.MISSING_ATTRIBUTE_VALUE)
}
}
const loc = getSelection(context, start)
// 指令属性,包括v-、:、@...开头的属性
if (!context.inVPre && /^(v-|:|@|#)/.test(name)) {
// 示例:指令v-bind:test用下面正则匹配后结果为:match = [
// 'v-bind:test',
// 'bind', // match[1]
// 'test', // match[2]
// undefined,
// index: 0,
// input: 'v-bind:test',
// groups: undefined
// ]
const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(
name
)!
// 确定指令所属类型:bind(e.g. :test)、on(e.g. @click)、slot
// 对于全名的指令,如v-{dirName}:test,他的类型是{}里的内容,即on或bind
// 简写命令如@click、:test对应的也是on、bind,指令的使用vue用户应该
// 再熟悉不过了吧
const dirName =
match[1] ||
(startsWith(name, ':') ? 'bind' : startsWith(name, '@') ? 'on' : 'slot')
// arg代表属性真名
let arg: ExpressionNode | undefined
// match[2]对应的就是指令的真名了,e.g. v-bind:{realName}
if (match[2]) {
const isSlot = dirName === 'slot'
const startOffset = name.indexOf(match[2])
const loc = getSelection(
context,
getNewPosition(context, start, startOffset),
getNewPosition(
context,
start,
startOffset + match[2].length + ((isSlot && match[3]) || '').length
)
)
let content = match[2]
let isStatic = true
if (content.startsWith('[')) {
isStatic = false
if (!content.endsWith(']')) {
emitError(
context,
ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END
)
}
content = content.substr(1, content.length - 2)
} else if (isSlot) {
// #1241 special case for v-slot: vuetify relies extensively on slot
// names containing dots. v-slot doesn't have any modifiers and Vue 2.x
// supports such usage so we are keeping it consistent with 2.x.
content += match[3] || ''
}
arg = {
type: NodeTypes.SIMPLE_EXPRESSION,
content, // 指令真名
isStatic, // 是否是静态
isConstant: isStatic, // 是否常量
loc
}
}
if (value && value.isQuoted) {
const valueLoc = value.loc
valueLoc.start.offset++
valueLoc.start.column++
valueLoc.end = advancePositionWithClone(valueLoc.start, value.content)
valueLoc.source = valueLoc.source.slice(1, -1)
}
// 指令属性的结构(v-if、v-for、v-bind、v-once...)
return {
type: NodeTypes.DIRECTIVE, // 表明是指令型属性
name: dirName, // 指令类型
exp: value && {
type: NodeTypes.SIMPLE_EXPRESSION,
content: value.content,
isStatic: false,
// transformExpression时可能会将isContant置为true,因为有些表达式是不会变化的
// 所以有必要让其参与到静态提升中
isConstant: false,
loc: value.loc
},
arg, // 指令真名表达式
modifiers: match[3] ? match[3].substr(1).split('.') : [],
loc // 位置信息
}
}
// 普通dom属性的结构,值一定是静态的,不会发生变化,如test="test"
return {
type: NodeTypes.ATTRIBUTE, // 非指令型指令类型
name, // 属性名称
value: value && {
type: NodeTypes.TEXT,
content: value.content,
loc: value.loc
}, // 属性值信息
loc
}
}
isEnd:
根据节点栈(ancestors)中的最后一个入栈节点和匹配到的结束标签做比较,如果判断为同一标签,表明节点是合法闭合的
function isEnd(
context: ParserContext,
mode: TextModes,
ancestors: ElementNode[]
): boolean {
const s = context.source
switch (mode) {
// 非html命名空间节点
case TextModes.DATA:
if (startsWith(s, '</')) {
//TODO: probably bad performance
for (let i = ancestors.length - 1; i >= 0; --i) {
if (startsWithEndTagOpen(s, ancestors[i].tag)) {
return true
}
}
}
break
// 直接装载文本的节点:一定是无层级的,直接判断入栈的最后一个节点即可
case TextModes.RCDATA: // title、textarea
case TextModes.RAWTEXT: { // script、noscript、iframe、style
const parent = last(ancestors)
if (parent && startsWithEndTagOpen(s, parent.tag)) {
return true
}
break
}
case TextModes.CDATA:
if (startsWith(s, ']]>')) {
return true
}
break
}
return !s
}
advanceBy:
parser推进template string后,裁切template string为推进后的位置至尾部之间的内容,将其作为最新template string
function advanceBy(context: ParserContext, numberOfCharacters: number): void {
const { source } = context
advancePositionWithMutation(context, source, numberOfCharacters)
context.source = source.slice(numberOfCharacters)
}
advancePositionWithMutation:
根据传入长度对模版字符串向前推进,同时推进后的最新位置信息。函数在parser中会频繁调用,考虑到拷贝新的位置信息耗费性能,因此直接修改源位置信息以节省开销。
export function advancePositionWithMutation(
pos: Position, // 解析器当前位置信息
source: string, // 当前待分析模版字符串
numberOfCharacters: number = source.length // 模版字符串推进长度
): Position {
let linesCount = 0 // 换行后的总行数
let lastNewLinePos = -1 // 换行后上一行最后一个字符在template string中的位置
// 遍历推进的内容,遇到换行符累加行数linesCount,同时记录上一行最后一个字符的
// 位置lastNewLinePos
for (let i = 0; i < numberOfCharacters; i++) {
if (source.charCodeAt(i) === 10 /* newline char code */) {
linesCount++
lastNewLinePos = i
}
}
// 累加之前的position结果,计算出parser最新的位置信息
pos.offset += numberOfCharacters
pos.line += linesCount
pos.column =
lastNewLinePos === -1
? pos.column + numberOfCharacters
: numberOfCharacters - lastNewLinePos
return pos
}
三、transform AST转换器
transform阶段主要是对parse阶段生成的AST节点树进行转化处理,产出可以在generate阶段生成运行时代码的信息(gencodeNode),转化过程中大量用表达式对象来描述信息(gencodeNode本身也可以是一个表达式对象),下面的代码就能体会到了。
表达式对象有很多种,以最简单的简单表达式(SimpleExpression)为例(比如一个变量引用):
{
type: NodeTypes.SIMPLE_EXPRESSION, // 表达式类型标示
loc, // 位置信息
isConstant, // 是否是常量
content, // 表达式内容
// 是否是静态的,
// e.g. v-bind:attr="value",value如果是动态变化的变量
// v-bind:attr="true",true是常量不会变化,因此是静态的
isStatic
}
// 比如v-bind:attr="true",true转换为简单表达式对象就是
// { isContant: true, content: 'true', isStatic: true ... }
transform:
function transform(root: RootNode, options: TransformOptions) {
// 创建transform上下文
const context = createTransformContext(root, options)
// 深度遍历AST,根据节点中的指令(v-if、v-for...转换为相应节点)
// 遍历过程中转换完成后每个节点的codegenNode会挂载转换后的节点内容
traverseNode(root, context)
if (options.hoistStatic) {
// 如果编译器配置了静态节点提升,对静态节点进行提升
hoistStatic(root, context)
}
if (!options.ssr) {
// 根据根节点下的children生成rootNode上的codegenNode
createRootCodegen(root, context)
}
// 将transform上下文上的信息挂载到根节点
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = [...context.imports]
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached
}
traverseNode:
遍历AST节点树过程中,通过node转换器(nodeTransforms)对当前节点进行node转换,子节点全部遍历完成后执行对应指令的onExit回调退出转换。对v-if、v-for等指令的转换生成对应节点,都是由nodeTransforms中对应的指令转换工具完成的。
经nodeTransforms处理过的AST节点会被挂载codeGenNode属性(其实就是调用vnode创建的interface),该属性包含patchFlag等在AST解析阶段无法获得的信息,其作用就是为了在后面的generate阶段生成vnode的创建调用。
本质上codegenNode是一个表达式对象。
function traverseNode(
node: RootNode | TemplateChildNode,
context: TransformContext
) {
// 上下文记录当前正在遍历的节点
context.currentNode = node
// 转换器:transformElement、transformExpression、transformText、
// transformSlotOutlet...
// transformElement负责整个节点层面的转换,transformExpression负责
// 节点中表达式的转化,transformText负责节点中文本的转换,转换后会增加
// 一堆表达式表述对象
const { nodeTransforms } = context
const exitFns = []
// 依次调用指令转换工具
for (let i = 0; i < nodeTransforms.length; i++) {
// 转换器只负责生成onExit回调(具体可以看下面的transformElement),
// onExit函数才是执行转换主逻辑的地方,为什么要推到栈中先不执行呢?
// 因为要等到子节点都转换完成挂载gencodeNode后,也就是深度遍历完成后
// 再执行当前节点栈中的onExit,这样保证了子节点的表达式全部生成完毕
const onExit = nodeTransforms[i](node, context)
if (onExit) {
if (isArray(onExit)) {
// v-if、v-for为结构化指令,其onExit是数组形式
exitFns.push(...onExit)
} else {
exitFns.push(onExit)
}
}
if (!context.currentNode) {
// node was removed
return
} else {
// node may have been replaced
node = context.currentNode
}
}
switch (node.type) {
case NodeTypes.COMMENT:
if (!context.ssr) {
// inject import for the Comment symbol, which is needed for creating
// comment nodes with `createVNode`
context.helper(CREATE_COMMENT)
}
break
case NodeTypes.INTERPOLATION:
// no need to traverse, but we need to inject toString helper
if (!context.ssr) {
context.helper(TO_DISPLAY_STRING)
}
break
// for container types, further traverse downwards
case NodeTypes.IF:
// 对v-if生成的节点束进行遍历
for (let i = 0; i < node.branches.length; i++) {
traverseNode(node.branches[i], context)
}
break
case NodeTypes.IF_BRANCH:
case NodeTypes.FOR:
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:
// 遍历子节点
traverseChildren(node, context)
break
}
// 当前节点树遍历完成,依次执行栈中的指令退出回调onExit
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}
transformElement:
nodeTransform有很多种,如文本、表达式相关转换,此处仅对element进行讲解,其他类型的有兴趣可查阅源码。
transformElement对原生dom元素和组件类型的AST生成对应的VNODE_CALL接口,即gencodeNode,用于后续generate阶段中进行创建vnode调用。
const transformElement: NodeTransform = (node, context) => {
// 组件和原生dom的type都属于element,tagType标示了具体的类型所属
if (
!(
node.type === NodeTypes.ELEMENT &&
(node.tagType === ElementTypes.ELEMENT ||
node.tagType === ElementTypes.COMPONENT)
)
) {
return
}
// 返回闭包,也是真正执行transform逻辑生成gencodeNode的地方,返回闭包是为了
// 透出到外部,由外部控制调用时机,等待子代表达式转换生成完毕后再执行当前AST节点
// 转换
return function postTransformElement() {
const { tag, props /* parsed props */ } = node
// 是否为组件
const isComponent = node.tagType === ElementTypes.COMPONENT
// 决定创建组件vnode时的tag值
const vnodeTag = isComponent
? resolveComponentType(node as ComponentNode, context)
: `"${tag}"`
// 是否是动态组件,resolveComponentType生成的tag值为call表达式
const isDynamicComponent =
isObject(vnodeTag) && vnodeTag.callee === RESOLVE_DYNAMIC_COMPONENT
let vnodeProps: VNodeCall['props']
let vnodeChildren: VNodeCall['children']
let vnodePatchFlag: VNodeCall['patchFlag']
let patchFlag: number = 0
let vnodeDynamicProps: VNodeCall['dynamicProps']
let dynamicPropNames: string[] | undefined
let vnodeDirectives: VNodeCall['directives']
// 需要创建block的情况:动态组件(v-bind:is)、svg、动态key...
let shouldUseBlock =
// dynamic component may resolve to plain elements
isDynamicComponent ||
(!isComponent &&
// <svg> and <foreignObject> must be forced into blocks so that block
// updates inside get proper isSVG flag at runtime. (#639, #643)
// This is technically web-specific, but splitting the logic out of core
// leads to too much unnecessary complexity.
(tag === 'svg' ||
tag === 'foreignObject' ||
// #938: elements with dynamic keys should be forced into blocks
findProp(node, 'key', true)))
// props转化
if (props.length > 0) {
// buildProps会在处理props过程中解析出vnode创建所需的表达式对象
// 包括属性、patchflag、动态属性名称集合、指令集合
// buildProps下面会重点介绍
const propsBuildResult = buildProps(node, context)
vnodeProps = propsBuildResult.props // ObjectExpressions属性集
patchFlag = propsBuildResult.patchFlag // patchFlag
dynamicPropNames = propsBuildResult.dynamicPropNames // 动态属性名称集合
const directives = propsBuildResult.directives
// 运行时指令,创建运行时指令的ArrayExpression
vnodeDirectives =
directives && directives.length
? (createArrayExpression(
directives.map(dir => buildDirectiveArgs(dir, context))
) as DirectiveArguments)
: undefined
}
// 对子节点children进行转化处理
if (node.children.length > 0) {
// keep-alive处理逻辑省略...
const shouldBuildAsSlots =
isComponent &&
// Teleport is not a real component and has dedicated runtime handling
vnodeTag !== TELEPORT &&
// explained above.
vnodeTag !== KEEP_ALIVE
if (shouldBuildAsSlots) {
const { slots, hasDynamicSlots } = buildSlots(node, context)
vnodeChildren = slots
if (hasDynamicSlots) {
patchFlag |= PatchFlags.DYNAMIC_SLOTS
}
} else if (node.children.length === 1 && vnodeTag !== TELEPORT) {
// 仅有一个子节点且不是teleport类型的情况
const child = node.children[0]
const type = child.type
// 检查是否包含动态文本节点,即插值、复合表达式
const hasDynamicTextChild =
type === NodeTypes.INTERPOLATION ||
type === NodeTypes.COMPOUND_EXPRESSION
if (hasDynamicTextChild && !getStaticType(child)) {
patchFlag |= PatchFlags.TEXT // 标记为动态文本
}
// 如果当前唯一的子节点是文本节点(插值类型、复合表达式类型、原生文本节点)
// 直接讲该文本节点作为vnodeChildren
if (hasDynamicTextChild || type === NodeTypes.TEXT) {
vnodeChildren = child as TemplateTextChildNode
} else {
vnodeChildren = node.children
}
} else {
// 多子节点情况直接拷贝其子节点作为vnodeChildren
vnodeChildren = node.children
}
}
// patchFlag & dynamicPropNames
if (patchFlag !== 0) {
if (__DEV__) {
// 省略无关代码...
} else {
vnodePatchFlag = String(patchFlag)
}
if (dynamicPropNames && dynamicPropNames.length) {
// 动态属性字符串化
vnodeDynamicProps = stringifyDynamicPropNames(dynamicPropNames)
}
}
// 最终生成的VNodeCall结构,和createVnode入参一样的名称,相信大家都熟悉
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren,
vnodePatchFlag,
vnodeDynamicProps,
vnodeDirectives,
!!shouldUseBlock,
false /* disableTracking */,
node.loc
)
}
}
buildProps:
分析属性得到对应的patchFlag信息、动态属性名称、运行时的指令、属性表达式,结构如下:
{
props: PropsExpression | undefined // 属性表达式
directives: DirectiveNode[] 运行时指令
patchFlag: number
dynamicPropNames: string[] // 动态属性集
}
function buildProps(
node: ElementNode,
context: TransformContext,
props: ElementNode['props'] = node.props,
ssr = false
): {
props: PropsExpression | undefined
directives: DirectiveNode[]
patchFlag: number
dynamicPropNames: string[]
} {
const { tag, loc: elementLoc } = node
// 节点是否是组件
const isComponent = node.tagType === ElementTypes.COMPONENT
// 存储属性表达式(key-value expression)
let properties: ObjectExpression['properties'] = []
const mergeArgs: PropsExpression[] = []
// 运行时指令,如自定义指令(v-custom)
const runtimeDirectives: DirectiveNode[] = []
// patchFlag analysis
let patchFlag = 0 // 解析生成的patchFlag
let hasRef = false // 是否包含ref属性(ref节点不能静态提升)
let hasClassBinding = false // 含有动态class属性(:class="")
let hasStyleBinding = false // 含有动态样式(:style="")
let hasHydrationEventBinding = false // ssr
let hasDynamicKeys = false // 含有除ref、class、style外的动态属性
const dynamicPropNames: string[] = []
// 解析生成patchFlag
const analyzePatchFlag = ({ key/* 属性真名表达式 */, value /* 属性值表达式 */ }: Property) => {
if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
// 值属性名称是静态的
const name = key.content
// 服务端渲染逻辑,代码省略...
if (
value.type === NodeTypes.JS_CACHE_EXPRESSION ||
((value.type === NodeTypes.SIMPLE_EXPRESSION ||
value.type === NodeTypes.COMPOUND_EXPRESSION) &&
getStaticType(value) > 0)
) {
// 属性包含缓存handler或为常量,不做patchFlag分类
return
}
// 走到此处署名属性值为动态(或ref),根据属性名称分类
if (name === 'ref') {
// 属性为ref
hasRef = true
} else if (name === 'class' && !isComponent) {
// 属性为动态class
hasClassBinding = true
} else if (name === 'style' && !isComponent) {
// 属性为动态style
hasStyleBinding = true
} else if (name !== 'key' && !dynamicPropNames.includes(name)) {
// 除key属性外的动态属性,收集到dynamicPropNames数组中
dynamicPropNames.push(name)
}
} else {
// 属性真名为动态的,标示含有动态指令名
hasDynamicKeys = true
}
}
// 对props进行遍历,依次处理
for (let i = 0; i < props.length; i++) {
const prop = props[i]
if (prop.type === NodeTypes.ATTRIBUTE) {
// 静态属性处理
const { loc, name /* 属性名 */, value /* 属性值 */ } = prop
if (name === 'ref') {
hasRef = true
}
// 跳过动态is属性,代码省略...
// 收集key-value属性表达式
properties.push(
// 生成key-value形式的属性表达式,只不过key、value均是表达式类型
createObjectProperty(
// key表达式
createSimpleExpression(
name,
true,
getInnerRange(loc, 0, name.length)
),
// value表达式
createSimpleExpression(
value ? value.content : '',
true,
value ? value.loc : loc
)
)
)
} else {
// 指令属性处理
const {
name /* 类型 */,
arg /* 属性真名表达式 */,
exp /* 属性值表达式 */,
loc
} = prop
const isBind = name === 'bind' // :
const isOn = name === 'on' // @
// 跳过v-slot、v-once、v-is、ssr下的v-on处理,代码省略...
// 特殊case,没有真名的指令,如v-bind="test"、v-on="test"
if (!arg && (isBind || isOn)) {
// 算包含动态属性的场景
hasDynamicKeys = true
if (exp) {
if (properties.length) {
mergeArgs.push(
createObjectExpression(dedupeProperties(properties), elementLoc)
)
properties = []
}
if (isBind) {
mergeArgs.push(exp)
} else {
// v-on="obj" -> toHandlers(obj)
mergeArgs.push({
type: NodeTypes.JS_CALL_EXPRESSION,
loc,
callee: context.helper(TO_HANDLERS),
arguments: [exp]
})
}
} else {
// 错误处理,省略...
}
continue
}
// 获取指令对应的转换器函数
const directiveTransform = context.directiveTransforms[name]
if (directiveTransform) {
// vue内置指令(v-if、v-for...)转换,后面会以v-bind为例
// 讲一下对应的directiveTransform(transformBind)
const {
// key-value属性表达式数组,通常数组中只有指令对应的一个key-value表达式
props,
// 属性是否为运行时指令
needRuntime
} = directiveTransform(prop, node, context)
// 分析指令prop对应的patchFlag
!ssr && props.forEach(analyzePatchFlag)
properties.push(...props)
// 当前指令为运行时指令
if (needRuntime) {
runtimeDirectives.push(prop)
if (isSymbol(needRuntime)) {
directiveImportMap.set(prop, needRuntime)
}
}
} else {
// 指令找不到对应的转换器函数,说明是自定义指令,推到runtimeDirectives中
runtimeDirectives.push(prop)
}
}
}
let propsExpression: PropsExpression | undefined = undefined
if (mergeArgs.length) {
//v-bind="object" v-on="object"边界场景处理
if (properties.length) {
mergeArgs.push(
createObjectExpression(dedupeProperties(properties), elementLoc)
)
}
if (mergeArgs.length > 1) {
propsExpression = createCallExpression(
context.helper(MERGE_PROPS),
mergeArgs,
elementLoc
)
} else {
// single v-bind with nothing else - no need for a mergeProps call
propsExpression = mergeArgs[0]
}
} else if (properties.length) {
// 正常props场景处理,生成对应的Object表达式对象,
propsExpression = createObjectExpression(
// dedupeProperties会对重复属性的值进行值的合并,变为一个属性
// 合并后属性value变为ArrayExpression,比如:
// 节点上写了多个class,dedupeProperties会把这个class的值
// 合并到一个ArrayExpression里,作为一个属性来处理
dedupeProperties(properties),
elementLoc
)
}
// 根据具体情况生成对应的patchFlag
if (hasDynamicKeys) {
// 含有动态属性名称
patchFlag |= PatchFlags.FULL_PROPS
} else {
if (hasClassBinding) {
// 有动态class
patchFlag |= PatchFlags.CLASS
}
if (hasStyleBinding) {
// 有动态style
patchFlag |= PatchFlags.STYLE
}
if (dynamicPropNames.length) {
patchFlag |= PatchFlags.PROPS
}
if (hasHydrationEventBinding) {
patchFlag |= PatchFlags.HYDRATE_EVENTS
}
}
if (
(patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) &&
(hasRef || runtimeDirectives.length > 0)
) {
patchFlag |= PatchFlags.NEED_PATCH
}
return {
// parse阶段属性集转化后的属性集,是ObjectExpression类型,该表达式由key-value
// 属性表达式构成
props: propsExpression,
// 运行时的指令,编译阶段不做转换,仍然是parse阶段的值
directives: runtimeDirectives,
patchFlag,
dynamicPropNames // 动态属性名集合
}
}
transformBind:
绑定类型的指令属性转换,输入parse props,输出key-value属性表达式数组,key、value分别对应属性的名称与值
const transformBind: DirectiveTransform = (dir/* parse生成的单个prop表达式对象 */, node, context) => {
const { exp /* 属性值表达式 */, modifiers /* 修饰符 */, loc } = dir
const arg = dir.arg! // 属性真名表达式
if (!exp || (exp.type === NodeTypes.SIMPLE_EXPRESSION && !exp.content)) {
context.onError(createCompilerError(ErrorCodes.X_V_BIND_NO_EXPRESSION, loc))
}
// 修饰符处理逻辑,省略...
return {
props: [
// 创建key-value表达式对象,key是属性真名表达式,value是属性值表达式
createObjectProperty(arg!, exp || createSimpleExpression('', true, loc))
]
}
}
traverChildren:
function traverseChildren(
parent: ParentNode,
context: TransformContext
) {
let i = 0
const nodeRemoved = () => {
i--
}
for (; i < parent.children.length; i++) {
const child = parent.children[i]
if (isString(child)) continue
// 上下文记录节点间亲子关系
context.parent = parent
// 记录当前父节点正在访问的子节点index
context.childIndex = i
context.onNodeRemoved = nodeRemoved
// 深度递归遍历
traverseNode(child, context)
}
}
hoistStatic:
hoistStatic内部实际调用的是walk
// transformContext上挂载的hoist方法,exp接受的是AST节点上的codegenNode属性,
// codegenNode其实本质上就是一个表达式对象
// hoist方法将原始gencodeNode推入上下文,并生成新的简单表达式对象gencodeNode,
// 该对象刻画提升后的变量标识符,并由外部将新gencodeNode挂载到原节点上
fucntion hoist(exp) {
// 将codegenNode(exp)推入hoists数组,生成render时会添加到闭包外
context.hoists.push(exp)
// 生成简单表达式类型的标识符(vue的表达式系统包括很多种:简单表达式、符合表达式、
// 函数表达式、call等等)
// 该标识符其实就是在render函数里声明的变量标识符,如:
// const _hoisted_1 = createVNode('div', {})
const identifier = createSimpleExpression(
`_hoisted_${context.hoists.length}`, // content:表达式内容,通常为变量名称
false, // isStatic:是否是静态内容,动态会受外界变化的影响
exp.loc, // loc:位置信息
true // isConstant:标示是否是常量
)
identifier.hoisted = exp
return identifier
}
// 遍历AST节点树查找需要做静态提升的内容
function walk(
node: ParentNode,
context: TransformContext,
resultCache: Map<TemplateChildNode, StaticType>,
// 这里需要注意下,如果模版中根节点的children只有一个节点,是不需要进行静态提升的
doNotHoistNode: boolean = false
) {
// 标记是否含有提升节点
let hasHoistedNode = false
// Some transforms, e.g. trasnformAssetUrls from @vue/compiler-sfc, replaces
// static bindings with expressions. These expressions are guaranteed to be
// constant so they are still eligible for hoisting, but they are only
// available at runtime and therefore cannot be evaluated ahead of time.
// This is only a concern for pre-stringification (via transformHoist by
// @vue/compiler-dom), but doing it here allows us to perform only one full
// walk of the AST and allow `stringifyStatic` to stop walking as soon as its
// stringficiation threshold is met.
// 标记是否含有运行时常量,比如这种case:
// <div :class="`myClass`">{{ `test` }}</div>
// class属性和插值虽然看似是动态的,但是其内容一直都不变
// 和<div class="myClass">test</div>完全等效,这就是
// 运行时常量,同样参与静态提升
let hasRuntimeConstant = false
const { children } = node
for (let i = 0; i < children.length; i++) {
const child = children[i]
// 只有原生dom节点才会做静态提升,比如一些指令类型的节点(if、for)就不会做提升,
// 而是作为block
if (
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT
) {
// 处理静态节点提升的逻辑
let staticType
if (
!doNotHoistNode &&
// staticType为0表示不是静态节点,getStaticType会递归判断子树
// 中是否有非静态内容,如果子树中全部节点均为静态,则整颗子树提升
(staticType = getStaticType(child, resultCache)) > 0
) {
if (staticType === StaticType.HAS_RUNTIME_CONSTANT) {
hasRuntimeConstant = true
}
// 在codegenNode打上patchFlag,标示当前节点树为静态提升,
// 运行时diff遇到静态节点将直接跳过
;(child.codegenNode as VNodeCall).patchFlag =
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
// 把codegenNode推入context.hoists中,供生成render函数时使用,
// 对应关系:静态节点提升 <-> 当前模版字符串相对应的context
// 并将codegenNode替换成新生成的codegenNode
// 注意:这里context.hoists中存储的是原始的codegenNode,
// 因为存储时的指针是旧指针
child.codegenNode = context.hoist(child.codegenNode!)
// 此时child.codegenNode已指向新生成的identifier对象,
// child.codegenNode的指针发生了变化
hasHoistedNode = true
continue
} else {
// 非静态节点,staticType为0,但是属性可能存在静态值,也需要做提升处理
const codegenNode = child.codegenNode!
if (codegenNode.type === NodeTypes.VNODE_CALL) {
const flag = getPatchFlag(codegenNode)
if (
(!flag ||
flag === PatchFlags.NEED_PATCH ||
flag === PatchFlags.TEXT) &&
// 这里需要注意一点,属性包含动态变化的key,或者含有ref属性时,节点不会被
// 提升。原因如下:
// 1⃣️动态key:动态变化的key表明该节点在diff时可能会被完全替换,顺便提下diff
// 的一个点,vue3.0里节点的tag和key有一个不同就会被当作不同节点,从而
// 完全替换掉。
// 2⃣️ref:ref属性是因为即使写了一个形如`<div ref="test"></div>`的
// 节点,但是假如你在setup里声明了有效的同名响应式数据,比如也叫test
// 那么静态ref也会和这个响应数据关联起来
!hasDynamicKeyOrRef(child) &&
// 不能有缓存的属性
!hasCachedProps(child)
) {
// 上面的判断表示节点本身不包含children的情况下是静态的,即属性全部为静态
const props = getNodeProps(child)
if (props) {
// 将节点的props提升到context中
codegenNode.props = context.hoist(props)
}
}
}
}
} else if (child.type === NodeTypes.TEXT_CALL) {
const staticType = getStaticType(child.content, resultCache)
if (staticType > 0) {
if (staticType === StaticType.HAS_RUNTIME_CONSTANT) {
hasRuntimeConstant = true
}
child.codegenNode = context.hoist(child.codegenNode)
hasHoistedNode = true
}
}
// 对子节点递归执行walk解析出静态提升内容
if (child.type === NodeTypes.ELEMENT) {
walk(child, context, resultCache)
} else if (child.type === NodeTypes.FOR) {
// 不提升v-for生成的单个节点,因为要把它作为一个block
walk(child, context, resultCache, child.children.length === 1)
} else if (child.type === NodeTypes.IF) {
for (let i = 0; i < child.branches.length; i++) {
// 不提升v-if生成的单个分支节点,因为要把它作为一个block
walk(
child.branches[i],
context,
resultCache,
child.branches[i].children.length === 1
)
}
}
}
if (!hasRuntimeConstant && hasHoistedNode && context.transformHoist) {
context.transformHoist(children, context, node)
}
}
createRootCodegen:
根据子节点的情况创建根节点的codegenNode,对于子节点为单节点的情况,需要创建block,多节点创建fragment block。
为什么根节点要创建block呢,原因是需要在根级block上挂载动态子代节点(dynamicChildren),在patch阶段做diff操作时忽略dom树层级,减少不必要的遍历成本,而rootNode是与组件template强相关的,这样就保证了diff一个组件时,会直接比较组件根block下的dynamicChildren,有效减少了组件这颗子树的遍历层级,最理想的情况下可以完全忽略根节点以下子树的遍历
,想象是不是就酸爽了。
function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper } = context
const { children } = root
const child = children[0]
if (children.length === 1) {
// 如果children为单节点,将它转化为block
if (isSingleElementRoot(root, child) && child.codegenNode) {
// single element root is never hoisted so codegenNode will never be
// SimpleExpressionNode
// 单根节点不会被提升,而是作为block
const codegenNode = child.codegenNode
// VNODE_CALL表示codegen是要生成vnode的
if (codegenNode.type === NodeTypes.VNODE_CALL) {
codegenNode.isBlock = true
// 推入openBlock、createBlock方法,后面generate生成创建代码要用到
helper(OPEN_BLOCK)
helper(CREATE_BLOCK)
}
root.codegenNode = codegenNode
} else {
// - single <slot/>, IfNode, ForNode: already blocks.
// - single text node: always patched.
// root codegen falls through via genNode()
root.codegenNode = child
}
} else if (children.length > 1) {
// 多根节点创建稳定fragment的block,并打上PATCH_FLAG
// 多根节点无容器,因此需要创建fragment VNodeCall
root.codegenNode = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
root.children,
`${PatchFlags.STABLE_FRAGMENT} /* ${
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
} */`,
undefined,
undefined,
true
)
} else {
// no children = noop. codegen will return null.
}
}
四、generate
codegenNode:
用于生成vnode,启用各种创建vnode的方法调用(createVnode、createBlock等),也就是我们最终在导入组件对象时里面的render函数,文末有关于compiler生成的render函数样例。
CodegenNode是一个复合类型,可以是TemplateChildNode也可以是JSChildNode,前者其实就是我们前面生成的AST节点,后者是表达式对象(如simpleExpression等)。
generate:
function generate(
ast: RootNode,
options: CodegenOptions = {}
): CodegenResult {
// 创建生成器上下文
const context = createCodegenContext(ast, options)
const {
mode,
push,
prefixIdentifiers,
indent,
deindent,
newline,
scopeId,
ssr
} = context
const hasHelpers = ast.helpers.length > 0
const useWithBlock = !prefixIdentifiers && mode !== 'module'
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
// 生成执行函数的前序部分,主要是方法(如createVnode、createBlock等)的引入,
// 和静态提升节点的声明
if (!__BROWSER__ && mode === 'module') {
genModulePreamble(ast, context, genScopeId)
} else {
// 浏览器环境生成函数的前序代码
genFunctionPreamble(ast, context)
}
// enter render function
if (!ssr) {
// 非浏览器环境推入render函数
if (genScopeId) {
push(`const render = ${PURE_ANNOTATION}_withId(`)
}
push(`function render(_ctx, _cache) {`)
} else {
if (genScopeId) {
push(`const ssrRender = ${PURE_ANNOTATION}_withId(`)
}
push(`function ssrRender(_ctx, _push, _parent, _attrs) {`)
}
indent()
if (useWithBlock) {
// 如果判断需要使用with函数,会将_ctx声明为with函数体内的上下文,这样
// 在调用属性时会方便很多,比如不用with调用_ctx.test,启用with后直接
// 调用test就可以了,不过with函数因为传入未知作用域context,导致js
// 预编译阶段不会提前确定好各个相应声明的所属位置,因此运行时会慢一些,
// 因为要花一些时间查找声明所属位置
push(`with (_ctx) {`)
indent()
// 在with函数体内导入节点创建方法,并重命名,防止和用户自定义属性冲突
if (hasHelpers) {
push(
`const { ${ast.helpers
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')} } = _Vue`
)
push(`\n`)
newline()
}
}
// generate asset resolution statements
if (ast.components.length) {
genAssets(ast.components, 'component', context)
if (ast.directives.length || ast.temps > 0) {
newline()
}
}
if (ast.directives.length) {
genAssets(ast.directives, 'directive', context)
if (ast.temps > 0) {
newline()
}
}
if (ast.temps > 0) {
push(`let `)
for (let i = 0; i < ast.temps; i++) {
push(`${i > 0 ? `, ` : ``}_temp${i}`)
}
}
if (ast.components.length || ast.directives.length || ast.temps) {
push(`\n`)
newline()
}
// generate the VNode tree expression
if (!ssr) {
push(`return `)
}
if (ast.codegenNode) {
// 根据rootNode.codegenNode生成创建Vnode的函数调用
genNode(ast.codegenNode, context)
} else {
push(`null`)
}
if (useWithBlock) {
deindent()
push(`}`)
}
deindent()
push(`}`)
if (genScopeId) {
push(`)`)
}
return {
ast,
code: context.code,
// SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined
}
}
genFunctionPreamble:
生成函数的前序部分,主要包括createVnode、createBlock等节点创建方法的引入,和静态节点提升的声明
function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const {
ssr,
prefixIdentifiers,
push,
newline,
runtimeModuleName,
runtimeGlobalName
} = context
// vue模块导入
const VueBinding =
!__BROWSER__ && ssr
? `require(${JSON.stringify(runtimeModuleName)})`
: runtimeGlobalName
// 解构重命名alias
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
// Generate const declaration for helpers
// In prefix mode, we place the const declaration at top so it's done
// only once; But if we not prefixing, we place the declaration inside the
// with block so it doesn't incur the `in` check cost for every helper access.
// AST节点的helpers主要包含createVnode等节点创建方法名,用于下面的解构批量导入对应方法
if (ast.helpers.length > 0) {
if (!__BROWSER__ && prefixIdentifiers) {
push(
`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`
)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
push(`const _Vue = ${VueBinding}\n`)
// in "with" mode, helpers are declared inside the with block to avoid
// has check cost, but hoists are lifted out of the function - we need
// to provide the helper here.
// AST中存在提升的静态节点时,导入静态节点创建的方法们
if (ast.hoists.length) {
const staticHelpers = [
CREATE_VNODE,
CREATE_COMMENT,
CREATE_TEXT,
CREATE_STATIC
]
.filter(helper => ast.helpers.includes(helper))
.map(aliasHelper)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
}
}
}
// 从@vue/server-renderer模块导入服务端渲染的对应创建方法
if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) {
// ssr guaruntees prefixIdentifier: true
push(
`const { ${ast.ssrHelpers
.map(aliasHelper)
.join(', ')} } = require("@vue/server-renderer")\n`
)
}
// 生成静态节点提升对应的代码
genHoists(ast.hoists, context)
newline()
// return后接render函数
push(`return `)
}
genHoists:
生成静态节点提升声明,形如const _hoisted_${i + 1} = ...
这样的节点创建声明
function genHoists(hoists: (JSChildNode | null)[], context: CodegenContext) {
if (!hoists.length) {
return
}
context.pure = true
const { push, newline, helper, scopeId, mode } = context
const genScopeId = !__BROWSER__ && scopeId != null && mode !== 'function'
newline()
// 非浏览器环境push scopeId相关,省略代码...
// 生成被提升静态节点的核心逻辑,具体的创建调用了genNode函数
// genNode是真正创建vnode的地方
hoists.forEach((exp, i) => {
if (exp) {
push(`const _hoisted_${i + 1} = `)
genNode(exp, context)
newline()
}
})
// 非浏览器环境pop scopeId相关,省略代码...
context.pure = false
}
genNode:
根据codegenNode生成对应创建代码,根据codegenNode的类型执行对应的代码生成逻辑,重点关注VNODE_CALL类型的节点,我们在render函数中调用createBlock、createVnode的相关代码都是通过VNODE_CALL类型节点来生成的。
注意:genNode并非只用来生成dom节点级别的创建代码,而是面向一切CodegenNode类型的节点,比如可以是transform后的props、dynamicProps等表达式对象,transform生成的信息绝大多数都是以表达式对象的形式来表述的。
比如genVNodeCall,在对节点级别生成创建代码过程中,会对VNodeCall对象中的各表达式信息(比如props等)调用genNode进行代码生成,其实是一个层层递归的生成过程:
genNode(节点级别) -> genVNodeCall -> genNode(属性级别)
// 入参的gencodeNode可能是表达式对象类型,也可能是模版节点类型
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
if (isString(node)) {
context.push(node)
return
}
if (isSymbol(node)) {
context.push(context.helper(node))
return
}
switch (node.type) {
/**
* 模版节点类型
*/
case NodeTypes.ELEMENT:
case NodeTypes.IF:
case NodeTypes.FOR:
// 省略无关逻辑...
genNode(node.codegenNode!, context)
break
case NodeTypes.TEXT:
genText(node, context)
break
/**
* 表达式类型,这类居多
*/
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context)
break
case NodeTypes.INTERPOLATION:
genInterpolation(node, context)
break
case NodeTypes.TEXT_CALL:
genNode(node.codegenNode, context)
break
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context)
break
case NodeTypes.COMMENT:
genComment(node, context)
break
case NodeTypes.VNODE_CALL:
// 生成VNodeCall,最常遇到的就是这种
genVNodeCall(node, context)
break
case NodeTypes.JS_CALL_EXPRESSION:
genCallExpression(node, context)
break
case NodeTypes.JS_OBJECT_EXPRESSION:
genObjectExpression(node, context)
break
case NodeTypes.JS_ARRAY_EXPRESSION:
genArrayExpression(node, context)
break
case NodeTypes.JS_FUNCTION_EXPRESSION:
genFunctionExpression(node, context)
break
case NodeTypes.JS_CONDITIONAL_EXPRESSION:
genConditionalExpression(node, context)
break
case NodeTypes.JS_CACHE_EXPRESSION:
genCacheExpression(node, context)
break
// 服务端渲染相关逻辑省略...
/* istanbul ignore next */
case NodeTypes.IF_BRANCH:
// noop
break
default:
// 省略无关代码...
}
}
genVNodeCall:
生成类似withDirectives((openBlock(true), createBlock(...args)), directivesObj)
这样的代码,即render函数中的Vnode核心创建逻辑代码,和我们手写render类似。
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const { push, helper, pure } = context
const {
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
disableTracking
} = node
// 是否有自定义指令
if (directives) {
push(helper(WITH_DIRECTIVES) + `(`)
}
// 是否创建block
if (isBlock) {
push(`(${helper(OPEN_BLOCK)}(${disableTracking ? `true` : ``}), `)
}
if (pure) {
push(PURE_ANNOTATION)
}
// 决定是创建block还是普通vnode
push(helper(isBlock ? CREATE_BLOCK : CREATE_VNODE) + `(`, node)
// 将tag、props、children等gencodeNode转化为真正的可渲染node,即我们手写render函数时
// 传入的props、children...
genNodeList(
// 非空参数数组,tag, props...这个时候还是codegenNode表达式对象
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
context
)
push(`)`)
if (isBlock) {
push(`)`)
}
if (directives) {
push(`, `)
genNode(directives, context)
push(`)`)
}
}
genNodeList:
将createVNode所需的参数(tag、props、children and so on)从gencodeNode形式转化为实际调用时的入参形式。
function genNodeList(
// nodes接受的是tag、props、children这些值对应的表达式对象(gencodeNode)
nodes: (string | symbol | CodegenNode | TemplateChildNode[])[],
context: CodegenContext,
multilines: boolean = false,
comma: boolean = true
) {
const { push, newline } = context
// 将gencodeNode依次生成对应的最终节点,即我们createVnode时的入参
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (isString(node)) {
push(node)
} else if (isArray(node)) {
genNodeListAsArray(node, context)
} else {
genNode(node, context)
}
if (i < nodes.length - 1) {
if (multilines) {
comma && push(',')
newline()
} else {
comma && push(', ')
}
}
}
}
五、示例
对于下面的模版
<template>
<div id="app">
<div id="nav">
<p @click="handleAdd">count: {{ count }}</p>
<p>{{ bigCount }}</p>
</div>
</div>
</template>
编译器生成的render函数如下方代码所示:
_hoisted_${I}
是被提升的静态属性,由于render函数在编译时是以闭包的形式生成的,因此这些提升的变量声明在闭包外的函数体中。
function render(_ctx, _cache) {
return (
openBlock(),
createBlock(
"div",
_hoisted_1,
[createVNode("div", _hoisted_2, [
createVNode("p", {
onClick: _cache[1] || (_cache[1] = function ($event) {
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
return _ctx.handleAdd.apply(
_ctx, [$event].concat(args));
})
},
"count: " + toDisplayString(_ctx.count), 1
/* TEXT */
),
createVNode("p", null, toDisplayString(_ctx.bigCount), 1
/* TEXT */
)]
)]
)
);
}