前言
作者简介:Quixn,专注于 Node.js 技术栈分享,前端从 JavaScript 到 Node.js,再到后端数据库,优质文章推荐,【小 Q 全栈指南】作者,Github 博客开源项目 github.com/Quixn...
大家好,我是 Quixn。今天简单聊聊 Vue 是怎样对AST进行静态分析的?
。也是对此前的"小白都能看懂的 Vue 渲染过程"一文的补充。本文分为两大部分:简要概括分析(比较通俗易懂,小白都能看懂)和源码阅读(比较枯燥,适合有一定基础的同学)。文章较长,文末有精简版总结
。
Vue 是怎样对 AST 进行静态分析的?
想要知道Vue
是怎样对AST
进行静态分析的,那么我们需要先知道一个前置条件,AST
节点又是怎么来的?下面我面先简要介绍一下Vue
是如何根据模板代码生成的AST
节点的,然后再分两部分介绍 Vue
是怎样对AST
进行静态分析的。
Vue 是如何根据模板代码生成的 AST 节点的?
Vue 根据模板代码生成 AST 节点的过程主要涉及词法分析
和语法分析
两个步骤。下面是详细的步骤解释:
-
词法分析(Lexical Analysis)
:Vue 首先将模板代码作为输入
,通过词法分析将其拆解成一个个的标记(Tokens)
。词法分析器会按照一定的规则
扫描模板代码,根据遇到的字符和符号,识别出不同的标记
。 -
语法分析(Syntax Analysis)
:在词法分析的基础上,Vue 使用语法分析器(Parser)
将标记序列转换为抽象语法树(AST)。语法分析器会根据预定义的语法规则,分析标记之间的关系和结构,并构建相应的 AST 节点。
下面是一个简单示例,演示 Vue 如何根据模板代码生成 AST 节点的过程:
js
<template>
<div>
<h1>{{ title }}</h1>
<p v-if="showMessage">{{ message }}</p>
<button @click="handleAction">小Q全栈指南</button>
</div>
</template>
在以上示例中,Vue 会进行以下步骤来生成对应的 AST 节点:
词法分析:Vue 会通过词法分析器将模板代码拆解成一系列的标记,如下所示:
js
[
{ type: 'tag-start', value: 'div' },
{ type: 'tag-end', value: 'div' },
{ type: 'tag-start', value: 'h1' },
{ type: 'mustache', value: 'title' },
{ type: 'tag-end', value: 'h1' },
{ type: 'tag-start', value: 'p' },
{ type: 'directive', value: 'v-if="showMessage"' },
{ type: 'mustache', value: 'message' },
{ type: 'tag-end', value: 'p' },
{ type: 'tag-start', value: 'button' },
{ type: 'event', value: '@click="handleAction"' },
{ type: 'text', value: '小Q全栈指南' },
{ type: 'tag-end', value: 'button' },
{ type: 'tag-start', value: '/div' },
{ type: 'tag-end', value: '/div' }
]
语法分析:Vue 的语法分析器会根据定义好的语法规则,将上述标记序列转换为 AST 节点。最终生成的抽象语法树如下所示:
js
{
type: 'root',
children: [
{
type: 'element',
tag: 'div',
children: [
{
type: 'element',
tag: 'h1',
children: [
{
type: 'mustache',
expression: 'title'
}
]
},
{
type: 'element',
tag: 'p',
directives: [
{
type: 'directive',
name: 'if',
value: 'showMessage'
}
],
children: [
{
type: 'mustache',
expression: 'message'
}
]
},
{
type: 'element',
tag: 'button',
events: [
{
type: 'event',
name: 'click',
value: 'handleAction'
}
],
children: [
{
type: 'text',
content: '小Q全栈指南'
}
]
}
]
}
]
}
通过以上步骤,Vue
成功将模板代码
转换为了对应的AST节点
。AST 节点将成为后续编译过程的基础,用于生成渲染函数
以及进行其他的静态分析和优化操作。
简要概括分析
在Vue
的编译过程中,它会对抽象语法树(AST)
进行静态分析。这意味着Vue
会通过分析AST
的结构和节点信息来提前了解模板中的各种情况,并作出相应的优化。
Vue
在AST
静态分析中的一些处理方式:
静态节点标记
:Vue 会遍历 AST,标记那些不需要动态更新的节点(即静态节点)。静态节点意味着其内容不会随数据变化而改变,可以在编译阶段直接生成静态的 DOM 片段,以提高性能。
静态属性提升
:Vue 会将那些被所有实例共享且不会改变的属性提升到编译阶段,避免每个实例都创建相同的属性。
条件判断优化
:Vue 会检测模板中的条件判断语句,尽可能地将静态的条件块提取出来,以减少运行时的条件判断次数。
下面是一个简单的示例,用于说明Vue
对AST
的静态分析过程:
js
<template>
<div>
<h1>{{ title }}</h1>
<p v-if="showMessage">{{ message }}</p>
<button @click="handleAction">小Q全栈指南</button>
</div>
</template>
<script>
export default {
data() {
return {
title: 'Vue静态分析示例',
showMessage: true,
message: 'Hello, Vue!'
};
},
methods: {
handleAction() {
this.showMessage = !this.showMessage;
}
}
}
</script>
在上述示例中,Vue 会进行以下静态分析操作:
- 1、遍历模板语法树,标记
<h1>{{ title }}</h1>
为静态节点,因为title
是一个不变的数据。 - 2、检测到
v-if="showMessage"
,Vue 会优化条件判断,将其提取出来,以避免每次渲染都重新计算条件。 - 3、解析
@click="handleAction"
,生成相应的事件处理函数。
通过以上静态分析过程,Vue 能够更好地理解模板并进行相应的优化,从而提高渲染性能和效率。
但是,具体的静态分析实现细节可能在不同版本的 Vue 中有所差异, 在 Vue 2.x 中,静态分析主要依赖于 Vue 的编译器vue-template-compile
来进行。而在 Vue 3.x 中,引入了基于Proxy
的响应式系统,对模板编译和渲染管道进行了全面重写,以提供更好的性能和更小的包体积。
源码阅读
Vue 的源码是通过编译器
将模板代码
转换成抽象语法树(AST)
节点的。
具体来说,Vue 的编译过程主要包括以下几个步骤:
-
解析:
编译器会先对模板代码进行解析,将其转换为一个初始的 AST。这个过程使用了HTML解析器
和文本解析器
,通过遍历模板代码的字符
来构建初始的 AST。 -
优化:
编译器会对初始的 AST 进行一些优化处理,例如静态节点的标记
、静态属性的提升
等。这些优化可以减少运行时的性能开销。 -
代码生成:
在完成优化后,编译器会将AST
转换为可执行的渲染函数
。在这个过程中,编译器会遍历 AST 节点,并根据节点的类型生成相应的代码片段。例如,对于元素节点,编译器会生成创建元素的代码;对于文本节点,编译器会生成插入文本的代码。
下面是 Vue2 源码中涉及到 AST 生成的相关文件和函数:
-
compiler/index.js:编译器的入口文件,定义了编译器的主要逻辑。
-
compiler/create-compiler:创建编译器的辅助函数,用于根据不同的配置创建不同类型的编译器。
-
compiler/parser/index.js:定义了解析器的逻辑,包括 HTML 解析器和文本解析器。
-
compiler/optimizer.js:定义了优化器的逻辑,包括静态节点的标记和属性提升等优化过程。
-
compiler/codegen/index.js:定义了代码生成器的逻辑,包括根据 AST 生成渲染函数的代码。
以上是 Vue 源码中与 AST 生成相关的文件和函数,通过这些模块的协作,Vue 的编译器能够将模板代码转换为可执行的渲染函数。
compiler/index.js
源码:
ts
import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'
import { CompilerOptions, CompiledResult } from 'types/compiler'
// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
在这段代码中,通过调用 createCompilerCreator
函数创建了一个编译器 baseCompile
。该编译器接受一个模板字符串和编译选项作为参数,并返回编译结果对象 CompiledResult
。具体的编译过程如下:
- 首先,使用
parse
函数将模板字符串解析成 AST,去除首尾空格。 - 如果编译选项中的
optimize
属性值不为false
,则调用optimize
函数对 AST 进行优化处理。 - 使用
generate
函数将优化后的 AST 生成可执行的 JavaScript 代码。 - 最后,将生成的 AST、渲染函数和静态渲染函数作为属性存储在编译结果对象中,并返回该对象。
这段代码的目的是创建一个基础的编译器,它使用默认的解析器、优化器和代码生成器。可以根据需要,使用 createCompilerCreator
函数创建其他类型的编译器,例如用于 SSR(服务器端渲染)的优化编译器。
compiler/create-compiler
源码:
ts
import { extend } from 'shared/util'
import { CompilerOptions, CompiledResult, WarningMessage } from 'types/compiler'
import { detectErrors } from './error-detector'
import { createCompileToFunctionFn } from './to-function'
export function createCompilerCreator(baseCompile: Function): Function {
return function createCompiler(baseOptions: CompilerOptions) {
function compile(
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
const errors: WarningMessage[] = []
const tips: WarningMessage[] = []
let warn = (
msg: WarningMessage,
range: { start: number; end: number },
tip: string
) => {
;(tip ? tips : errors).push(msg)
}
if (options) {
if (__DEV__ && options.outputSourceRange) {
// $flow-disable-line
const leadingSpaceLength = template.match(/^\s*/)![0].length
warn = (
msg: WarningMessage | string,
range: { start: number; end: number },
tip: string
) => {
const data: WarningMessage = typeof msg === 'string' ? { msg } : msg
if (range) {
if (range.start != null) {
data.start = range.start + leadingSpaceLength
}
if (range.end != null) {
data.end = range.end + leadingSpaceLength
}
}
;(tip ? tips : errors).push(data)
}
}
// merge custom modules
if (options.modules) {
finalOptions.modules = (baseOptions.modules || []).concat(
options.modules
)
}
// merge custom directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
)
}
// copy other options
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key as keyof CompilerOptions]
}
}
}
finalOptions.warn = warn
const compiled = baseCompile(template.trim(), finalOptions)
if (__DEV__) {
detectErrors(compiled.ast, warn)
}
compiled.errors = errors
compiled.tips = tips
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
代码逻辑:
它导出了一个函数 createCompilerCreator
和一个对象。
函数 createCompilerCreator
接受一个参数 baseCompile
,并返回一个函数。返回的函数接受一个参数 baseOptions
,并返回一个对象。
返回的对象包含两个方法: compile
和 compileToFunctions
。
compile
方法接受两个参数: template
和 options
,并返回一个编译结果对象。在这个方法中,首先创建了一个 finalOptions
对象,作为编译选项的基础。然后创建了两个空数组 errors
和 tips
,用于存储警告信息。接下来定义了一个 warn
函数,用于将警告信息推入对应的数组中。
如果传入了 options
参数,会根据不同的选项进行处理。首先判断是否开启了源码范围输出,如果开启了,会计算模板的前导空格长度,并重写 warn
函数,将警告信息的范围进行调整后再推入数组中。然后,会合并自定义的模块和指令到 finalOptions
中,并将其他选项复制到 finalOptions
中。
最后,将 warn
函数赋值给 finalOptions
的 warn
属性,调用 baseCompile
方法对模板进行编译,并将编译结果赋值给 compiled
。在开发环境下,还会调用 detectErrors
方法检测编译结果中的错误,并将错误信息推入 errors
数组中。最后,将 errors
和 tips
数组赋值给 compiled
的 errors
和 tips
属性,并返回 compiled
。
compileToFunctions
方法调用了 createCompileToFunctionFn
函数,并将 compile
方法作为参数传入,返回一个编译为函数的方法。
这段代码的作用是:
创建一个编译器,用于将模板编译为可执行的函数。它接受一个基础编译函数 baseCompile
和编译选项 baseOptions
,并返回一个包含 compile
和 compileToFunctions
方法的对象。
compile
方法接受模板和可选的编译选项作为参数,将模板编译为一个包含编译结果的对象。在编译过程中,它会处理选项,合并自定义模块和指令,以及处理警告信息。最后,它将编译结果返回。
compileToFunctions
方法则是将 compile
方法进行进一步封装,将编译结果转换为可执行的函数形式。
总的来说,这段代码实现了一个编译器的创建和模板编译的功能。
compiler/parser/index.js
源码:
ts
import he from 'he'
import { parseHTML } from './html-parser'
import { parseText } from './text-parser'
import { parseFilters } from './filter-parser'
import { genAssignmentCode } from '../directives/model'
import { extend, cached, no, camelize, hyphenate } from 'shared/util'
import { isIE, isEdge, isServerRendering } from 'core/util/env'
import {
addProp,
addAttr,
baseWarn,
addHandler,
addDirective,
getBindingAttr,
getAndRemoveAttr,
getRawBindingAttr,
pluckModuleFunction,
getAndRemoveAttrByRegex
} from '../helpers'
import {
ASTAttr,
ASTElement,
ASTIfCondition,
ASTNode,
ASTText,
CompilerOptions
} from 'types/compiler'
export const onRE = /^@|^v-on:/
export const dirRE = process.env.VBIND_PROP_SHORTHAND
? /^v-|^@|^:|^\.|^#/
: /^v-|^@|^:|^#/
export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
const dynamicArgRE = /^\[.*\]$/
const argRE = /:(.*)$/
export const bindRE = /^:|^\.|^v-bind:/
const propBindRE = /^\./
const modifierRE = /\.[^.\]]+(?=[^\]]*$)/g
export const slotRE = /^v-slot(:|$)|^#/
const lineBreakRE = /[\r\n]/
const whitespaceRE = /[ \f\t\r\n]+/g
const invalidAttributeRE = /[\s"'<>\/=]/
const decodeHTMLCached = cached(he.decode)
export const emptySlotScopeToken = `_empty_`
// configurable state
export let warn: any
let delimiters
let transforms
let preTransforms
let postTransforms
let platformIsPreTag
let platformMustUseProp
let platformGetTagNamespace
let maybeComponent
export function createASTElement(
tag: string,
attrs: Array<ASTAttr>,
parent: ASTElement | void
): ASTElement {
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent,
children: []
}
}
/**
* Convert HTML string to AST.
*/
export function parse(template: string, options: CompilerOptions): ASTElement {
warn = options.warn || baseWarn
platformIsPreTag = options.isPreTag || no
platformMustUseProp = options.mustUseProp || no
platformGetTagNamespace = options.getTagNamespace || no
const isReservedTag = options.isReservedTag || no
maybeComponent = (el: ASTElement) =>
!!(
el.component ||
el.attrsMap[':is'] ||
el.attrsMap['v-bind:is'] ||
!(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag))
)
transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
delimiters = options.delimiters
const stack: any[] = []
const preserveWhitespace = options.preserveWhitespace !== false
const whitespaceOption = options.whitespace
let root
let currentParent
let inVPre = false
let inPre = false
let warned = false
function warnOnce(msg, range) {
if (!warned) {
warned = true
warn(msg, range)
}
}
function closeElement(element) {
trimEndingWhitespace(element)
if (!inVPre && !element.processed) {
element = processElement(element, options)
}
// tree management
if (!stack.length && element !== root) {
// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
if (__DEV__) {
checkRootConstraints(element)
}
addIfCondition(root, {
exp: element.elseif,
block: element
})
} else if (__DEV__) {
warnOnce(
`Component template should contain exactly one root element. ` +
`If you are using v-if on multiple elements, ` +
`use v-else-if to chain them instead.`,
{ start: element.start }
)
}
}
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else {
if (element.slotScope) {
// scoped slot
// keep it in the children list so that v-else(-if) conditions can
// find it as the prev node.
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[
name
] = element
}
currentParent.children.push(element)
element.parent = currentParent
}
}
// final children cleanup
// filter out scoped slots
element.children = element.children.filter(c => !c.slotScope)
// remove trailing whitespace node again
trimEndingWhitespace(element)
// check pre state
if (element.pre) {
inVPre = false
}
if (platformIsPreTag(element.tag)) {
inPre = false
}
// apply post-transforms
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
}
function trimEndingWhitespace(el) {
// remove trailing whitespace node
if (!inPre) {
let lastNode
while (
(lastNode = el.children[el.children.length - 1]) &&
lastNode.type === 3 &&
lastNode.text === ' '
) {
el.children.pop()
}
}
}
function checkRootConstraints(el) {
if (el.tag === 'slot' || el.tag === 'template') {
warnOnce(
`Cannot use <${el.tag}> as component root element because it may ` +
'contain multiple nodes.',
{ start: el.start }
)
}
if (el.attrsMap.hasOwnProperty('v-for')) {
warnOnce(
'Cannot use v-for on stateful component root element because ' +
'it renders multiple elements.',
el.rawAttrsMap['v-for']
)
}
}
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
start(tag, attrs, unary, start, end) {
// check namespace.
// inherit parent ns if there is one
const ns =
(currentParent && currentParent.ns) || platformGetTagNamespace(tag)
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs)
}
let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
element.ns = ns
}
if (__DEV__) {
if (options.outputSourceRange) {
element.start = start
element.end = end
element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
cumulated[attr.name] = attr
return cumulated
}, {})
}
attrs.forEach(attr => {
if (invalidAttributeRE.test(attr.name)) {
warn(
`Invalid dynamic argument expression: attribute names cannot contain ` +
`spaces, quotes, <, >, / or =.`,
options.outputSourceRange
? {
start: attr.start! + attr.name.indexOf(`[`),
end: attr.start! + attr.name.length
}
: undefined
)
}
})
}
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true
__DEV__ &&
warn(
'Templates should only be responsible for mapping the state to the ' +
'UI. Avoid placing tags with side-effects in your templates, such as ' +
`<${tag}>` +
', as they will not be parsed.',
{ start: element.start }
)
}
// apply pre-transforms
for (let i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element
}
if (!inVPre) {
processPre(element)
if (element.pre) {
inVPre = true
}
}
if (platformIsPreTag(element.tag)) {
inPre = true
}
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
processFor(element)
processIf(element)
processOnce(element)
}
if (!root) {
root = element
if (__DEV__) {
checkRootConstraints(root)
}
}
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
},
end(tag, start, end) {
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
if (__DEV__ && options.outputSourceRange) {
element.end = end
}
closeElement(element)
},
chars(text: string, start?: number, end?: number) {
if (!currentParent) {
if (__DEV__) {
if (text === template) {
warnOnce(
'Component template requires a root element, rather than just text.',
{ start }
)
} else if ((text = text.trim())) {
warnOnce(`text "${text}" outside root element will be ignored.`, {
start
})
}
}
return
}
// IE textarea placeholder bug
/* istanbul ignore if */
if (
isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
const children = currentParent.children
if (inPre || text.trim()) {
text = isTextTag(currentParent)
? text
: (decodeHTMLCached(text) as string)
} else if (!children.length) {
// remove the whitespace-only node right after an opening tag
text = ''
} else if (whitespaceOption) {
if (whitespaceOption === 'condense') {
// in condense mode, remove the whitespace node if it contains
// line break, otherwise condense to a single space
text = lineBreakRE.test(text) ? '' : ' '
} else {
text = ' '
}
} else {
text = preserveWhitespace ? ' ' : ''
}
if (text) {
if (!inPre && whitespaceOption === 'condense') {
// condense consecutive whitespaces into single space
text = text.replace(whitespaceRE, ' ')
}
let res
let child: ASTNode | undefined
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
} else if (
text !== ' ' ||
!children.length ||
children[children.length - 1].text !== ' '
) {
child = {
type: 3,
text
}
}
if (child) {
if (__DEV__ && options.outputSourceRange) {
child.start = start
child.end = end
}
children.push(child)
}
}
},
comment(text: string, start, end) {
// adding anything as a sibling to the root node is forbidden
// comments should still be allowed, but ignored
if (currentParent) {
const child: ASTText = {
type: 3,
text,
isComment: true
}
if (__DEV__ && options.outputSourceRange) {
child.start = start
child.end = end
}
currentParent.children.push(child)
}
}
})
return root
}
function processPre(el) {
if (getAndRemoveAttr(el, 'v-pre') != null) {
el.pre = true
}
}
function processRawAttrs(el) {
const list = el.attrsList
const len = list.length
if (len) {
const attrs: Array<ASTAttr> = (el.attrs = new Array(len))
for (let i = 0; i < len; i++) {
attrs[i] = {
name: list[i].name,
value: JSON.stringify(list[i].value)
}
if (list[i].start != null) {
attrs[i].start = list[i].start
attrs[i].end = list[i].end
}
}
} else if (!el.pre) {
// non root node in pre blocks with no attributes
el.plain = true
}
}
export function processElement(element: ASTElement, options: CompilerOptions) {
processKey(element)
// determine whether this is a plain element after
// removing structural attributes
element.plain =
!element.key && !element.scopedSlots && !element.attrsList.length
processRef(element)
processSlotContent(element)
processSlotOutlet(element)
processComponent(element)
for (let i = 0; i < transforms.length; i++) {
element = transforms[i](element, options) || element
}
processAttrs(element)
return element
}
function processKey(el) {
const exp = getBindingAttr(el, 'key')
if (exp) {
if (__DEV__) {
if (el.tag === 'template') {
warn(
`<template> cannot be keyed. Place the key on real elements instead.`,
getRawBindingAttr(el, 'key')
)
}
if (el.for) {
const iterator = el.iterator2 || el.iterator1
const parent = el.parent
if (
iterator &&
iterator === exp &&
parent &&
parent.tag === 'transition-group'
) {
warn(
`Do not use v-for index as key on <transition-group> children, ` +
`this is the same as not using keys.`,
getRawBindingAttr(el, 'key'),
true /* tip */
)
}
}
}
el.key = exp
}
}
function processRef(el) {
const ref = getBindingAttr(el, 'ref')
if (ref) {
el.ref = ref
el.refInFor = checkInFor(el)
}
}
export function processFor(el: ASTElement) {
let exp
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
const res = parseFor(exp)
if (res) {
extend(el, res)
} else if (__DEV__) {
warn(`Invalid v-for expression: ${exp}`, el.rawAttrsMap['v-for'])
}
}
}
type ForParseResult = {
for: string
alias: string
iterator1?: string
iterator2?: string
}
export function parseFor(exp: string): ForParseResult | undefined {
const inMatch = exp.match(forAliasRE)
if (!inMatch) return
const res: any = {}
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
}
function processIf(el) {
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
el.if = exp
addIfCondition(el, {
exp: exp,
block: el
})
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}
function processIfConditions(el, parent) {
const prev = findPrevElement(parent.children)
if (prev && prev.if) {
addIfCondition(prev, {
exp: el.elseif,
block: el
})
} else if (__DEV__) {
warn(
`v-${el.elseif ? 'else-if="' + el.elseif + '"' : 'else'} ` +
`used on element <${el.tag}> without corresponding v-if.`,
el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else']
)
}
}
function findPrevElement(children: Array<any>): ASTElement | void {
let i = children.length
while (i--) {
if (children[i].type === 1) {
return children[i]
} else {
if (__DEV__ && children[i].text !== ' ') {
warn(
`text "${children[i].text.trim()}" between v-if and v-else(-if) ` +
`will be ignored.`,
children[i]
)
}
children.pop()
}
}
}
export function addIfCondition(el: ASTElement, condition: ASTIfCondition) {
if (!el.ifConditions) {
el.ifConditions = []
}
el.ifConditions.push(condition)
}
function processOnce(el) {
const once = getAndRemoveAttr(el, 'v-once')
if (once != null) {
el.once = true
}
}
// handle content being passed to a component as slot,
// e.g. <template slot="xxx">, <div slot-scope="xxx">
function processSlotContent(el) {
let slotScope
if (el.tag === 'template') {
slotScope = getAndRemoveAttr(el, 'scope')
/* istanbul ignore if */
if (__DEV__ && slotScope) {
warn(
`the "scope" attribute for scoped slots have been deprecated and ` +
`replaced by "slot-scope" since 2.5. The new "slot-scope" attribute ` +
`can also be used on plain elements in addition to <template> to ` +
`denote scoped slots.`,
el.rawAttrsMap['scope'],
true
)
}
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
/* istanbul ignore if */
if (__DEV__ && el.attrsMap['v-for']) {
warn(
`Ambiguous combined usage of slot-scope and v-for on <${el.tag}> ` +
`(v-for takes higher priority). Use a wrapper <template> for the ` +
`scoped slot to make it clearer.`,
el.rawAttrsMap['slot-scope'],
true
)
}
el.slotScope = slotScope
}
// slot="xxx"
const slotTarget = getBindingAttr(el, 'slot')
if (slotTarget) {
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
el.slotTargetDynamic = !!(
el.attrsMap[':slot'] || el.attrsMap['v-bind:slot']
)
// preserve slot as an attribute for native shadow DOM compat
// only for non-scoped slots.
if (el.tag !== 'template' && !el.slotScope) {
addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
}
}
// 2.6 v-slot syntax
if (process.env.NEW_SLOT_SYNTAX) {
if (el.tag === 'template') {
// v-slot on <template>
const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
if (slotBinding) {
if (__DEV__) {
if (el.slotTarget || el.slotScope) {
warn(`Unexpected mixed usage of different slot syntaxes.`, el)
}
if (el.parent && !maybeComponent(el.parent)) {
warn(
`<template v-slot> can only appear at the root level inside ` +
`the receiving component`,
el
)
}
}
const { name, dynamic } = getSlotName(slotBinding)
el.slotTarget = name
el.slotTargetDynamic = dynamic
el.slotScope = slotBinding.value || emptySlotScopeToken // force it into a scoped slot for perf
}
} else {
// v-slot on component, denotes default slot
const slotBinding = getAndRemoveAttrByRegex(el, slotRE)
if (slotBinding) {
if (__DEV__) {
if (!maybeComponent(el)) {
warn(
`v-slot can only be used on components or <template>.`,
slotBinding
)
}
if (el.slotScope || el.slotTarget) {
warn(`Unexpected mixed usage of different slot syntaxes.`, el)
}
if (el.scopedSlots) {
warn(
`To avoid scope ambiguity, the default slot should also use ` +
`<template> syntax when there are other named slots.`,
slotBinding
)
}
}
// add the component's children to its default slot
const slots = el.scopedSlots || (el.scopedSlots = {})
const { name, dynamic } = getSlotName(slotBinding)
const slotContainer = (slots[name] = createASTElement(
'template',
[],
el
))
slotContainer.slotTarget = name
slotContainer.slotTargetDynamic = dynamic
slotContainer.children = el.children.filter((c: any) => {
if (!c.slotScope) {
c.parent = slotContainer
return true
}
})
slotContainer.slotScope = slotBinding.value || emptySlotScopeToken
// remove children as they are returned from scopedSlots now
el.children = []
// mark el non-plain so data gets generated
el.plain = false
}
}
}
}
function getSlotName(binding) {
let name = binding.name.replace(slotRE, '')
if (!name) {
if (binding.name[0] !== '#') {
name = 'default'
} else if (__DEV__) {
warn(`v-slot shorthand syntax requires a slot name.`, binding)
}
}
return dynamicArgRE.test(name)
? // dynamic [name]
{ name: name.slice(1, -1), dynamic: true }
: // static name
{ name: `"${name}"`, dynamic: false }
}
// handle <slot/> outlets
function processSlotOutlet(el) {
if (el.tag === 'slot') {
el.slotName = getBindingAttr(el, 'name')
if (__DEV__ && el.key) {
warn(
`\`key\` does not work on <slot> because slots are abstract outlets ` +
`and can possibly expand into multiple elements. ` +
`Use the key on a wrapping element instead.`,
getRawBindingAttr(el, 'key')
)
}
}
}
function processComponent(el) {
let binding
if ((binding = getBindingAttr(el, 'is'))) {
el.component = binding
}
if (getAndRemoveAttr(el, 'inline-template') != null) {
el.inlineTemplate = true
}
}
function processAttrs(el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, syncGen, isDynamic
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
// mark element as dynamic
el.hasBindings = true
// modifiers
modifiers = parseModifiers(name.replace(dirRE, ''))
// support .foo shorthand syntax for the .prop modifier
if (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) {
;(modifiers || (modifiers = {})).prop = true
name = `.` + name.slice(1).replace(modifierRE, '')
} else if (modifiers) {
name = name.replace(modifierRE, '')
}
if (bindRE.test(name)) {
// v-bind
name = name.replace(bindRE, '')
value = parseFilters(value)
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
name = name.slice(1, -1)
}
if (__DEV__ && value.trim().length === 0) {
warn(
`The value for a v-bind expression cannot be empty. Found in "v-bind:${name}"`
)
}
if (modifiers) {
if (modifiers.prop && !isDynamic) {
name = camelize(name)
if (name === 'innerHtml') name = 'innerHTML'
}
if (modifiers.camel && !isDynamic) {
name = camelize(name)
}
if (modifiers.sync) {
syncGen = genAssignmentCode(value, `$event`)
if (!isDynamic) {
addHandler(
el,
`update:${camelize(name)}`,
syncGen,
null,
false,
warn,
list[i]
)
if (hyphenate(name) !== camelize(name)) {
addHandler(
el,
`update:${hyphenate(name)}`,
syncGen,
null,
false,
warn,
list[i]
)
}
} else {
// handler w/ dynamic event name
addHandler(
el,
`"update:"+(${name})`,
syncGen,
null,
false,
warn,
list[i],
true // dynamic
)
}
}
}
if (
(modifiers && modifiers.prop) ||
(!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name))
) {
addProp(el, name, value, list[i], isDynamic)
} else {
addAttr(el, name, value, list[i], isDynamic)
}
} else if (onRE.test(name)) {
// v-on
name = name.replace(onRE, '')
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
name = name.slice(1, -1)
}
addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
} else {
// normal directives
name = name.replace(dirRE, '')
// parse arg
const argMatch = name.match(argRE)
let arg = argMatch && argMatch[1]
isDynamic = false
if (arg) {
name = name.slice(0, -(arg.length + 1))
if (dynamicArgRE.test(arg)) {
arg = arg.slice(1, -1)
isDynamic = true
}
}
addDirective(
el,
name,
rawName,
value,
arg,
isDynamic,
modifiers,
list[i]
)
if (__DEV__ && name === 'model') {
checkForAliasModel(el, value)
}
}
} else {
// literal attribute
if (__DEV__) {
const res = parseText(value, delimiters)
if (res) {
warn(
`${name}="${value}": ` +
'Interpolation inside attributes has been removed. ' +
'Use v-bind or the colon shorthand instead. For example, ' +
'instead of <div id="{{ val }}">, use <div :id="val">.',
list[i]
)
}
}
addAttr(el, name, JSON.stringify(value), list[i])
// #6887 firefox doesn't update muted state if set via attribute
// even immediately after element creation
if (
!el.component &&
name === 'muted' &&
platformMustUseProp(el.tag, el.attrsMap.type, name)
) {
addProp(el, name, 'true', list[i])
}
}
}
}
function checkInFor(el: ASTElement): boolean {
let parent: ASTElement | void = el
while (parent) {
if (parent.for !== undefined) {
return true
}
parent = parent.parent
}
return false
}
function parseModifiers(name: string): Object | void {
const match = name.match(modifierRE)
if (match) {
const ret = {}
match.forEach(m => {
ret[m.slice(1)] = true
})
return ret
}
}
function makeAttrsMap(attrs: Array<Record<string, any>>): Record<string, any> {
const map = {}
for (let i = 0, l = attrs.length; i < l; i++) {
if (__DEV__ && map[attrs[i].name] && !isIE && !isEdge) {
warn('duplicate attribute: ' + attrs[i].name, attrs[i])
}
map[attrs[i].name] = attrs[i].value
}
return map
}
// for script (e.g. type="x/template") or style, do not decode content
function isTextTag(el): boolean {
return el.tag === 'script' || el.tag === 'style'
}
function isForbiddenTag(el): boolean {
return (
el.tag === 'style' ||
(el.tag === 'script' &&
(!el.attrsMap.type || el.attrsMap.type === 'text/javascript'))
)
}
const ieNSBug = /^xmlns:NS\d+/
const ieNSPrefix = /^NS\d+:/
/* istanbul ignore next */
function guardIESVGBug(attrs) {
const res: any[] = []
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i]
if (!ieNSBug.test(attr.name)) {
attr.name = attr.name.replace(ieNSPrefix, '')
res.push(attr)
}
}
return res
}
function checkForAliasModel(el, value) {
let _el = el
while (_el) {
if (_el.for && _el.alias === value) {
warn(
`<${el.tag} v-model="${value}">: ` +
`You are binding v-model directly to a v-for iteration alias. ` +
`This will not be able to modify the v-for source array because ` +
`writing to the alias is like modifying a function local variable. ` +
`Consider using an array of objects and use v-model on an object property instead.`,
el.rawAttrsMap['v-model']
)
}
_el = _el.parent
}
}
这是模板解析器的代码,用于将 HTML 模板字符串转换为抽象语法树(AST)。以下是代码的步骤:
-
导入所需的模块和函数。
-
定义一些正则表达式和常量。
-
定义一些全局变量和函数。
-
创建一个
AST
元素的函数,用于创建 AST 树的节点。 -
定义一个解析函数,接收模板字符串和选项作为参数,并返回 AST 树。
-
在解析函数中,根据选项初始化一些全局变量。
-
定义一些辅助函数和变量。
-
调用
parseHTML
函数,将模板字符串解析为 AST 树。 -
在解析过程中,根据解析的标签、属性和文本内容,创建 AST 节点,并将其添加到 AST 树中。
-
在解析过程中,根据标签、属性和文本内容的不同,执行相应的处理逻辑,如处理
v-for 指令、处理 v-if 指令
等。 -
解析完成后,对 AST 树进行一些处理和优化,如去除空白节点、处理 v-pre 指令等。
-
返回最终的 AST 树。
-
之后是一个 Vue 编译器的处理元素的函数。它接收一个 AST 元素对象和编译选项作为参数,然后对元素进行一系列处理操作,并返回处理后的元素对象。首先,函数调用了
processKey、processRef、processSlotContent、processSlotOutlet、processComponent 和 processAttrs 等函数
来处理元素的 key、ref、插槽内容、插槽出口、组件和属性等。然后,使用一个循环遍历 transforms 数组,对元素对象进行一系列转换操作。最后,返回处理后的元素对象。 -
之后定义了一个类型
ForParseResult
,表示解析 v-for 属性后的结果,包括 for、alias、iterator1 和 iterator2 四个属性。定义了一个函数 parseFor,用来解析 v-for 属性。该函数首先使用正则表达式匹配 v-for 属性,如果匹配不到,则返回 undefined。如果匹配到了 v-for 属性,则创建一个空对象 res,将匹配到的值存入 res 的 for 属性中。然后,提取 v-for 属性中的 alias 部分,并使用正则表达式去除可能存在的括号,将结果存入 res 的 alias 属性中。接着,再次使用正则表达式匹配 alias 属性中的迭代器部分,如果匹配到了,则将匹配到的值存入 res 的 iterator1 属性中。如果还存在第二个迭代器,则将其存入 res 的 iterator2 属性中。如果没有匹配到迭代器,则直接将 alias 存入 res 的 alias 属性中。最后,返回 res 作为解析结果。 -
接下来,定义了一系列处理 v-if、v-else-if、v-else、v-once 和 slot 相关属性的函数,包括 processIf、processIfConditions、processOnce 和 processSlotContent。
processIf 函数
用于处理 v-if 属性。processIfConditions 函数
用于处理 v-else-if 和 v-else 属性。findPrevElement 函数
用于在 children 数组中找到前一个元素,如果该元素是 ASTElement 类型,则返回该元素,否则,如果该元素是文本节点且不为空格,则将其从 children 数组中删除。该函数用于处理 v-if 和 v-else 之间的文本节点。addIfCondition 函数
用于将条件添加到 ASTElement 的 ifConditions 数组中。- 定义了一个
函数 processSlotContent
,用于处理 slot 相关的属性。
- 最后是用于处理模板中的属性和指令的方法。
-
getSlotName(binding)
函数用于获取插槽的名称。它首先从binding.name
中移除插槽的正则表达式匹配项,并将结果赋给name
变量。如果name
为空,则判断binding.name[0]
是否为'#'
,如果是,则在开发环境下会发出警告。最后,根据name
是否满足动态参数的正则表达式,返回一个对象,包含插槽的名称和是否为动态插槽。 -
processSlotOutlet(el)
函数用于处理<slot/>
元素。如果元素的标签为slot
,则通过getBindingAttr(el, 'name')
获取插槽的名称,并赋值给el.slotName
。如果在开发环境下el.key
存在,则发出警告。 -
processComponent(el)
函数用于处理组件元素。首先通过getBindingAttr(el, 'is')
获取组件的is
属性,并将结果赋给el.component
。然后判断el
是否存在inline-template
属性,并将结果赋给el.inlineTemplate
。 -
processAttrs(el)
函数用于处理元素的属性。首先获取元素的属性列表,并遍历列表中的每个属性。如果属性的名称满足指令的正则表达式,则表示该属性是一个指令。在处理指令的过程中,会标记元素为动态元素,并解析指令的修饰符。如果指令的名称满足.foo
的缩写语法,则将修饰符中的prop
设置为true
,并修改指令的名称。然后判断指令的类型,如果是v-bind
指令,则移除指令的前缀,并解析指令的值。如果指令的名称满足动态参数的正则表达式,则将指令的名称修改为去掉头尾字符的结果。如果指令的值为空,并且在开发环境下,会发出警告。如果指令存在修饰符,并且修饰符中包含prop
,并且指令的名称不是动态的,则将指令的名称转为驼峰命名,并将innerHtml
转为innerHTML
。如果修饰符中包含camel
,并且指令的名称不是动态的,则将指令的名称转为驼峰命名。如果修饰符中包含sync
,则生成一个同步更新的处理函数,并根据指令的名称是否是动态的,添加相应的事件处理函数。如果指令需要使用prop
,或者元素不是组件且需要使用prop
,则调用addProp
函数添加属性。否则,调用addAttr
函数添加属性。如果指令的名称满足v-on
的正则表达式,则移除指令的前缀,并判断指令的名称是否是动态的,然后调用addHandler
函数添加事件处理函数。如果指令的名称不满足以上两个正则表达式,则表示该属性是一个普通的属性。在开发环境下,会检查属性值是否包含插值,并发出相应的警告。最后,调用addAttr
函数添加属性。 -
checkInFor(el: ASTElement): boolean
函数用于检查元素是否在v-for
指令中。通过遍历元素的父级元素,判断父级元素是否存在for
属性,如果存在,则返回true
,否则返回false
。 -
parseModifiers(name: string): Object | void
函数用于解析指令的修饰符。通过正则表达式匹配修饰符,并返回一个包含修饰符的对象。 -
makeAttrsMap(attrs: Array<Record<string, any>>): Record<string, any>
函数用于将属性列表转换为属性映射对象。遍历属性列表,将属性的名称作为键,属性的值作为值,存储在映射对象中。 -
isTextTag(el): boolean
函数用于判断元素是否是文本标签。如果元素的标签是script
或style
,则返回true
,否则返回false
。 -
isForbiddenTag(el): boolean
函数用于判断元素是否是禁止的标签。如果元素的标签是style
或script
,且type
属性为空或为text/javascript
,则返回true
,否则返回false
。 -
guardIESVGBug(attrs)
函数用于修复 IE 中的 SVG bug。遍历属性列表,将不满足ieNSBug
正则表达式的属性的名称中的ieNSPrefix
替换为空字符串,并将结果存储在新的数组中。 -
checkForAliasModel(el, value)
函数用于检查v-model
指令是否直接绑定到v-for
循环的别名上。通过遍历元素的父级元素,检查父级元素是否存在for
属性,并且别名与v-model
的值相同。如果满足条件,则发出相应的警告。
compiler/optimizer.js
源码:
ts
import { makeMap, isBuiltInTag, cached, no } from 'shared/util'
import { ASTElement, CompilerOptions, ASTNode } from 'types/compiler'
let isStaticKey
let isPlatformReservedTag
const genStaticKeysCached = cached(genStaticKeys)
/**
* Goal of the optimizer: walk the generated template AST tree
* and detect sub-trees that are purely static, i.e. parts of
* the DOM that never needs to change.
*
* Once we detect these sub-trees, we can:
*
* 1. Hoist them into constants, so that we no longer need to
* create fresh nodes for them on each re-render;
* 2. Completely skip them in the patching process.
*/
export function optimize(
root: ASTElement | null | undefined,
options: CompilerOptions
) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
markStatic(root)
// second pass: mark static roots.
markStaticRoots(root, false)
}
function genStaticKeys(keys: string): Function {
return makeMap(
'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +
(keys ? ',' + keys : '')
)
}
function markStatic(node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {
node.static = false
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
if (!block.static) {
node.static = false
}
}
}
}
}
function markStaticRoots(node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor
}
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
if (
node.static &&
node.children.length &&
!(node.children.length === 1 && node.children[0].type === 3)
) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
function isStatic(node: ASTNode): boolean {
if (node.type === 2) {
// expression
return false
}
if (node.type === 3) {
// text
return true
}
return !!(
node.pre ||
(!node.hasBindings && // no dynamic bindings
!node.if &&
!node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey))
)
}
function isDirectChildOfTemplateFor(node: ASTElement): boolean {
while (node.parent) {
node = node.parent
if (node.tag !== 'template') {
return false
}
if (node.for) {
return true
}
}
return false
}
这段代码是编译器中的优化器部分,主要的作用是对生成的模板抽象语法树(AST)进行遍历,检测出那些纯静态的子树,也就是那些在重新渲染时不需要变化的 DOM 部分。
具体的优化过程如下:
- 定义了一些辅助函数和变量,包括
makeMap
、isBuiltInTag
、cached
、no
等。 - 使用
genStaticKeysCached
函数生成静态键的缓存版本。 - 定义了
optimize
函数,接受两个参数:根节点root
和编译选项options
。 - 如果根节点不存在,则直接返回。
- 初始化一些变量
isStaticKey
和isPlatformReservedTag
,用于判断是否为静态节点以及是否为保留标签。 - 第一次遍历:标记所有非静态节点。通过调用
markStatic
函数实现。 - 第二次遍历:标记静态根节点。通过调用
markStaticRoots
函数实现。
具体的函数解释如下:
genStaticKeys
函数:根据传入的键值字符串生成一个函数,用于创建一个包含指定键的映射表。markStatic
函数:递归遍历 AST 节点,并为每个节点标记是否为静态节点。对于元素节点,判断标签是否为保留标签以及是否包含inline-template
属性。markStaticRoots
函数:递归遍历 AST 节点,并为每个节点标记是否为静态根节点。对于元素节点,判断其是否是静态节点或只包含一个文本节点。isStatic
函数:判断一个节点是否为静态节点。对于元素节点,判断是否满足一系列条件,如不包含动态绑定、条件渲染和循环等特性。isDirectChildOfTemplateFor
函数:判断一个元素节点是否是直接处于template
标签内并且存在for
循环的子节点。
通过优化器的处理,可以将那些静态的 DOM 部分提取为常量,避免每次重新渲染时创建新的节点,从而提高渲染性能。同时,在更新过程中也可以完全跳过这些静态的子树,减少不必要的比对和计算操作。
compiler/codegen/index.js
源码:
ts
import { genHandlers } from './events'
import baseDirectives from '../directives/index'
import { camelize, no, extend, capitalize } from 'shared/util'
import { baseWarn, pluckModuleFunction } from '../helpers'
import { emptySlotScopeToken } from '../parser/index'
import {
ASTAttr,
ASTDirective,
ASTElement,
ASTExpression,
ASTIfConditions,
ASTNode,
ASTText,
CompilerOptions
} from 'types/compiler'
import { BindingMetadata, BindingTypes } from 'sfc/types'
type TransformFunction = (el: ASTElement, code: string) => string
type DataGenFunction = (el: ASTElement) => string
type DirectiveFunction = (
el: ASTElement,
dir: ASTDirective,
warn: Function
) => boolean
export class CodegenState {
options: CompilerOptions
warn: Function
transforms: Array<TransformFunction>
dataGenFns: Array<DataGenFunction>
directives: { [key: string]: DirectiveFunction }
maybeComponent: (el: ASTElement) => boolean
onceId: number
staticRenderFns: Array<string>
pre: boolean
constructor(options: CompilerOptions) {
this.options = options
this.warn = options.warn || baseWarn
this.transforms = pluckModuleFunction(options.modules, 'transformCode')
this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
this.directives = extend(extend({}, baseDirectives), options.directives)
const isReservedTag = options.isReservedTag || no
this.maybeComponent = (el: ASTElement) =>
!!el.component || !isReservedTag(el.tag)
this.onceId = 0
this.staticRenderFns = []
this.pre = false
}
}
export type CodegenResult = {
render: string
staticRenderFns: Array<string>
}
export function generate(
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
// fix #11483, Root level <script> tags should not be rendered.
const code = ast
? ast.tag === 'script'
? 'null'
: genElement(ast, state)
: '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
export function genElement(el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
const maybeComponent = state.maybeComponent(el)
if (!el.plain || (el.pre && maybeComponent)) {
data = genData(el, state)
}
let tag: string | undefined
// check if this is a component in <script setup>
const bindings = state.options.bindings
if (maybeComponent && bindings && bindings.__isScriptSetup !== false) {
tag = checkBindingType(bindings, el.tag)
}
if (!tag) tag = `'${el.tag}'`
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c(${tag}${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
function checkBindingType(bindings: BindingMetadata, key: string) {
const camelName = camelize(key)
const PascalName = capitalize(camelName)
const checkType = (type) => {
if (bindings[key] === type) {
return key
}
if (bindings[camelName] === type) {
return camelName
}
if (bindings[PascalName] === type) {
return PascalName
}
}
const fromConst =
checkType(BindingTypes.SETUP_CONST) ||
checkType(BindingTypes.SETUP_REACTIVE_CONST)
if (fromConst) {
return fromConst
}
const fromMaybeRef =
checkType(BindingTypes.SETUP_LET) ||
checkType(BindingTypes.SETUP_REF) ||
checkType(BindingTypes.SETUP_MAYBE_REF)
if (fromMaybeRef) {
return fromMaybeRef
}
}
// hoist static sub-trees out
function genStatic(el: ASTElement, state: CodegenState): string {
el.staticProcessed = true
// Some elements (templates) need to behave differently inside of a v-pre
// node. All pre nodes are static roots, so we can use this as a location to
// wrap a state change and reset it upon exiting the pre node.
const originalPreState = state.pre
if (el.pre) {
state.pre = el.pre
}
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
state.pre = originalPreState
return `_m(${state.staticRenderFns.length - 1}${
el.staticInFor ? ',true' : ''
})`
}
// v-once
function genOnce(el: ASTElement, state: CodegenState): string {
el.onceProcessed = true
if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.staticInFor) {
let key = ''
let parent = el.parent
while (parent) {
if (parent.for) {
key = parent.key!
break
}
parent = parent.parent
}
if (!key) {
__DEV__ &&
state.warn(
`v-once can only be used inside v-for that is keyed. `,
el.rawAttrsMap['v-once']
)
return genElement(el, state)
}
return `_o(${genElement(el, state)},${state.onceId++},${key})`
} else {
return genStatic(el, state)
}
}
export function genIf(
el: any,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
el.ifProcessed = true // avoid recursion
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
function genIfConditions(
conditions: ASTIfConditions,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
if (!conditions.length) {
return altEmpty || '_e()'
}
const condition = conditions.shift()!
if (condition.exp) {
return `(${condition.exp})?${genTernaryExp(
condition.block
)}:${genIfConditions(conditions, state, altGen, altEmpty)}`
} else {
return `${genTernaryExp(condition.block)}`
}
// v-if with v-once should generate code like (a)?_m(0):_m(1)
function genTernaryExp(el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state)
}
}
export function genFor(
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
const exp = el.for
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
if (
__DEV__ &&
state.maybeComponent(el) &&
el.tag !== 'slot' &&
el.tag !== 'template' &&
!el.key
) {
state.warn(
`<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
`v-for should have explicit keys. ` +
`See https://v2.vuejs.org/v2/guide/list.html#key for more info.`,
el.rawAttrsMap['v-for'],
true /* tip */
)
}
el.forProcessed = true // avoid recursion
return (
`${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
)
}
export function genData(el: ASTElement, state: CodegenState): string {
let data = '{'
// directives first.
// directives may mutate the el's other properties before they are generated.
const dirs = genDirectives(el, state)
if (dirs) data += dirs + ','
// key
if (el.key) {
data += `key:${el.key},`
}
// ref
if (el.ref) {
data += `ref:${el.ref},`
}
if (el.refInFor) {
data += `refInFor:true,`
}
// pre
if (el.pre) {
data += `pre:true,`
}
// record original tag name for components using "is" attribute
if (el.component) {
data += `tag:"${el.tag}",`
}
// module data generation functions
for (let i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el)
}
// attributes
if (el.attrs) {
data += `attrs:${genProps(el.attrs)},`
}
// DOM props
if (el.props) {
data += `domProps:${genProps(el.props)},`
}
// event handlers
if (el.events) {
data += `${genHandlers(el.events, false)},`
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true)},`
}
// slot target
// only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
data += `slot:${el.slotTarget},`
}
// scoped slots
if (el.scopedSlots) {
data += `${genScopedSlots(el, el.scopedSlots, state)},`
}
// component v-model
if (el.model) {
data += `model:{value:${el.model.value},callback:${el.model.callback},expression:${el.model.expression}},`
}
// inline-template
if (el.inlineTemplate) {
const inlineTemplate = genInlineTemplate(el, state)
if (inlineTemplate) {
data += `${inlineTemplate},`
}
}
data = data.replace(/,$/, '') + '}'
// v-bind dynamic argument wrap
// v-bind with dynamic arguments must be applied using the same v-bind object
// merge helper so that class/style/mustUseProp attrs are handled correctly.
if (el.dynamicAttrs) {
data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
}
// v-bind data wrap
if (el.wrapData) {
data = el.wrapData(data)
}
// v-on data wrap
if (el.wrapListeners) {
data = el.wrapListeners(data)
}
return data
}
function genDirectives(el: ASTElement, state: CodegenState): string | void {
const dirs = el.directives
if (!dirs) return
let res = 'directives:['
let hasRuntime = false
let i, l, dir, needRuntime
for (i = 0, l = dirs.length; i < l; i++) {
dir = dirs[i]
needRuntime = true
const gen: DirectiveFunction = state.directives[dir.name]
if (gen) {
// compile-time directive that manipulates AST.
// returns true if it also needs a runtime counterpart.
needRuntime = !!gen(el, dir, state.warn)
}
if (needRuntime) {
hasRuntime = true
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
dir.value
? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}`
: ''
}${dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
}
}
if (hasRuntime) {
return res.slice(0, -1) + ']'
}
}
function genInlineTemplate(
el: ASTElement,
state: CodegenState
): string | undefined {
const ast = el.children[0]
if (__DEV__ && (el.children.length !== 1 || ast.type !== 1)) {
state.warn(
'Inline-template components must have exactly one child element.',
{ start: el.start }
)
}
if (ast && ast.type === 1) {
const inlineRenderFns = generate(ast, state.options)
return `inlineTemplate:{render:function(){${
inlineRenderFns.render
}},staticRenderFns:[${inlineRenderFns.staticRenderFns
.map(code => `function(){${code}}`)
.join(',')}]}`
}
}
function genScopedSlots(
el: ASTElement,
slots: { [key: string]: ASTElement },
state: CodegenState
): string {
// by default scoped slots are considered "stable", this allows child
// components with only scoped slots to skip forced updates from parent.
// but in some cases we have to bail-out of this optimization
// for example if the slot contains dynamic names, has v-if or v-for on them...
let needsForceUpdate =
el.for ||
Object.keys(slots).some(key => {
const slot = slots[key]
return (
slot.slotTargetDynamic || slot.if || slot.for || containsSlotChild(slot) // is passing down slot from parent which may be dynamic
)
})
// #9534: if a component with scoped slots is inside a conditional branch,
// it's possible for the same component to be reused but with different
// compiled slot content. To avoid that, we generate a unique key based on
// the generated code of all the slot contents.
let needsKey = !!el.if
// OR when it is inside another scoped slot or v-for (the reactivity may be
// disconnected due to the intermediate scope variable)
// #9438, #9506
// TODO: this can be further optimized by properly analyzing in-scope bindings
// and skip force updating ones that do not actually use scope variables.
if (!needsForceUpdate) {
let parent = el.parent
while (parent) {
if (
(parent.slotScope && parent.slotScope !== emptySlotScopeToken) ||
parent.for
) {
needsForceUpdate = true
break
}
if (parent.if) {
needsKey = true
}
parent = parent.parent
}
}
const generatedSlots = Object.keys(slots)
.map(key => genScopedSlot(slots[key], state))
.join(',')
return `scopedSlots:_u([${generatedSlots}]${
needsForceUpdate ? `,null,true` : ``
}${
!needsForceUpdate && needsKey ? `,null,false,${hash(generatedSlots)}` : ``
})`
}
function hash(str) {
let hash = 5381
let i = str.length
while (i) {
hash = (hash * 33) ^ str.charCodeAt(--i)
}
return hash >>> 0
}
function containsSlotChild(el: ASTNode): boolean {
if (el.type === 1) {
if (el.tag === 'slot') {
return true
}
return el.children.some(containsSlotChild)
}
return false
}
function genScopedSlot(el: ASTElement, state: CodegenState): string {
const isLegacySyntax = el.attrsMap['slot-scope']
if (el.if && !el.ifProcessed && !isLegacySyntax) {
return genIf(el, state, genScopedSlot, `null`)
}
if (el.for && !el.forProcessed) {
return genFor(el, state, genScopedSlot)
}
const slotScope =
el.slotScope === emptySlotScopeToken ? `` : String(el.slotScope)
const fn =
`function(${slotScope}){` +
`return ${
el.tag === 'template'
? el.if && isLegacySyntax
? `(${el.if})?${genChildren(el, state) || 'undefined'}:undefined`
: genChildren(el, state) || 'undefined'
: genElement(el, state)
}}`
// reverse proxy v-slot without scope on this.$slots
const reverseProxy = slotScope ? `` : `,proxy:true`
return `{key:${el.slotTarget || `"default"`},fn:${fn}${reverseProxy}}`
}
export function genChildren(
el: ASTElement,
state: CodegenState,
checkSkip?: boolean,
altGenElement?: Function,
altGenNode?: Function
): string | void {
const children = el.children
if (children.length) {
const el: any = children[0]
// optimize single v-for
if (
children.length === 1 &&
el.for &&
el.tag !== 'template' &&
el.tag !== 'slot'
) {
const normalizationType = checkSkip
? state.maybeComponent(el)
? `,1`
: `,0`
: ``
return `${(altGenElement || genElement)(el, state)}${normalizationType}`
}
const normalizationType = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0
const gen = altGenNode || genNode
return `[${children.map(c => gen(c, state)).join(',')}]${
normalizationType ? `,${normalizationType}` : ''
}`
}
}
// determine the normalization needed for the children array.
// 0: no normalization needed
// 1: simple normalization needed (possible 1-level deep nested array)
// 2: full normalization needed
function getNormalizationType(
children: Array<ASTNode>,
maybeComponent: (el: ASTElement) => boolean
): number {
let res = 0
for (let i = 0; i < children.length; i++) {
const el: ASTNode = children[i]
if (el.type !== 1) {
continue
}
if (
needsNormalization(el) ||
(el.ifConditions &&
el.ifConditions.some(c => needsNormalization(c.block)))
) {
res = 2
break
}
if (
maybeComponent(el) ||
(el.ifConditions && el.ifConditions.some(c => maybeComponent(c.block)))
) {
res = 1
}
}
return res
}
function needsNormalization(el: ASTElement): boolean {
return el.for !== undefined || el.tag === 'template' || el.tag === 'slot'
}
function genNode(node: ASTNode, state: CodegenState): string {
if (node.type === 1) {
return genElement(node, state)
} else if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
export function genText(text: ASTText | ASTExpression): string {
return `_v(${
text.type === 2
? text.expression // no need for () because already wrapped in _s()
: transformSpecialNewlines(JSON.stringify(text.text))
})`
}
export function genComment(comment: ASTText): string {
return `_e(${JSON.stringify(comment.text)})`
}
function genSlot(el: ASTElement, state: CodegenState): string {
const slotName = el.slotName || '"default"'
const children = genChildren(el, state)
let res = `_t(${slotName}${children ? `,function(){return ${children}}` : ''}`
const attrs =
el.attrs || el.dynamicAttrs
? genProps(
(el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
// slot props are camelized
name: camelize(attr.name),
value: attr.value,
dynamic: attr.dynamic
}))
)
: null
const bind = el.attrsMap['v-bind']
if ((attrs || bind) && !children) {
res += `,null`
}
if (attrs) {
res += `,${attrs}`
}
if (bind) {
res += `${attrs ? '' : ',null'},${bind}`
}
return res + ')'
}
// componentName is el.component, take it as argument to shun flow's pessimistic refinement
function genComponent(
componentName: string,
el: ASTElement,
state: CodegenState
): string {
const children = el.inlineTemplate ? null : genChildren(el, state, true)
return `_c(${componentName},${genData(el, state)}${
children ? `,${children}` : ''
})`
}
function genProps(props: Array<ASTAttr>): string {
let staticProps = ``
let dynamicProps = ``
for (let i = 0; i < props.length; i++) {
const prop = props[i]
const value = transformSpecialNewlines(prop.value)
if (prop.dynamic) {
dynamicProps += `${prop.name},${value},`
} else {
staticProps += `"${prop.name}":${value},`
}
}
staticProps = `{${staticProps.slice(0, -1)}}`
if (dynamicProps) {
return `_d(${staticProps},[${dynamicProps.slice(0, -1)}])`
} else {
return staticProps
}
}
// #3895, #4268
function transformSpecialNewlines(text: string): string {
return text.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029')
}
代码逻辑如下:
首先,代码导入了一些需要使用的函数和类型。
然后,定义了一个名为CodegenState
的类,该类包含了一些用于存储编译选项和状态的属性和方法。
接下来,定义了一个名为CodegenResult
的类型,用于表示生成的代码结果。
然后,定义了一个名为generate
的函数,该函数接受一个AST和编译选项作为参数,返回一个CodegenResult对象。在函数内部,首先创建了一个CodegenState
实例,然后根据AST的类型生成相应的代码。如果AST存在且为script标签,则返回'null',否则调用genElement函数生成元素代码。最后,返回一个包含render代码和静态渲染函数数组的对象。
接下来,定义了一个名为genElement
的函数,该函数接受一个AST元素和CodegenState
实例作为参数,返回一个字符串表示的代码。在函数内部,首先根据AST元素的父元素设置pre属性,然后根据AST元素的类型调用相应的生成函数生成代码。如果AST元素是静态根且未处理过,则调用genStatic
函数生成静态代码;如果AST元素是v-once指令且未处理过,则调用genOnce
函数生成v-once代码;如果AST元素是v-for指令且未处理过,则调用genFor函数生成v-for代码;如果AST元素是v-if指令且未处理过,则调用genIf
函数生成v-if代码;如果AST元素是template标签且没有插槽目标且不是pre标签,则调用genChildren
函数生成子元素代码,如果没有子元素则返回'void 0';如果AST元素是slot标签,则调用genSlot
函数生成slot代码;否则,根据AST元素的类型生成组件或元素代码。最后,根据模块的转换函数对代码进行转换。
接下来是一些辅助函数,如checkBindingType
用于检查绑定类型,genStatic用于生成静态代码,genOnce用于生成v-once代码,genIf用于生成v-if代码,genIfConditions用于生成v-if条件代码,genFor用于生成v-for代码。
之后,是一个用于生成数据的函数。函数的参数是一个AST元素和一个代码生成状态对象。
个别函数说明:
-
genDirectives函数
接受一个AST元素和一个代码生成状态对象作为参数。函数首先获取AST元素的directives属性,如果不存在则直接返回。接下来,函数初始化一个res变量,值为一个字符串''。然后,函数遍历directives数组,对每个directive进行处理。函数首先获取directive的name属性,并根据该属性从state.directives中获取对应的生成函数gen。然后,函数判断gen是否存在,如果存在则调用该函数对AST元素和directive进行处理,并根据返回值判断是否需要运行时处理。最后,函数将处理后的directive数据添加到res中。最后,函数根据是否存在运行时处理的directive来决定返回的数据。 -
genInlineTemplate函数
接受一个AST元素和一个代码生成状态对象作为参数。函数首先获取AST元素的第一个子元素,并进行一些判断。如果子元素的数量不等于1或者类型不为1,则会调用state.warn函数进行警告。然后,函数判断子元素是否存在且类型为1。如果满足条件,则调用generate函数生成内联模板相关的数据,并将其作为字符串返回。 -
genScopedSlots函数
接受一个AST元素、一个包含作用域插槽的对象和一个代码生成状态对象作为参数。函数首先判断是否需要强制更新。如果AST元素有for属性或者作用域插槽对象中的某个插槽满足一定条件,则需要强制更新。然后,函数判断是否需要生成唯一的key。如果AST元素有if属性,则需要生成唯一的key。接下来,函数遍历作用域插槽对象,对每个插槽进行处理,并将处理后的数据拼接成字符串。最后,函数返回生成的作用域插槽数据的字符串。 -
hash函数
接受一个字符串作为参数。函数首先初始化一个hash变量为5381。然后,函数遍历字符串的每个字符,对每个字符进行一些运算。最后,函数返回计算后的hash值。 -
containsSlotChild函数
接受一个AST节点作为参数。函数首先判断节点的类型是否为1。如果是1,则判断节点的标签是否为'slot',如果是则返回true。否则,递归遍历节点的子节点,并判断是否存在包含插槽的子节点。如果存在,则返回true。如果不满足上述条件,则返回false。
最后
,是一个用于生成AST元素的子元素的函数。
- 首先,函数接受一个AST元素(el)和一个代码生成状态(state)作为参数。
- 然后,将el的子元素赋值给一个名为
children
的变量。 - 如果
children
数组的长度大于0,则继续执行下面的步骤。 - 检查children数组的长度是否为1,并且第一个子元素(el)具有v-for属性,并且标签不是"template"和"slot"。如果满足这些条件,则进行优化处理。
- 根据
checkSkip
参数的值决定是否需要规范化处理。如果checkSkip
为true,则调用state.maybeComponent(el)函数来确定是否是组件,如果是则返回",1",否则返回",0"。如果checkSkip为false,则返回空字符串。 - 返回一个字符串,其中包括调用
altGenElement
函数(如果存在)或genElement函数生成的元素代码和规范化类型。如果altGenElement不存在,则调用genElement函数生成元素代码。 - 如果checkSkip为true,则调用getNormalizationType函数来确定children数组是否需要规范化处理。否则,返回0。
- 根据
altGenNode
参数的值决定是否需要调用altGenNode函数来生成节点代码。如果altGenNode存在,则调用altGenNode函数,否则调用genNode函数。 - 返回一个包含
children
数组中每个子元素调用gen函数生成的代码的字符串。如果规范化类型不为0,则在字符串末尾添加规范化类型。 - 如果
children
数组的长度为0,则返回空。
getNormalizationType函数
用于确定children数组是否需要规范化处理。
needsNormalization函数
用于确定一个AST元素是否需要规范化处理。
genNode函数
用于生成一个AST节点的代码。
- 检查节点的类型。如果是1,则调用genElement函数生成元素代码。
- 如果节点的类型是3并且isComment属性为true,则调用genComment函数生成注释代码。
- 否则,调用genText函数生成文本代码。
genText函数
用于生成文本节点的代码。
- 返回一个字符串,其中包括调用_v函数生成的代码。
- 如果文本节点的类型是2,则返回文本节点的表达式。
- 否则,调用transformSpecialNewlines函数对文本进行特殊换行符的转换,并将结果作为参数传递给JSON.stringify函数。
genComment函数
用于生成注释节点的代码。
- 返回一个字符串,其中包括调用_e函数生成的代码。
- 将注释节点的文本作为参数传递给JSON.stringify函数。
genSlot函数
用于生成插槽节点的代码。
- 首先,获取插槽的名称,如果插槽没有名称,则默认为"default"。
- 调用genChildren函数生成插槽的子元素代码,并将结果赋值给children变量。
- 初始化一个字符串变量res,其中包括调用_t函数生成的代码和插槽名称。如果children存在,则添加一个匿名函数,返回children的代码。
- 检查插槽的属性(attrs)或动态属性(dynamicAttrs)是否存在。如果存在,则调用genProps函数生成属性的代码。
- 检查插槽的属性中是否有"v-bind"属性。如果attrs或bind存在,并且children不存在,则添加",null"到res字符串中。
- 如果attrs存在,则将attrs的代码添加到res字符串中。
- 如果bind存在,则将bind的代码添加到res字符串中。
- 返回res字符串。
genComponent函数
用于生成组件节点的代码。
- 首先,获取组件的名称。
- 调用genChildren函数生成组件的子元素代码,并将结果赋值给children变量。如果组件具有内联模板,则children为null。
- 返回一个字符串,其中包括调用_c函数生成的代码、组件名称和genData函数生成的数据代码。如果children存在,则添加children的代码。
genProps函数
用于生成属性的代码。
- 初始化两个字符串变量staticProps和dynamicProps。
- 使用循环遍历props数组中的每个属性。
- 获取属性的值,并对特殊换行符进行转换。
- 如果属性是动态属性,则将属性的名称和值添加到dynamicProps字符串中。
- 否则,将属性的名称和值添加到staticProps字符串中。
- 将staticProps的末尾逗号去掉,并将其包装在花括号中。
- 如果dynamicProps存在,则调用_d函数生成动态属性的代码,并将staticProps和dynamicProps作为参数传递。
- 否则,返回staticProps。
transformSpecialNewlines函数
用于将特殊换行符转换为转义序列。
- 使用正则表达式替换所有的"\u2028"为"\u2028"的转义序列。
- 使用正则表达式替换所有的"\u2029"为"\u2029"的转义序列。
- 返回转换后的文本。
总结
-
本文我们先简单的介绍了
Vue
是如何根据模板代码生成的AST
节点的?答案是:Vue
根据模板代码生成AST
节点的过程主要涉及词法分析
和语法分析
两个步骤。Vue在AST静态分析中的一些处理方式有:静态节点标记、静态属性提升、条件判断优化等
。 -
源码部分,我们首先明确了是
通过编译器将模板代码转换成抽象语法树(AST)节点的
。 -
Vue 的编译过程主要包括以下3个步骤:
先解析,再优化,最后代码生成
。 -
解析的过程使用了
HTML解析器
和文本解析器
,通过遍历模板代码的字符来构建初始的 AST。 -
优化的过程编译器会对初始的
AST
进行一些优化处理,例如静态节点的标记
、静态属性的提升
等。 -
代码生成的过程,编译器会将
AST
转换为可执行的渲染函数。在这个过程中,编译器会遍历 AST 节点,并根据节点的类型生成相应的代码片段。例如,对于元素节点,编译器会生成创建元素的代码;对于文本节点,编译器会生成插入文本的代码。 -
最后,我们提及了
Vue2
源码中涉及到AST
生成的5
个重要的相关文件,并做了一些解读。 -
compiler/index.ts
文件是编译器的入口文件,定义了编译器的主要逻辑。通过调用 createCompilerCreator 函数创建了一个编译器 baseCompile。该编译器接受一个模板字符串和编译选项作为参数,并返回编译结果对象 CompiledResult。 -
compiler/create-compiler.ts
文件里创建编译器的辅助函数,用于根据不同的配置创建不同类型的编译器。 -
compiler/parser/index.ts
文件定义了解析器的逻辑,包括 HTML 解析器和文本解析器。 -
compiler/optimizer.ts
文件定义了优化器的逻辑,包括静态节点的标记和属性提升等优化过程。 -
compiler/codegen/index.ts
文件定义了代码生成器的逻辑,包括根据 AST 生成渲染函数的代码。
欢迎关注,公众号回复【vue是怎样对AST进行静态分析的
】获取文章的全部脑图资源。
关于我 & Node交流群
大家好,我是 Quixn,专注于 Node.js 技术栈分享,前端从 JavaScript 到 Node.js,再到后端数据库,优质文章推荐。如果你对 Node.js 学习感兴趣的话(后续有计划也可以),可以关注我,加我微信【 Quixn1314 】,拉你进交流群一起交流、学习、共建,或者关注我的公众号【 小Q全栈指南 】。Github 博客开源项目 github.com/Quixn...
欢迎加我微信【 Quixn1314 】,拉你 进 Node.js 高级进阶群,一起学Node,长期交流学习...