总览
Vue 3 完整版在挂载组件的时候,如果组件实例没有render
函数,就会走模板编译的逻辑(运行时版已通过工程化框架的插件自动编译模板,生成render
函数)。这里用的是 v3.4.9 版本的源码,具体在 packages/vue/src/index.ts 的compileToFunction
函数中,它最后调用 packages/compiler-core/src/compile.ts 的baseCompile
进行编译模板,大体流程如下:
graph LR baseParse-->transform-->generate
baseParse
这个和 vue 2 的parse
差别不大,就是把模板字符串解析为 AST。tokenizer
是一个模板字符串解析器,在 parser.ts 中为tokenizer
注入各种回调函数来生成 AST 节点。baseParse
调用tokenizer.parse
函数解析模板。
ts- export function baseParse(input: string, options?: ParserOptions): RootNode {
- reset()
- currentInput = input
- currentOptions = extend({}, defaultParserOptions)
- if (options) {
- let key: keyof ParserOptions
- for (key in options) {
- if (options[key] != null) {
- // @ts-expect-error
- currentOptions[key] = options[key]
- }
- }
- }
- tokenizer.mode =
- currentOptions.parseMode === 'html'
- ? ParseMode.HTML
- : currentOptions.parseMode === 'sfc'
- ? ParseMode.SFC
- : ParseMode.BASE
- tokenizer.inXML =
- currentOptions.ns === Namespaces.SVG ||
- currentOptions.ns === Namespaces.MATH_ML
- const delimiters = options?.delimiters
- if (delimiters) {
- tokenizer.delimiterOpen = toCharCodes(delimiters[0])
- tokenizer.delimiterClose = toCharCodes(delimiters[1])
- }
- const root = (currentRoot = createRoot([], input))
- tokenizer.parse(currentInput)
- root.loc = getLoc(0, input.length)
- root.children = condenseWhitespace(root.children)
- currentRoot = null
- return root
- }
tokenizer.parse
这个函数…emmm,我们不看了,这段东西非常的长,主要内容是把 vue 2 的正则匹配和 vue 3 早些版本的一长串 if-else 匹配模板改成了自动状态机,原理还是一样的。这里处理标签闭合还是用的栈结构,最后生成 AST。
匹配模板就不看了,来看 AST 节点的结构:
ts- interface Node {
- type: NodeTypes
- loc: SourceLocation
- }
- interface BaseElementNode extends Node {
- type: NodeTypes.ELEMENT
- ns: Namespace
- tag: string
- tagType: ElementTypes
- props: Array<AttributeNode | DirectiveNode>
- children: TemplateChildNode[]
- isSelfClosing?: boolean
- innerLoc?: SourceLocation // only for SFC root level elements
- }
此外,部分节点会有codegenNode
属性,会在trasnform
时加入,用于调整 AST 节点,在generate
中以此生成 VNode 的代码。
举个栗子,这样子的标签
html- <span
- data1="test1"
- :data2="test2"
- @click="clickHandler"
- >{{ testText }}</span>
可以生成
json- {
- "type": 1, // 1 为 标签类型,AST 节点的类型,这里每个标签、文本、属性、属性值等等都可以算一个节点。
- "tag": "span", // 标签名
- "ns": 0,
- "tagType": 0, // 标签类型,0 是普通的 HTML 标签
- "props": [
- {
- "type": 6, // 属性类型
- "name": "data1", // 属性名
- "value": {
- "type": 2, // 文本类型
- "content": "test1", // 内容
- },
- },
- {
- "type": 7, // 命令类型的 AST 节点
- "name": "bind", // 即为 v-bind 命令
- "rawName": ":data2",
- "exp": { // 命令的内容
- "type": 4, // 简单表达式类型的 AST 节点
- "content": "test2",
- "isStatic": false,
- "constType": 0
- },
- "arg": { // 命令的入参
- "type": 4,
- "content": "data2",
- "isStatic": true,
- "constType": 3
- },
- "modifiers": [],
- },
- {
- "type": 7, // 命令类型,即 v-on
- "name": "on",
- "rawName": "@click",
- "exp": {
- "type": 4,
- "content": "clickHandler",
- "isStatic": false,
- "constType": 0
- },
- "arg": {
- "type": 4,
- "content": "click",
- "isStatic": true,
- "constType": 3
- },
- "modifiers": [],
- }
- ],
- "children": [ // 子节点
- {
- "type": 5, // 双括号表达式类型
- "content": {
- "type": 4,
- "content": "testText",
- "isStatic": false,
- "constType": 0
- },
- }
- ],
- }
这里的 AST 节点和 2.x 版本还是有很多变化的。
transform
2.x 版本第二步叫optimize
,做的事情主要是对特定的命令和 DOM 做 AST 的调整,还有就是标记静态的部分,优化后续的 diff。
ts- 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 = new Set([...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
- root.transformed = true
- if (__COMPAT__) {
- root.filters = [...context.filters!]
- }
- }
PatchFlag
在baseCompile
中,多个 transformXxx 函数被传入到transform
中,来看transformElement
。PatchFlag 会影响渲染时的 diff,优化渲染,在transformElement
中:
ts- const propsBuildResult = buildProps(
- node,
- context,
- undefined,
- isComponent,
- isDynamicComponent,
- )
- patchFlag = propsBuildResult.patchFlag
来看buildProps
的patchFlag
相关逻辑
ts- export function buildProps(
- // ...
- ) {
- // patchFlag analysis
- let patchFlag = 0
- //...
- if (hasDynamicKeys) {
- patchFlag |= PatchFlags.FULL_PROPS
- } else {
- if (hasClassBinding && !isComponent) {
- patchFlag |= PatchFlags.CLASS
- }
- if (hasStyleBinding && !isComponent) {
- patchFlag |= PatchFlags.STYLE
- }
- if (dynamicPropNames.length) {
- patchFlag |= PatchFlags.PROPS
- }
- if (hasHydrationEventBinding) {
- patchFlag |= PatchFlags.NEED_HYDRATION
- }
- }
- if (
- !shouldUseBlock &&
- (patchFlag === 0 || patchFlag === PatchFlags.NEED_HYDRATION) &&
- (hasRef || hasVnodeHook || runtimeDirectives.length > 0)
- ) {
- patchFlag |= PatchFlags.NEED_PATCH
- }
- // ...
- return {
- patchFlag,
- // ...
- }
- }
很明显,PatchFlag 采用二进制的方式存储,在 diff 的时候,通过与运算判断 VNode 是否有某种 patchFlag 优化运算,例如,没有PatchFlags.CLASS
说明没有动态类名,diff 的时候就不需要做相关处理。下面是 PatchFlag 的枚举:
ts- export enum PatchFlags {
- // 动态文本
- TEXT = 1,
- // 动态类名
- CLASS = 1 << 1,
- // 动态样式
- STYLE = 1 << 2,
- // 拥有非 class 和 style 的其他动态 props(组件的 props 上写了的话也可以包含 class 和 style)
- PROPS = 1 << 3,
- // 全量 diff props
- FULL_PROPS = 1 << 4,
- // 注水用的,有这个 flag 或者 FULL_PROPS,注水时会 diff props
- NEED_HYDRATION = 1 << 5,
- // 子节点不会变化的 fragment
- STABLE_FRAGMENT = 1 << 6,
- // 有 key 或者子节点有 key 的 fragment
- KEYED_FRAGMENT = 1 << 7,
- // 没有 key 的 fragment
- UNKEYED_FRAGMENT = 1 << 8,
- // 不需要比较 props 的节点
- NEED_PATCH = 1 << 9,
- // 动态插槽
- DYNAMIC_SLOTS = 1 << 10,
- DEV_ROOT_FRAGMENT = 1 << 11,
- // 静态节点
- HOISTED = -1,
- // 不采用优化的 diff 算法
- BAIL = -2,
- }
静态提升
来看hoistStatic
,调用walk
函数进行静态提升,渲染时直接使用缓存中的内容,减少不必要的运算(后续在 generate 中处理)。
ts- 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]),
- )
- }
函数很长,就不看了,下面是静态提升逻辑图示:
graph TB A(walk)-->H(遍历子节点数组各元素)-->B(普通元素)-->|if 静态节点|C(静态提升) B-->|else|D(非静态节点)-->|if props 可被静态提升|E(静态提升 props) D-->F(静态提升动态 props 的属性名) H-->G("组件、v-for、v-if")-->A H-->|所有子节点都被提升了|I(提升包含子节点的数组)
静态提升时调用方法context.hoist
:
ts- 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
- }
- // createSimpleExpression
- export function createSimpleExpression(
- content: SimpleExpressionNode['content'],
- isStatic: SimpleExpressionNode['isStatic'] = false,
- loc: SourceLocation = locStub,
- constType: ConstantTypes = ConstantTypes.NOT_CONSTANT,
- ): SimpleExpressionNode {
- return {
- type: NodeTypes.SIMPLE_EXPRESSION,
- loc,
- content,
- isStatic,
- constType: isStatic ? ConstantTypes.CAN_STRINGIFY : constType,
- }
- }
生成内容为_hoisted_xxx
的SIMPLE_EXPRESSION
类型的 AST 节点。在generate
中context.hoists
所含元素会被定义成render
函数“顶层”作用域下,名为_hoisted_xxx
的变量。而context.hoist
生成的 AST 节点则会在render
函数中拼接上_hoisted_xxx
的变量名,这样子就可以引用被提升的 AST 节点所生成的 VNode 生成函数了。
当一个标签节点被处理完后,调用transformHoist
函数进行字符串化,在渲染时以innerHTML
的形式创建元素提高性能。注意到 packages/compiler-dom/src/index.ts 中:
ts- export function compile(
- src: string | RootNode,
- options: CompilerOptions = {},
- ): CodegenResult {
- return baseCompile(
- src,
- extend({}, parserOptions, options, {
- // ...
- transformHoist: __BROWSER__ ? null : stringifyStatic,
- }),
- )
- }
节点静态提升时会调用stringifyStatic
函数。太长就不看了,这个函数简单来说,它对当前标签节点的子元素进行解析,如果连续的被静态提升的子元素中,标签/文本/注释数量大于等于 20 或者有属性的标签大于等于 5 的时候,把context.hoists
中首个子元素对应节点如下 AST 节点:
ts- const staticCall = createCallExpression(context.helper(CREATE_STATIC), [
- JSON.stringify(
- currentChunk.map(node => stringifyNode(node, context)).join(''),
- ).replace(expReplaceRE, `" + $1 + "`),
- // the 2nd argument indicates the number of DOM nodes this static vnode
- // will insert / hydrate
- String(currentChunk.length),
- ])
createCallExpression
就是创建一个函数调用的 JS 的 AST 节点。context.helper(CREATE_STATIC)
在generate
会被转换成createStaticVNode
,即:
ts- export function createStaticVNode(
- content: string,
- numberOfNodes: number,
- ): VNode {
- // A static vnode can contain multiple stringified elements, and the number
- // of elements is necessary for hydration.
- const vnode = createVNode(Static, null, content)
- vnode.staticCount = numberOfNodes
- return vnode
- }
举个栗子🌰:
html- <div>
- <div>
- <!-- test -->
- </div>
- <div>test</div>
- <div>test</div>
- <div>test</div>
- <div>test</div>
- <div>test</div>
- <div>test</div>
- <div>test</div>
- <div>test</div>
- <div>test</div>
- </div>
会被编译成
ts- const _Vue = Vue
- const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode, createStaticVNode: _createStaticVNode } = _Vue
- const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<div><!-- test --></div><div>test</div><div>test</div><div>test</div><div>test</div><div>test</div><div>test</div><div>test</div><div>test</div><div>test</div>", 10)
- const _hoisted_11 = [
- _hoisted_1
- ]
- return function render(_ctx, _cache) {
- with (_ctx) {
- const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
- return (_openBlock(), _createElementBlock("div", null, _hoisted_11))
- }
- }
事件缓存
模板编译器baseCompile
函数如果传入cacheHandlers
的配置(在单文件组件中默认 true),就可以缓存方法,(在 generate 中)事件回调函数被缓存起来,当使用行内函数的时候,diff 的时候每次拿到的是同一个函数,就不会引起不必要的的重新渲染。见transformOn
:
ts- export const transformOn: DirectiveTransform = (
- dir,
- node,
- context,
- augmentor,
- ) => {
- // ...
- if (shouldCache) {
- // cache handlers so that it's always the same handler being passed down.
- // this avoids unnecessary re-renders when users use inline handlers on
- // components.
- ret.props[0].value = context.cache(ret.props[0].value)
- }
- // ...
- }
看context.cache
ts- cache(exp, isVNode = false) {
- return createCacheExpression(context.cached++, exp, isVNode)
- }
以及createCacheExpression
:
ts- export function createCacheExpression(
- index: number,
- value: JSChildNode,
- isVNode: boolean = false,
- ): CacheExpression {
- return {
- type: NodeTypes.JS_CACHE_EXPRESSION,
- index,
- value,
- isVNode,
- loc: locStub,
- }
- }
generate
根据上面 AST 节点来生成代码,以及 sourceMap。
静态提升
先来看genHoists
,看起来是把静态提升的内容直接声明了一个变量。
ts- 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
- }
也就是上文中_hoisted_1
之类的变量声明
事件缓存
如果是JS_CACHE_EXPRESSION
类型的节点,调用genCacheExpression
。可见,函数被存到一个叫做_cache
的数组里面了,就算我们用内联代码作为回调函数,也可以每次都去到同一个函数的引用。
ts- function genCacheExpression(node: CacheExpression, context: CodegenContext) {
- const { push, helper, indent, deindent, newline } = context
- push(`_cache[${node.index}] || (`)
- if (node.isVNode) {
- indent()
- push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
- newline()
- }
- push(`_cache[${node.index}] = `)
- genNode(node.value, context)
- if (node.isVNode) {
- push(`,`)
- newline()
- push(`${helper(SET_BLOCK_TRACKING)}(1),`)
- newline()
- push(`_cache[${node.index}]`)
- deindent()
- }
- push(`)`)
- }
总结
对 Vue 3 模板编译流程重要的地方进行了简介。