前言#
我们昨天分析完了parse的内容,今天我们来分析compile的内容。
坏蛋Dan:vue/compiler-core源码分析学习--day2: parse部分
本来不打算拆开来的。。。没想到字数上限了我还没分析完三分之二。。。。
所以这里拆分为两部分:
一、
- 主流程
- 主流程中遇到的一些函数
二、
nodeTransforms中注册的插件- 分析
genCode生成render function过程中的所有类型
今天我们来分析第一部分
入口参数#
由于这个compile方法贯穿三个包:compiler-sfc、compiler-dom以及今天分析的compiler-core三个包,所以这里需要把两外两处涉及到的地方都放出来,先把入口说明白了,到时候分析也能清晰点。
先说下这三者的关系,
-
compiler-dom包中的compile方法基于compiler-core包中baseCompile方法封装了一层 -
然后
compiler-sfc方法调用compiler-dom包中的compile方法,传入自己的参数。
baseCompile是我们要分析的入口,所以这里就不放出来了。
我们先来看下compiler-dom中调用这个方法传入的options。
在compiler-dom/src/index.ts文件中。
export const DOMNodeTransforms: NodeTransform[] = [
transformStyle,
...(__DEV__ ? [transformTransition] : [])
]
export const DOMDirectiveTransforms: Record<string, DirectiveTransform> = {
cloak: noopDirectiveTransform,
html: transformVHtml,
text: transformVText,
model: transformModel, // override compiler-core
on: transformOn, // override compiler-core
show: transformShow
}
export function compile(
template: string,
options: CompilerOptions = {}
): CodegenResult {
return baseCompile(
template,
extend({}, parserOptions, options, {
nodeTransforms: [
// ignore <script> and <tag>
// this is not put inside DOMNodeTransforms because that list is used
// by compiler-ssr to generate vnode fallback branches
ignoreSideEffectTags,
...DOMNodeTransforms,
...(options.nodeTransforms || [])
],
directiveTransforms: extend(
{},
DOMDirectiveTransforms,
options.directiveTransforms || {}
),
transformHoist: __BROWSER__ ? null : stringifyStatic
})
)
}可以看到compile-dom中对compiler-core/baseCompile方法做了一层封装,然后加入了一些加工函数,具体分析可以看之前的文章。
vue/compiler-dom源码分析学习--final:整理
然后我们来看下compiler-sfc里面的调用,在compiler-sfc/src/compileTemplate.ts文件中
if (isObject(transformAssetUrls)) {
const assetOptions = normalizeOptions(transformAssetUrls)
nodeTransforms = [
createAssetUrlTransformWithOptions(assetOptions),
createSrcsetTransformWithOptions(assetOptions)
]
} else if (transformAssetUrls !== false) {
nodeTransforms = [transformAssetUrl, transformSrcset]
}
let { code, ast, preamble, map } = compiler.compile(source, {
mode: 'module',
prefixIdentifiers: true,
hoistStatic: true,
cacheHandlers: true,
ssrCssVars:
ssr && ssrCssVars && ssrCssVars.length
? genCssVarsFromList(ssrCssVars, shortId, isProd, true)
: '',
scopeId: scoped ? longId : undefined,
slotted,
sourceMap: true,
...compilerOptions,
nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
filename,
onError: e => errors.push(e),
onWarn: w => warnings.push(w)
})传入一些参数。
对这一块分析感兴趣的可以看下之前的文章
我们来看下最终组合的参数

baseCompile#
在开始之前#
开始分析前,推荐下:Vue Template Explorer
可以直观的看到render function,可以辅助你阅读源码.
另外,这里最好记下哪个指令搭配哪个辅助函数,哪种节点搭配哪种辅助函数,这对后面我们学习runtime相关的包是很有帮助的。
补充:block的概念[1

这后面我们会遇到一些block的概念,避免不理解先放这里了
// we name it `baseCompile` so that higher order compilers like
// @vue/compiler-dom can export `compile` while re-exporting everything else.
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
const onError = options.onError || defaultOnError
const isModuleMode = options.mode === 'module'
// ...省略browser场景
const prefixIdentifiers =
!__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)
if (!prefixIdentifiers && options.cacheHandlers) {
onError(createCompilerError(ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED))
}
if (options.scopeId && !isModuleMode) {
onError(createCompilerError(ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED))
}
const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] =
getBaseTransformPreset(prefixIdentifiers)
if (!__BROWSER__ && options.isTS) {
const { expressionPlugins } = options
if (!expressionPlugins || !expressionPlugins.includes('typescript')) {
options.expressionPlugins = [...(expressionPlugins || []), 'typescript']
}
}
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}prefixIdentifiers:这玩意儿应该是用于cache的,目前暂时还不清楚是做什么用的。ast自然就是通过baseParse方法解析出来的template AST。从这里也可以看出来它俩的分工了,parse专门用来转化成ast,而compile具体做了什么我们来慢慢分析。getBaseTransformPreset:老规矩,这样不会有太多代码堆积在主流程分析上。
然后对typescript的场景做支持处理,如果检测到使用ts但是没有ts插件,这个时候就会自动加上。
接着调用transform方法,把ast传入里面。
最后调用generate方法将ast处理并返回。
注意这里的transform和generate不是babel的transform和generate,切忌不要搞混,这个用法太像了。。。
transform#
export function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
traverseNode(root, context)
if (options.hoistStatic) {
hoistStatic(root, context)
}
if (!options.ssr) {
createRootCodegen(root, context)
}
// finalize meta information
root.helpers = [...context.helpers.keys()]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = context.imports
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached
if (__COMPAT__) {
root.filters = [...context.filters!]
}
}createTransformContext:用来创建转换执行上下文的,具体代码看主流程transform中遇到的函数里面的分析traverseNode:同上,这里简单的说就是遍历节点,给每个节点都执行一遍nodeTranforms里面注册的回调,并执行这些回调返回的回调。hoistStatic:同上,这里简单的说就是查找能否静态提升的节点并提升,甚至是stringfy。createRootCodegen:同上,简单的说就是子节点们的codegenNode都已经好了,那么就轮到根节点了。。
最后就是将这些过程中存储的数据赋值给root,也就是compile.ts文件中传入的AST根节点。
generate#
export function generate(
ast: RootNode,
options: CodegenOptions & {
onContextCreated?: (context: CodegenContext) => void
} = {}
): CodegenResult {
const context = createCodegenContext(ast, options)
if (options.onContextCreated) options.onContextCreated(context)
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'
const isSetupInlined = !__BROWSER__ && !!options.inline
// preambles
// in setup() inline mode, the preamble is generated in a sub context
// and returned separately.
const preambleContext = isSetupInlined
? createCodegenContext(ast, options)
: context
if (!__BROWSER__ && mode === 'module') {
genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined)
} else {
genFunctionPreamble(ast, preambleContext)
}
// enter render function
const functionName = ssr ? `ssrRender` : `render`
const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache']
if (!__BROWSER__ && options.bindingMetadata && !options.inline) {
// binding optimization args
args.push('$props', '$setup', '$data', '$options')
}
const signature =
!__BROWSER__ && options.isTS
? args.map(arg => `${arg}: any`).join(',')
: args.join(', ')
if (isSetupInlined) {
push(`(${signature}) => {`)
} else {
push(`function ${functionName}(${signature}) {`)
}
indent()
if (useWithBlock) {
push(`with (_ctx) {`)
indent()
// function mode const declarations should be inside with block
// also they should be renamed to avoid collision with user properties
if (hasHelpers) {
push(`const { ${ast.helpers.map(aliasHelper).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 (__COMPAT__ && ast.filters && ast.filters.length) {
newline()
genAssets(ast.filters, 'filter', context)
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) {
genNode(ast.codegenNode, context)
} else {
push(`null`)
}
if (useWithBlock) {
deindent()
push(`}`)
}
deindent()
push(`}`)
return {
ast,
code: context.code,
preamble: isSetupInlined ? preambleContext.code : ``,
// SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined
}
}generate顾名思义就是将之前的codeGen节点转换为render function。
createCodegenContext:生成code generate执行上下文,代码就不看了,里面有些方法我们遇到了再细嗦。

onContextCreated:创建上下文时需要执行的回调,这里就不说了,我们并没有。preamble:这个词翻译过来叫“前言”,在这里应该是在做准备,同样的,涉及到的两个函数的代码分析放到下面genModulePreamble:这个方法简单的说就是import和生成hoists的const常量,具体分析请往下看。genFunctionPreamble:这个函数我们就不看了,我们这里没有涉及到,感兴趣的大佬可自行查看。genAssets:老规矩,简单的说就是转换成一个辅助函数,runtime时引入处理component/filter/directive。filter和directive的我们就不看了,不过注意这个directive是自定义directive[2 。ast.temps:这个是临时变量,我们这里没遇到,不得已需要缓存到全局deindent:自然就是换行 + 减少缩进。genNode:老规矩,简单的说就是给每一个node都创建runtime函数,然后一个套一个。
总结#
那么compile的就都说完了。
流程比较清晰,你可以参考babel的core来了解:
transform: 里面对每个节点都做了plugin里的操作,然后对部分节点hoistStatic以及staticStringify化,然后给每个节点都创建一个codegenNode字段,它们将被用于generate中转换成render function。generate: 顾名思义,就是生成render function,通过transform加工(这里说加工是因为解析是在parse阶段)之后拿到的codegenNode通过runtime辅助函数包裹转换成可被runtime直接使用 的function字符串。
最后来看下数据
源码:
123asdads
<template>
<div class="wrapper" v-for="item in 10">
{{item}}
<template>
<div slot-scope="A, B">
123
</div>
</template>
<div class="static_wrapper">
<Add>
<div class="slot" slot="test">
<div class="a">aa</div>
<div class="b">bb</div>
<div class="c">cc</div>
</div>
</Add>
<div class="oo">
<div class="c" v-html="'<p>123</p>'"></div>
<div class="d" style="color: red; background-color: blue;">dd</div>
<div class="d">
dd
<div class="ff">
ff
<div class="h">hhh</div>
</div>
</div>
<div class="d">dd</div>
<div class="d">dd</div>
<div class="d">dd</div>
<div class="d">dd</div>
<div class="d">dd</div>
</div>
</div>
<!-- dynamic event -->
<button v-on:[event].once="a['aa']"></button>
<!-- method handler -->
<button
@[`${test}`]="
a = true
b
"
@vue:test="test"
v-on:click="doThis"
></button>
<!-- inline statement -->
<button v-on:click="doThat('hello', $event)"></button>
<!-- shorthand -->
<button @click="a ? b : c"></button>
<!-- shorthand dynamic event -->
<button @[event]="doThis"></button>
<!-- stop propagation -->
<button @click.stop="doThis"></button>
<!-- prevent default -->
<button @click.prevent="doThis"></button>
<!-- prevent default without expression -->
<form @submit.prevent></form>
<!-- chain modifiers -->
<button @click.stop.prevent="doThis"></button>
<!-- key modifier using keyAlias -->
<input @keyup.enter="onEnter" />
<!-- the click event will be triggered at most once -->
<button v-on:click.once="doThis"></button>
<!-- object syntax -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
<button @click="show = !show">Toggle</button>
<Transition>
<p v-if="show">hello</p>
</Transition>
<Add
@handleAdd="handleAdd"
:team-max-num="teamMaxNum"
/>
<input v-bind="{ type: 'checkbox' }" name="" id="" v-model="checkbox" />
<input type="text" name="name" id="" v-model="model" />
<p v-text="text"></p>
<p v-text="'text'"></p>
<p v-html="html"></p>
<div class="team_name_and_logo" v-cloak>
<h1 style="border: 1px solid #000" :style="cStyle">
we are {{ teamName }}!
</h1>
<img class="img" src="./a.jpg" alt="" srcset="./a.jpg 1x, ./b.jpg 2x" />
</div>
<Add @handleAdd="handleAdd" :team-max-num="teamMaxNum" />
<div v-for="item in list" :key="item.id">
<p>hi! i am {{ item.name }},</p>
<p>i am {{ item.age }} age old,</p>
<p>my job is {{ item.job }},</p>
<p @click="handleChangeColor">i am glad to met you!</p>
<p v-if="item.gender === 'man'">Do you like van♂ you xi?</p>
<p v-else>You don't love me anymore</p>
</div>
<div class="total">total: {{ totalNum }}</div>
<div class="max">the upper limit is {{ teamMaxNum }}</div>
</div>
</template>render function(mode == module):
import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode, createTextVNode as _createTextVNode, createCommentVNode as _createCommentVNode, toHandlerKey as _toHandlerKey, mergeProps as _mergeProps, withModifiers as _withModifiers, withKeys as _withKeys, toHandlers as _toHandlers, Transition as _Transition, vModelDynamic as _vModelDynamic, withDirectives as _withDirectives, vModelText as _vModelText, normalizeStyle as _normalizeStyle, createStaticVNode as _createStaticVNode } from "vue"
const _hoisted_1 = { class: "wrapper" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("template", null, [
/*#__PURE__*/_createElementVNode("div", { "slot-scope": "A, B" }, " 123 ")
], -1 /* HOISTED */)
const _hoisted_3 = { class: "static_wrapper" }
const _hoisted_4 = /*#__PURE__*/_createElementVNode("div", {
class: "slot",
slot: "test"
}, [
/*#__PURE__*/_createElementVNode("div", { class: "a" }, "aa"),
/*#__PURE__*/_createElementVNode("div", { class: "b" }, "bb"),
/*#__PURE__*/_createElementVNode("div", { class: "c" }, "cc")
], -1 /* HOISTED */)
const _hoisted_5 = /*#__PURE__*/_createStaticVNode("<div class=\"oo\"><div class=\"c\"><p>123</p></div><div class=\"d\" style=\"color:red;background-color:blue;\">dd</div><div class=\"d\"> dd <div class=\"ff\"> ff <div class=\"h\">hhh</div></div></div><div class=\"d\">dd</div><div class=\"d\">dd</div><div class=\"d\">dd</div><div class=\"d\">dd</div><div class=\"d\">dd</div></div>", 1)
const _hoisted_6 = ["onVnodeTest", "onClick"]
const _hoisted_7 = ["onClick"]
const _hoisted_8 = ["onClick"]
const _hoisted_9 = ["onClick"]
const _hoisted_10 = ["onClick"]
const _hoisted_11 = ["onSubmit"]
const _hoisted_12 = ["onClick"]
const _hoisted_13 = ["onKeyup"]
const _hoisted_14 = ["onClickOnce"]
const _hoisted_15 = ["onClick"]
const _hoisted_16 = { key: 0 }
const _hoisted_17 = ["onUpdate:modelValue"]
const _hoisted_18 = ["onUpdate:modelValue"]
const _hoisted_19 = ["textContent"]
const _hoisted_20 = /*#__PURE__*/_createElementVNode("p", { textContent: 'text' }, null, -1 /* HOISTED */)
const _hoisted_21 = ["innerHTML"]
const _hoisted_22 = { class: "team_name_and_logo" }
const _hoisted_23 = /*#__PURE__*/_createElementVNode("img", {
class: "img",
src: "./a.jpg",
alt: "",
srcset: "./a.jpg 1x, ./b.jpg 2x"
}, null, -1 /* HOISTED */)
const _hoisted_24 = ["onClick"]
const _hoisted_25 = { key: 0 }
const _hoisted_26 = { key: 1 }
const _hoisted_27 = { class: "total" }
const _hoisted_28 = { class: "max" }
export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_Add = _resolveComponent("Add")
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createTextVNode("123asdads "),
_createElementVNode("template", null, [
(_openBlock(), _createElementBlock(_Fragment, null, _renderList(10, (item) => {
return _createElementVNode("div", _hoisted_1, [
_createTextVNode(_toDisplayString(item) + " ", 1 /* TEXT */),
_hoisted_2,
_createElementVNode("div", _hoisted_3, [
_createVNode(_component_Add, null, {
default: _withCtx(() => [
_hoisted_4
], undefined, true),
_: 1 /* STABLE */
}),
_hoisted_5
]),
_createCommentVNode(" dynamic event "),
_createElementVNode("button", {
[(_toHandlerKey(_ctx.event)) + "Once"]: _ctx.a['aa']
}, null, 16 /* FULL_PROPS */),
_createCommentVNode(" method handler "),
_createElementVNode("button", _mergeProps({
[_toHandlerKey(`${_ctx.test}`)]: $event => (
a = true
b
)
}, {
onVnodeTest: _ctx.test,
onClick: _ctx.doThis
}), null, 16 /* FULL_PROPS */, _hoisted_6),
_createCommentVNode(" inline statement "),
_createElementVNode("button", {
onClick: $event => (_ctx.doThat('hello', $event))
}, null, 8 /* PROPS */, _hoisted_7),
_createCommentVNode(" shorthand "),
_createElementVNode("button", {
onClick: $event => (_ctx.a ? _ctx.b : _ctx.c)
}, null, 8 /* PROPS */, _hoisted_8),
_createCommentVNode(" shorthand dynamic event "),
_createElementVNode("button", { [_toHandlerKey(_ctx.event)]: _ctx.doThis }, null, 16 /* FULL_PROPS */),
_createCommentVNode(" stop propagation "),
_createElementVNode("button", {
onClick: _withModifiers(_ctx.doThis, ["stop"])
}, null, 8 /* PROPS */, _hoisted_9),
_createCommentVNode(" prevent default "),
_createElementVNode("button", {
onClick: _withModifiers(_ctx.doThis, ["prevent"])
}, null, 8 /* PROPS */, _hoisted_10),
_createCommentVNode(" prevent default without expression "),
_createElementVNode("form", {
onSubmit: _withModifiers(() => {}, ["prevent"])
}, null, 40 /* PROPS, HYDRATE_EVENTS */, _hoisted_11),
_createCommentVNode(" chain modifiers "),
_createElementVNode("button", {
onClick: _withModifiers(_ctx.doThis, ["stop","prevent"])
}, null, 8 /* PROPS */, _hoisted_12),
_createCommentVNode(" key modifier using keyAlias "),
_createElementVNode("input", {
onKeyup: _withKeys(_ctx.onEnter, ["enter"])
}, null, 40 /* PROPS, HYDRATE_EVENTS */, _hoisted_13),
_createCommentVNode(" the click event will be triggered at most once "),
_createElementVNode("button", { onClickOnce: _ctx.doThis }, null, 40 /* PROPS, HYDRATE_EVENTS */, _hoisted_14),
_createCommentVNode(" object syntax "),
_createElementVNode("button", _toHandlers({ mousedown: _ctx.doThis, mouseup: _ctx.doThat }, true), null, 16 /* FULL_PROPS */),
_createElementVNode("button", {
onClick: $event => (_ctx.show = !_ctx.show)
}, "Toggle", 8 /* PROPS */, _hoisted_15),
_createVNode(_Transition, null, {
default: _withCtx(() => [
(_ctx.show)
? (_openBlock(), _createElementBlock("p", _hoisted_16, "hello"))
: _createCommentVNode("v-if", true)
], undefined, true),
_: 1 /* STABLE */
}),
_createVNode(_component_Add, {
onHandleAdd: _ctx.handleAdd,
"team-max-num": _ctx.teamMaxNum
}, null, 8 /* PROPS */, ["onHandleAdd", "team-max-num"]),
_withDirectives(_createElementVNode("input", _mergeProps({ type: 'checkbox' }, {
name: "",
id: "",
"onUpdate:modelValue": $event => ((_ctx.checkbox) = $event)
}), null, 16 /* FULL_PROPS */, _hoisted_17), [
[_vModelDynamic, _ctx.checkbox]
]),
_withDirectives(_createElementVNode("input", {
type: "text",
name: "name",
id: "",
"onUpdate:modelValue": $event => ((_ctx.model) = $event)
}, null, 8 /* PROPS */, _hoisted_18), [
[_vModelText, _ctx.model]
]),
_createElementVNode("p", {
textContent: _toDisplayString(_ctx.text)
}, null, 8 /* PROPS */, _hoisted_19),
_hoisted_20,
_createElementVNode("p", { innerHTML: _ctx.html }, null, 8 /* PROPS */, _hoisted_21),
_createElementVNode("div", _hoisted_22, [
_createElementVNode("h1", {
style: _normalizeStyle([{"border":"1px solid #000"}, _ctx.cStyle])
}, " we are " + _toDisplayString(_ctx.teamName) + "! ", 5 /* TEXT, STYLE */),
_hoisted_23
]),
_createVNode(_component_Add, {
onHandleAdd: _ctx.handleAdd,
"team-max-num": _ctx.teamMaxNum
}, null, 8 /* PROPS */, ["onHandleAdd", "team-max-num"]),
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, (item) => {
return (_openBlock(), _createElementBlock("div", {
key: item.id
}, [
_createElementVNode("p", null, "hi! i am " + _toDisplayString(item.name) + ",", 1 /* TEXT */),
_createElementVNode("p", null, "i am " + _toDisplayString(item.age) + " age old,", 1 /* TEXT */),
_createElementVNode("p", null, "my job is " + _toDisplayString(item.job) + ",", 1 /* TEXT */),
_createElementVNode("p", { onClick: _ctx.handleChangeColor }, "i am glad to met you!", 8 /* PROPS */, _hoisted_24),
(item.gender === 'man')
? (_openBlock(), _createElementBlock("p", _hoisted_25, "Do you like van♂ you xi?"))
: (_openBlock(), _createElementBlock("p", _hoisted_26, "You don't love me anymore"))
]))
}), 128 /* KEYED_FRAGMENT */)),
_createElementVNode("div", _hoisted_27, "total: " + _toDisplayString(_ctx.totalNum), 1 /* TEXT */),
_createElementVNode("div", _hoisted_28, "the upper limit is " + _toDisplayString(_ctx.teamMaxNum), 1 /* TEXT */)
])
}), 64 /* STABLE_FRAGMENT */))
])
], 64 /* STABLE_FRAGMENT */))
}
// Check the console for the AST可读性极差~,不过线上要啥可读性呢~
补充#
- 你可以
mark下每个节点类型对应什么样子的源码,这对于后面学习runtime来说是很有帮助的,我这里没有罗列出来,后面有空再说。不过你也可以边学runtime包边用开头推荐的页面看下每种源码对应的节点类型,也是一个不错的选择 - 关于节点类型这里还有些要说的,
NodeTypes实际上包含的节点类型可以大致分类两类,一类是AST的节点类型,另一类是AST的codegenNode的节点类型,所以codegenNode的类型只会出现在traverseNode之后。 - 为了方便理解,这里画了张图

主流程transform中遇到的函数#
getBaseTransformPreset#
export function getBaseTransformPreset(
prefixIdentifiers?: boolean
): TransformPreset {
return [
[
transformOnce,
transformIf,
transformMemo,
transformFor,
...(__COMPAT__ ? [transformFilter] : []),
...(!__BROWSER__ && prefixIdentifiers
? [
// order is important
trackVForSlotScopes,
transformExpression
]
: __BROWSER__ && __DEV__
? [transformExpression]
: []),
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
{
on: transformOn,
bind: transformBind,
model: transformModel
}
]
}这个方法没什么好说的,就是预先整合需要用于转换的辅助函数(这里的辅助不是runtime的辅助)。
至于这里面的辅助函数我们先不在这里分析,需要用到的时候单独给它们开个小标题分析。
createTransformContext#
export function createTransformContext(
root: RootNode,
{
filename = '',
prefixIdentifiers = false,
hoistStatic = false,
cacheHandlers = false,
nodeTransforms = [],
directiveTransforms = {},
transformHoist = null,
isBuiltInComponent = NOOP,
isCustomElement = NOOP,
expressionPlugins = [],
scopeId = null,
slotted = true,
ssr = false,
inSSR = false,
ssrCssVars = ``,
bindingMetadata = EMPTY_OBJ,
inline = false,
isTS = false,
onError = defaultOnError,
onWarn = defaultOnWarn,
compatConfig
}: TransformOptions
): TransformContext {
const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/)
const context: TransformContext = {
// options
selfName: nameMatch && capitalize(camelize(nameMatch[1])),
prefixIdentifiers,
hoistStatic,
cacheHandlers,
nodeTransforms,
directiveTransforms,
transformHoist,
isBuiltInComponent,
isCustomElement,
expressionPlugins,
scopeId,
slotted,
ssr,
inSSR,
ssrCssVars,
bindingMetadata,
inline,
isTS,
onError,
onWarn,
compatConfig,
// state
root,
helpers: new Map(),
components: new Set(),
directives: new Set(),
hoists: [],
imports: [],
constantCache: new Map(),
temps: 0,
cached: 0,
identifiers: Object.create(null),
scopes: {
vFor: 0,
vSlot: 0,
vPre: 0,
vOnce: 0
},
parent: null,
currentNode: root,
childIndex: 0,
inVOnce: false,
// methods
helper(name) {
const count = context.helpers.get(name) || 0
context.helpers.set(name, count + 1)
return name
},
removeHelper(name) {
const count = context.helpers.get(name)
if (count) {
const currentCount = count - 1
if (!currentCount) {
context.helpers.delete(name)
} else {
context.helpers.set(name, currentCount)
}
}
},
helperString(name) {
return `_${helperNameMap[context.helper(name)]}`
},
replaceNode(node) {
/* istanbul ignore if */
if (__DEV__) {
if (!context.currentNode) {
throw new Error(`Node being replaced is already removed.`)
}
if (!context.parent) {
throw new Error(`Cannot replace root node.`)
}
}
context.parent!.children[context.childIndex] = context.currentNode = node
},
removeNode(node) {
if (__DEV__ && !context.parent) {
throw new Error(`Cannot remove root node.`)
}
const list = context.parent!.children
const removalIndex = node
? list.indexOf(node)
: context.currentNode
? context.childIndex
: -1
/* istanbul ignore if */
if (__DEV__ && removalIndex < 0) {
throw new Error(`node being removed is not a child of current parent`)
}
if (!node || node === context.currentNode) {
// current node removed
context.currentNode = null
context.onNodeRemoved()
} else {
// sibling node removed
if (context.childIndex > removalIndex) {
context.childIndex--
context.onNodeRemoved()
}
}
context.parent!.children.splice(removalIndex, 1)
},
onNodeRemoved: () => {},
addIdentifiers(exp) {
// identifier tracking only happens in non-browser builds.
if (!__BROWSER__) {
if (isString(exp)) {
addId(exp)
} else if (exp.identifiers) {
exp.identifiers.forEach(addId)
} else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
addId(exp.content)
}
}
},
removeIdentifiers(exp) {
if (!__BROWSER__) {
if (isString(exp)) {
removeId(exp)
} else if (exp.identifiers) {
exp.identifiers.forEach(removeId)
} else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
removeId(exp.content)
}
}
},
hoist(exp) {
if (isString(exp)) exp = createSimpleExpression(exp)
context.hoists.push(exp)
const identifier = createSimpleExpression(
`_hoisted_${context.hoists.length}`,
false,
exp.loc,
ConstantTypes.CAN_HOIST
)
identifier.hoisted = exp
return identifier
},
cache(exp, isVNode = false) {
return createCacheExpression(context.cached++, exp, isVNode)
}
}
if (__COMPAT__) {
context.filters = new Set()
}
function addId(id: string) {
const { identifiers } = context
if (identifiers[id] === undefined) {
identifiers[id] = 0
}
identifiers[id]!++
}
function removeId(id: string) {
context.identifiers[id]!--
}
return context
}量有些大,我们来说下方法即可。
helper:用来存储辅助函数的名字,这里的辅助函数就是runtime的辅助函数removeHelper:自然就是移除对应的辅助函数,不过由于这个执行上下文是全局唯一的,所以同名的辅助函数很大概率是存在多个的,所以这里为了确保辅助函数的存取准确性,用了个数来计算,当没有辅助函数之后就移除这个辅助函数,否则减一。helperString:将辅助函数stringfy化,这样做是为了runtime可以直接使用。replaceNode:用来替换node节点的。removeNode:移除节点,将这个节点赋值为null,然后调用上下文的onNodeRemoved方法。然后将它移除。onNodeRemoved:目前是初始化状态,没有可执行代码addIdentifiers:新增一个节点,调用addId这个方法。removeIdentifiers:自然就是移除节点。hoist:将节点标记为CAN_HOIST。到时候会被静态提升。cache:缓存处理。
这些应该是提供给插件们使用的方法,插件有可能将当前节点删除或者新插入一个节点等操作,所以这里提供了这些方法。依旧是和babel类似。
具体内容到时具体地方使用到会细嗦。
traverseNode#
export function traverseNode(
node: RootNode | TemplateChildNode,
context: TransformContext
) {
context.currentNode = node
// apply transform plugins
const { nodeTransforms } = context
const exitFns = []
for (let i = 0; i < nodeTransforms.length; i++) {
const onExit = nodeTransforms[i](node, context)
if (onExit) {
if (isArray(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:
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
}
// exit transforms
context.currentNode = node
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}其实看到这里大概能猜到整体是怎么样的了,应该是惨遭babel的core[3包的架构来写的,包括这里traversed以及插件的注册。扯远了。
这里开始处理之前注册了的nodeTransforms插件,插件们是啥,做了什么这里先不分析,等会会单独开个一级标题分析它们。
这一块代码很简单,调用nodeTransforms注册的回调们,然后将它们执行,注意,每一个节点都会调用所有这些回调。
如果当前节点在这些回调的其中一个中被remove了,那么直接就return处理。
如果没有,那么再重新赋值一遍context.currentNode,确保这个node没有替换掉。
然后就是针对不同的节点类型进行处理:
COMMENT:也就是注释节点,对应的辅助函数是createCommentVNodeINTERPOLATION:mustache也就是双大括号语法,昨天parse部分分析过的,这里就不多说了,对应的辅助函数是toDisplayStringIF:会将每一个分支都递归调用traverseNode方法来处理子节点IF_BRANCH:同下FOR:同下ELEMENT:同下ROOT:调用traverseChildren这个方法.
export 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
context.childIndex = i
context.onNodeRemoved = nodeRemoved
traverseNode(child, context)
}
}注意这里的context.onNodeRemoved,上面我们初始化的时候是一个空函数,而这里被替换成i--的函数,所以context上的一些数据是会在执行的过程中被替换的,需要注意。
结束遍历后,将node赋值给context.currentNode,这里为啥又要重新将context.currentNode赋值为node呢?我们暂时不清楚是为啥,不过可以猜测,由于currentNode是全局的,所以这里应该会在遍历/递归的过程中发生变化,所以遍历/递归结束之后再替换回当前的节点,避免节点不对导致后续一些列问题。
如果这些注册的回调有返回回调,那么顺便帮它们执行了,注意,这里是i--来执行的,也就是说是逆序,这里类似调用栈,后进先出。
hoistStatic#
在分析前,我们先需要知道静态提升是啥东西,我之前分析compiler-dom的时候有说到过,感兴趣的可以去看下,这里就不再解释了。
vue/compiler-dom源码分析学习--day4: 字符串化hoist节点
另外官网也有相应的解释:
Rendering Mechanism | Vue.js (vuejs.org)
export function hoistStatic(root: RootNode, context: TransformContext) {
walk(
root,
context,
// Root node is unfortunately non-hoistable due to potential parent
// fallthrough attributes.
isSingleElementRoot(root, root.children[0])
)
}isSingleElementRoot方法就不看了,就是判断你这个文件中是否存在两个根节点。walk:来看下代码
function walk(
node: ParentNode,
context: TransformContext,
doNotHoistNode: boolean = false
) {
const { children } = node
const originalCount = children.length
let hoistedCount = 0
for (let i = 0; i < children.length; i++) {
const child = children[i]
// only plain elements & text calls are eligible for hoisting.
if (
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT
) {
const constantType = doNotHoistNode
? ConstantTypes.NOT_CONSTANT
: getConstantType(child, context)
if (constantType > ConstantTypes.NOT_CONSTANT) {
if (constantType >= ConstantTypes.CAN_HOIST) {
;(child.codegenNode as VNodeCall).patchFlag =
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
child.codegenNode = context.hoist(child.codegenNode!)
hoistedCount++
continue
}
} else {
// node may contain dynamic children, but its props may be eligible for
// hoisting.
const codegenNode = child.codegenNode!
if (codegenNode.type === NodeTypes.VNODE_CALL) {
const flag = getPatchFlag(codegenNode)
if (
(!flag ||
flag === PatchFlags.NEED_PATCH ||
flag === PatchFlags.TEXT) &&
getGeneratedPropsConstantType(child, context) >=
ConstantTypes.CAN_HOIST
) {
const props = getNodeProps(child)
if (props) {
codegenNode.props = context.hoist(props)
}
}
if (codegenNode.dynamicProps) {
codegenNode.dynamicProps = context.hoist(codegenNode.dynamicProps)
}
}
}
}
// walk further
if (child.type === NodeTypes.ELEMENT) {
const isComponent = child.tagType === ElementTypes.COMPONENT
if (isComponent) {
context.scopes.vSlot++
}
walk(child, context)
if (isComponent) {
context.scopes.vSlot--
}
} else if (child.type === NodeTypes.FOR) {
// Do not hoist v-for single child because it has to be a block
walk(child, context, child.children.length === 1)
} else if (child.type === NodeTypes.IF) {
for (let i = 0; i < child.branches.length; i++) {
// Do not hoist v-if single child because it has to be a block
walk(
child.branches[i],
context,
child.branches[i].children.length === 1
)
}
}
}
if (hoistedCount && context.transformHoist) {
context.transformHoist(children, context, node)
}
// all children were hoisted - the entire children array is hoistable.
if (
hoistedCount &&
hoistedCount === originalCount &&
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT &&
node.codegenNode &&
node.codegenNode.type === NodeTypes.VNODE_CALL &&
isArray(node.codegenNode.children)
) {
node.codegenNode.children = context.hoist(
createArrayExpression(node.codegenNode.children)
)
}
} 代码较长,我们一点一点的分析。
其实我们之前分析compiler-dom这个包中的staticStringfy的时候也是遇到过了一个walk,但实际上俩walk并没有关系。
开头一句注释:只有文本和纯元素可以静态提升。
getConstantType:这个方法这里就不分析了,放到下面的章节中。简单的说就是在判断节点是否可以静态提升。ConstantTypes是一个枚举,有四种变体,这个之前其实有说过,不过这里再说一次加深印象。
/**
* Static types have several levels.
* Higher levels implies lower levels. e.g. a node that can be stringified
* can always be hoisted and skipped for patch.
*/
export const enum ConstantTypes {
NOT_CONSTANT = 0,
CAN_SKIP_PATCH,
CAN_HOIST,
CAN_STRINGIFY
}其中变体等级越高越安静稳定。那么是用在什么时候的呢?用在patch也就是打补丁的时候,更准确一点,是diff阶段。
NOT_CONSTANT:一定要处理,不能跳过,也不能静态提升,更不能字符串化。CAN_SKIP_PATCH:打补丁的时候可以绕过。CAN_HOIST: 可以静态提升。CAN_STRINGFY:可以字符串化。
而patchFlags[4 , 是用来标记当前节点的打补丁类型,由于这个类型的唯一性,在打补丁阶段可以快速定位并且做最少的操作。
可以简单的看下
export const enum PatchFlags {
/**
* Indicates an element with dynamic textContent (children fast path)
*/
TEXT = 1,
/**
* Indicates an element with dynamic class binding.
*/
CLASS = 1 << 1,
/**
* Indicates an element with dynamic style
* The compiler pre-compiles static string styles into static objects
* + detects and hoists inline static objects
* e.g. `style="color: red"` and `:style="{ color: 'red' }"` both get hoisted
* as:
* ```js
* const style = { color: 'red' }
* render() { return e('div', { style }) }
* ```
*/
STYLE = 1 << 2,
/**
* Indicates an element that has non-class/style dynamic props.
* Can also be on a component that has any dynamic props (includes
* class/style). when this flag is present, the vnode also has a dynamicProps
* array that contains the keys of the props that may change so the runtime
* can diff them faster (without having to worry about removed props)
*/
PROPS = 1 << 3,
/**
* Indicates an element with props with dynamic keys. When keys change, a full
* diff is always needed to remove the old key. This flag is mutually
* exclusive with CLASS, STYLE and PROPS.
*/
FULL_PROPS = 1 << 4,
/**
* Indicates an element with event listeners (which need to be attached
* during hydration)
*/
HYDRATE_EVENTS = 1 << 5,
/**
* Indicates a fragment whose children order doesn't change.
*/
STABLE_FRAGMENT = 1 << 6,
/**
* Indicates a fragment with keyed or partially keyed children
*/
KEYED_FRAGMENT = 1 << 7,
/**
* Indicates a fragment with unkeyed children.
*/
UNKEYED_FRAGMENT = 1 << 8,
/**
* Indicates an element that only needs non-props patching, e.g. ref or
* directives (onVnodeXXX hooks). since every patched vnode checks for refs
* and onVnodeXXX hooks, it simply marks the vnode so that a parent block
* will track it.
*/
NEED_PATCH = 1 << 9,
/**
* Indicates a component with dynamic slots (e.g. slot that references a v-for
* iterated value, or dynamic slot names).
* Components with this flag are always force updated.
*/
DYNAMIC_SLOTS = 1 << 10,
/**
* Indicates a fragment that was created only because the user has placed
* comments at the root level of a template. This is a dev-only flag since
* comments are stripped in production.
*/
DEV_ROOT_FRAGMENT = 1 << 11,
/**
* SPECIAL FLAGS -------------------------------------------------------------
* Special flags are negative integers. They are never matched against using
* bitwise operators (bitwise matching should only happen in branches where
* patchFlag > 0), and are mutually exclusive. When checking for a special
* flag, simply check patchFlag === FLAG.
*/
/**
* Indicates a hoisted static vnode. This is a hint for hydration to skip
* the entire sub tree since static content never needs to be updated.
*/
HOISTED = -1,
/**
* A special flag that indicates that the diffing algorithm should bail out
* of optimized mode. For example, on block fragments created by renderSlot()
* when encountering non-compiler generated slots (i.e. manually written
* render functions, which should always be fully diffed)
* OR manually cloneVNodes
*/
BAIL = -2
}这里面还使用了位运算符。你可以看下注释里的介绍。
这里还用到了hoist这个方法,由于代码上面createTransformContext方法中有贴出来了,所以这里只是细讲一下,忘了代码的可以回看。
hoist
hoist这个方法中,如果表达式是一段字符串(在hoistStatic中确实是一个字符串),那就先处理成一个单表达式节点,然后将它存入context.hoists中。
接着再根据这个节点在context.hoists这个数组中的下标位置创建一个新的单表达式节点,content自然就是_hoisted_${index},注意这里isStatic是false,有点没搞懂,先mark下来。
然后将旧的单节点表达式作为新的单节点表达式中的hoisted字段的值。
最后返回。
然后回到我们的walk方法中。
getPatchFlag: 代码就不看了,就是在获取这个节点的patchFlag。getGeneratedPropsConstantType:这个方法我也放到下面开个小标题,这里简单的说就是判断这个节点的属性们是否都可以hoist/stringify,感觉都没必要把代码贴出来了。。。。。dynamicProps:这个我们又不认识。。mark下。scopes.vSlot:有几种情况会生成scope,比如slot/for等。transformHoist:这个就是compiler-dom的staticStringify,之前分析过了,这里就不多说了。
那么来总结下这个方法:简单的说这个方法就是在查找哪些节点(包括属性节点)可以hoist/stringify,然后将它们hoist/stringify处理, 注意这里改动都是改动node.codegenNode而不是node本身了,因为这个阶段是在traverseNode之后。
这里再说下三种特殊场景需要递归的:
- 节点是一个
component,这个时候自然需要递归处理这个节点的子组件并且可以确定这些都是slot的内容 v-for,除了只有一个子节点的情况,其它都是得递归继续判断的。v-if,同上。
另外如果这个节点的所有子节点都可以hoist甚至stringify,那这个节点自身自然可以hoist/stringify。


这里你应该有个疑问,那就是第一个图为什么只是hoist,而不是stringify。
其实这个点在分析staticStringify的时候说过,由于这么做的性能体现是需要超出多少个节点的时候才能体现出来的,所以少于某个阈值的时候是不会stringify的。
我们来改下第一张图里的源码,当节点变多了之后就变成stringify了。

补充:Rendering Mechanism | Vue.js (vuejs.org)

有标记的节点才会被跟踪,这样打补丁的时候就能省下很多时间,前面有的地方应该说错了。。。
createRootCodegen#
function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper } = context
const { children } = root
if (children.length === 1) {
const child = children[0]
// if the single child is an element, turn it into a block.
if (isSingleElementRoot(root, child) && child.codegenNode) {
// single element root is never hoisted so codegenNode will never be
// SimpleExpressionNode
const codegenNode = child.codegenNode
if (codegenNode.type === NodeTypes.VNODE_CALL) {
makeBlock(codegenNode, context)
}
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) {
// root has multiple nodes - return a fragment block.
let patchFlag = PatchFlags.STABLE_FRAGMENT
let patchFlagText = PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
// check if the fragment actually contains a single valid child with
// the rest being comments
if (
__DEV__ &&
children.filter(c => c.type !== NodeTypes.COMMENT).length === 1
) {
patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT
patchFlagText += `, ${PatchFlagNames[PatchFlags.DEV_ROOT_FRAGMENT]}`
}
root.codegenNode = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
root.children,
patchFlag + (__DEV__ ? ` /* ${patchFlagText} */` : ``),
undefined,
undefined,
true,
undefined,
false /* isComponent */
)
} else {
// no children = noop. codegen will return null.
}
}子节点们的codegenNode都已经好了,那么就轮到根节点了。
这里有两种情况:
- 多根节点:
vue3.x支持的新写法,允许template中存在多个根节点。多根节点使用Fragement,当然,它也是一个block,block的概念和作用我们开篇就讲了,这里就不多说了。 - 单根节点:单根节点则是创建一个
block,如果根节点本身就是个block,比如v-for/v-if/节点,它们自身就是block了,所以这里会把根节点的codegenNode替换成子节点而不是创建一个block makeBlock这个方法就不看了,就是移除createElementVNode/createVNode的辅助函数,增加openBlock和createBlock/createElementBlock这俩辅助函数createVNodeCall:这个方法也不看了,就是创建一个VNODE_CALL的节点


getGeneratedPropsConstantType#
function getGeneratedPropsConstantType(
node: PlainElementNode,
context: TransformContext
): ConstantTypes {
let returnType = ConstantTypes.CAN_STRINGIFY
const props = getNodeProps(node)
if (props && props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
const { properties } = props
for (let i = 0; i < properties.length; i++) {
const { key, value } = properties[i]
const keyType = getConstantType(key, context)
if (keyType === ConstantTypes.NOT_CONSTANT) {
return keyType
}
if (keyType < returnType) {
returnType = keyType
}
let valueType: ConstantTypes
if (value.type === NodeTypes.SIMPLE_EXPRESSION) {
valueType = getConstantType(value, context)
} else if (value.type === NodeTypes.JS_CALL_EXPRESSION) {
// some helper calls can be hoisted,
// such as the `normalizeProps` generated by the compiler for pre-normalize class,
// in this case we need to respect the ConstantType of the helper's arguments
valueType = getConstantTypeOfHelperCall(value, context)
} else {
valueType = ConstantTypes.NOT_CONSTANT
}
if (valueType === ConstantTypes.NOT_CONSTANT) {
return valueType
}
if (valueType < returnType) {
returnType = valueType
}
}
}
return returnType
}这个函数看名字应该是用来判断属性是否可以hoist或者stringify的。
注意这里的props,它们已经是被traverseNode里的nodeTransforms里注册的插件们加工过的了。
JS_OBJECT_EXPRESSION:是codegen之后的类型,先mark下,后面分析插件的时候应该会遇到。key:属性的名字value:属性的值getConstantType:这个下面会说, 这里简单的说就是判断这个节点的每一处地方是否都可以hoist/stringify。getConstantTypeOfHelperCall:这个函数就不看代码了,获取辅助函数调用的类型,有些是可以被hoist的。
这个方法挺简单的,就是判断这个节点的属性和属性的值是否可以被hoist或者stringify。两者取等级最低的那个。
getConstantType#
export function getConstantType(
node: TemplateChildNode | SimpleExpressionNode,
context: TransformContext
): ConstantTypes {
const { constantCache } = context
switch (node.type) {
case NodeTypes.ELEMENT:
if (node.tagType !== ElementTypes.ELEMENT) {
return ConstantTypes.NOT_CONSTANT
}
const cached = constantCache.get(node)
if (cached !== undefined) {
return cached
}
const codegenNode = node.codegenNode!
if (codegenNode.type !== NodeTypes.VNODE_CALL) {
return ConstantTypes.NOT_CONSTANT
}
if (
codegenNode.isBlock &&
node.tag !== 'svg' &&
node.tag !== 'foreignObject'
) {
return ConstantTypes.NOT_CONSTANT
}
const flag = getPatchFlag(codegenNode)
if (!flag) {
let returnType = ConstantTypes.CAN_STRINGIFY
// Element itself has no patch flag. However we still need to check:
// 1. Even for a node with no patch flag, it is possible for it to contain
// non-hoistable expressions that refers to scope variables, e.g. compiler
// injected keys or cached event handlers. Therefore we need to always
// check the codegenNode's props to be sure.
const generatedPropsType = getGeneratedPropsConstantType(node, context)
if (generatedPropsType === ConstantTypes.NOT_CONSTANT) {
constantCache.set(node, ConstantTypes.NOT_CONSTANT)
return ConstantTypes.NOT_CONSTANT
}
if (generatedPropsType < returnType) {
returnType = generatedPropsType
}
// 2. its children.
for (let i = 0; i < node.children.length; i++) {
const childType = getConstantType(node.children[i], context)
if (childType === ConstantTypes.NOT_CONSTANT) {
constantCache.set(node, ConstantTypes.NOT_CONSTANT)
return ConstantTypes.NOT_CONSTANT
}
if (childType < returnType) {
returnType = childType
}
}
// 3. if the type is not already CAN_SKIP_PATCH which is the lowest non-0
// type, check if any of the props can cause the type to be lowered
// we can skip can_patch because it's guaranteed by the absence of a
// patchFlag.
if (returnType > ConstantTypes.CAN_SKIP_PATCH) {
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind' && p.exp) {
const expType = getConstantType(p.exp, context)
if (expType === ConstantTypes.NOT_CONSTANT) {
constantCache.set(node, ConstantTypes.NOT_CONSTANT)
return ConstantTypes.NOT_CONSTANT
}
if (expType < returnType) {
returnType = expType
}
}
}
}
// only svg/foreignObject could be block here, however if they are
// static then they don't need to be blocks since there will be no
// nested updates.
if (codegenNode.isBlock) {
// except set custom directives.
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (p.type === NodeTypes.DIRECTIVE) {
constantCache.set(node, ConstantTypes.NOT_CONSTANT)
return ConstantTypes.NOT_CONSTANT
}
}
context.removeHelper(OPEN_BLOCK)
context.removeHelper(
getVNodeBlockHelper(context.inSSR, codegenNode.isComponent)
)
codegenNode.isBlock = false
context.helper(getVNodeHelper(context.inSSR, codegenNode.isComponent))
}
constantCache.set(node, returnType)
return returnType
} else {
constantCache.set(node, ConstantTypes.NOT_CONSTANT)
return ConstantTypes.NOT_CONSTANT
}
case NodeTypes.TEXT:
case NodeTypes.COMMENT:
return ConstantTypes.CAN_STRINGIFY
case NodeTypes.IF:
case NodeTypes.FOR:
case NodeTypes.IF_BRANCH:
return ConstantTypes.NOT_CONSTANT
case NodeTypes.INTERPOLATION:
case NodeTypes.TEXT_CALL:
return getConstantType(node.content, context)
case NodeTypes.SIMPLE_EXPRESSION:
return node.constType
case NodeTypes.COMPOUND_EXPRESSION:
let returnType = ConstantTypes.CAN_STRINGIFY
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i]
if (isString(child) || isSymbol(child)) {
continue
}
const childType = getConstantType(child, context)
if (childType === ConstantTypes.NOT_CONSTANT) {
return ConstantTypes.NOT_CONSTANT
} else if (childType < returnType) {
returnType = childType
}
}
return returnType
default:
if (__DEV__) {
const exhaustiveCheck: never = node
exhaustiveCheck
}
return ConstantTypes.NOT_CONSTANT
}
}其实这个方法之前分析compiler-dom的时候就遇到了,那时候没有说,只是看了某个node类型的处理过程,现在算是把坑埋上了。
codegenNode是什么时候放到node上面的呢?实际上是在一个插件上postTransformElement方法上,这个我们会在分析指令/插件的时候分析到。
当NodeType是ELELMENT的时候,如果tag的类型不是ELEMENT,那就是NOT_CONSTANT的,为什么呢?还记得之前说的么,ElementTypes指的是vDom的类型,除了ELEMENT之外就是COMPONENT/SLOT/TEMPLATE这仨货,所以自然都是NOT_CONSTANT的。
VNODE_CALL这个类型我们后面会遇到isBlock先mark下,也是到时候分析插件的时候会遇到,应该是和svg/foreignObject有关的.
对于没有flag的元素节点,它们的属性中可能引用了奇怪的东西,比如scope变量,并且这个变量表达式是不能hoist的,所以每次都需要check一遍节点的属性才行。既然属性不能放过,那它的子节点们自然更不可能错过。另外这里再强调一次,遇到等级低的就减低等级,宁可杀错不可放过。
接着又遇到isBlock了,看到了句注释only svg/foreignObject could be block here,这么说前面猜测和svg/foreignObject有关是正确的。
遇到svg,isBlock字段会变成true,这里还做了removeHelper的操作,处理完之后isBlock字段又置为false了。然后又新增一个helper,注意这里的两个helper,是不一样的。这里可以猜测下是因为前面处理svg有些问题,并不能处理成VNode,所以需要放到这里
export function getVNodeHelper(ssr: boolean, isComponent: boolean) {
return ssr || isComponent ? CREATE_VNODE : CREATE_ELEMENT_VNODE
}
export function getVNodeBlockHelper(ssr: boolean, isComponent: boolean) {
return ssr || isComponent ? CREATE_BLOCK : CREATE_ELEMENT_BLOCK
}这里有个else逻辑:element节点自身是没有flag的,如果有那肯定是NOT_CONSTANT等级的,这里先mark下来,应该后面会遇到。
这里还剩下几类:
TEXT/COMMENT:这俩货自然是CAN_STRINGIFY最高规格对待IF/FOR/IF_BRANCH:这仨也不用多想,自然的是NOT_CONSTANT最低规格对待。INTERPOLATION/TEXT_CALL:这个INTERPOLATION我们已经知道了,是{{}}相关的,但是这个TEXT_CALL,我们目前还没遇到过,暂时还不清楚是什么场景,所以先mark下,他俩因为是复合类型的,所以需要确定他们的content才行。SIMPLE_EXPRESSION:这个比较简单,在hoist之前就可以确定下来了,我们在compiler-dom的时候接触过,所以处理的位置应该是在nodeTransforms或者directiveTransforms里面,这里就不多说了。COMPOUND_EXPRESSION:复合表达式,存在多个表达式,这个自然是给每一个表达式过一遍。- 兜底默认是
NOT_COSTANT:自然是宁杀错不放过。
稍微总结下这个方法:简单的说就是给几种节点类型做判断,判断是否可以hoist/stringify。
主流程generate中遇到的函数#
genModulePreamble#
function genModulePreamble(
ast: RootNode,
context: CodegenContext,
genScopeId: boolean,
inline?: boolean
) {
const {
push,
newline,
optimizeImports,
runtimeModuleName,
ssrRuntimeModuleName
} = context
if (genScopeId && ast.hoists.length) {
ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
}
// generate import statements for helpers
if (ast.helpers.length) {
if (optimizeImports) {
// when bundled with webpack with code-split, calling an import binding
// as a function leads to it being wrapped with `Object(a.b)` or `(0,a.b)`,
// incurring both payload size increase and potential perf overhead.
// therefore we assign the imports to variables (which is a constant ~50b
// cost per-component instead of scaling with template size)
push(
`import { ${ast.helpers
.map(s => helperNameMap[s])
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
)
push(
`\n// Binding optimization for webpack code-split\nconst ${ast.helpers
.map(s => `_${helperNameMap[s]} = ${helperNameMap[s]}`)
.join(', ')}\n`
)
} else {
push(
`import { ${ast.helpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
)
}
}
if (ast.ssrHelpers && ast.ssrHelpers.length) {
push(
`import { ${ast.ssrHelpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from "${ssrRuntimeModuleName}"\n`
)
}
if (ast.imports.length) {
genImports(ast.imports, context)
newline()
}
genHoists(ast.hoists, context)
newline()
if (!inline) {
push(`export `)
}
} PUSH_SCOPED_ID:runtime的辅助函数,pushScopeIdPOP_SCOPE_ID:同上,popScopeId,这俩目前猜测应该是用来生成scopeId相关的,具体等到时我们去分析runtime的包的时候再看。optimizeImports:应该是用来兼容webpack的code-split也就是代码切割的,由于webpack会把import的函数装换成Object(a.b)或者(0, a.b),这样可能有潜在的性能开销以及负载变大的问题。所以这里把导入的函数分配给变量而不是直接使用,这样就能避免上面的问题,每个组件多开销大概50b左右。ast.imports:如果你看过我之前分析compiler-sfc的compileTemplate的话,应该知道这个是干啥的,当然,看了也大概率忘了,我自己都忘了,看数据才想起来的。

当时分析compileTemplate方法的时候我顺便把几个nodeTransforms注册的回调也跟着分析了。这个是和src等属性相关的,因为它们有可能用的是本地资源连接又或者是webpack特殊用法require包裹的路径等,所以这里需要重写路径等处理。
genImports:代码很少,我们直接看
function genImports(importsOptions: ImportItem[], context: CodegenContext) {
if (!importsOptions.length) {
return
}
importsOptions.forEach(imports => {
context.push(`import `)
genNode(imports.exp, context)
context.push(` from '${imports.path}'`)
context.newline()
})
}
function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
const { content, isStatic } = node
context.push(isStatic ? JSON.stringify(content) : content, node)
}
function newline(n: number) {
context.push('\n' + ` `.repeat(n))
}genNode方法是根据NodeTypes的类型来判断具体执行哪个方法,所以这里就不看了,这里的NodeTypes是4,对应SIMPLE_EXPRESSION,所以这里直接拿过来了。
newline:这里说一下,就是换行以及+/-缩进。
这里用到了context的push方法,我们顺便来细嗦下push这个方法。
push(code, node) {
context.code += code
if (!__BROWSER__ && context.map) {
if (node) {
let name
if (node.type === NodeTypes.SIMPLE_EXPRESSION && !node.isStatic) {
const content = node.content.replace(/^_ctx\./, '')
if (content !== node.content && isSimpleIdentifier(content)) {
name = content
}
}
addMapping(node.loc.start, name)
}
advancePositionWithMutation(context, code)
if (node && node.loc !== locStub) {
addMapping(node.loc.end)
}
}
},直接字符串拼接,然后(在这里)content也就是_imports_index是带有_ctx.的,那么就去掉。
addMapping就不看了,和代码位置相关的。advancePositionWithMutation这个方法之前有说过,这里就不说了,在这里也是用来改变定位的,因为我们push了code,所以我们也需要迁移code.length个位置,这样定位才能准确。
最后会变成这样\nimport _imports_0 from './a.jpg'\nimport _imports_1 from './b.jpg'\n'。
回到genModulePreamble方法中
genHoists:这里代码也比较少,我们直接看就好
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()
// generate inlined withScopeId helper
if (genScopeId) {
push(
`const _withScopeId = n => (${helper(
PUSH_SCOPE_ID
)}("${scopeId}"),n=n(),${helper(POP_SCOPE_ID)}(),n)`
)
newline()
}
for (let i = 0; i < hoists.length; i++) {
const exp = hoists[i]
if (exp) {
const needScopeIdWrapper = genScopeId && exp.type === NodeTypes.VNODE_CALL
push(
`const _hoisted_${i + 1} = ${
needScopeIdWrapper ? `${PURE_ANNOTATION} _withScopeId(() => ` : ``
}`
)
genNode(exp, context)
if (needScopeIdWrapper) {
push(`)`)
}
newline()
}
}
context.pure = false
}看名字就知道是用来生成静态提升的节点的runtime代码的。
注意这里面的节点前面有分析过了,不仅有dom节点,还有属性节点。
genScopeId:这个是生成scopeId的辅助函数,这里会变成const _withScopeId = n => (push_scope_id(), n = n(), pop_scope_id(), n)\n
其它没啥好说的,最后的数据类似下面这样
const _hoisted_9 = /*#__PURE__*/_createElementVNode("p", { textContent: 'text' }, null, -1 /* HOISTED */)\n至于genCode里面做了什么,这里就不分析了,后面再开个一级标题一个个分析。
回到genModulePreamble方法中
最后还多加了个export,为什么呢?因为已经import和常量声明都已经完毕了,可以准备整合导出render function了。
genAssets#
function genAssets(
assets: string[],
type: 'component' | 'directive' | 'filter',
{ helper, push, newline, isTS }: CodegenContext
) {
const resolver = helper(
__COMPAT__ && type === 'filter'
? RESOLVE_FILTER
: type === 'component'
? RESOLVE_COMPONENT
: RESOLVE_DIRECTIVE
)
for (let i = 0; i < assets.length; i++) {
let id = assets[i]
// potential component implicit self-reference inferred from SFC filename
const maybeSelfReference = id.endsWith('__self')
if (maybeSelfReference) {
id = id.slice(0, -6)
}
push(
`const ${toValidAssetId(id, type)} = ${resolver}(${JSON.stringify(id)}${
maybeSelfReference ? `, true` : ``
})${isTS ? `!` : ``}`
)
if (i < assets.length - 1) {
newline()
}
}
}
export function toValidAssetId(
name: string,
type: 'component' | 'directive' | 'filter'
): string {
// see issue#4422, we need adding identifier on validAssetId if variable `name` has specific character
return `_${type}_${name.replace(/[^\w]/g, (searchValue, replaceValue) => {
return searchValue === '-' ? '_' : name.charCodeAt(replaceValue).toString()
})}`
}这个方法其实没什么好说的,就是在组装一个辅助函数。有三种类型component/filter/directive。我们这里的场景是component,对应的runtime函数是resolveComponent。
另外把这个代码贴出来最主要的原因就是这里有一段对自引用的分析。
在vue组件中,我们可以在template通过使用当前组件的名字来使用当前组件达成递归组件的方式。这里通过注释可以了解到是怎么判断的:potential component implicit self-reference inferred from SFC filename,实际上是通过文件名推断出来的。并且在转换过程中会给它的结尾加上__self用于区分。
来看下转换出来的runtime代码: const _component_Add = _resolveComponent("Add");
另外两种场景就不看了,类似的。
最后#
祝大家新年快乐~~~~~~~~
参考#
- ^vue-tree-flattern https://vuejs.org/guide/extras/rendering-mechanism.html#tree-flattening
- ^vue-custom-directive https://vuejs.org/guide/reusability/custom-directives.html#custom-directives
- ^babel-core https://babeljs.io/docs/en/babel-core
- ^vue-patchFlags https://vuejs.org/guide/extras/rendering-mechanism.html#patch-flags
编辑于 2023-01-21 18:19・IP 属地广东
