coverPiccoverPic

Vue 3 模板编译

总览

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
  1. export function baseParse(input: string, options?: ParserOptions): RootNode {
  2. reset()
  3. currentInput = input
  4. currentOptions = extend({}, defaultParserOptions)
  5. if (options) {
  6. let key: keyof ParserOptions
  7. for (key in options) {
  8. if (options[key] != null) {
  9. // @ts-expect-error
  10. currentOptions[key] = options[key]
  11. }
  12. }
  13. }
  14. tokenizer.mode =
  15. currentOptions.parseMode === 'html'
  16. ? ParseMode.HTML
  17. : currentOptions.parseMode === 'sfc'
  18. ? ParseMode.SFC
  19. : ParseMode.BASE
  20. tokenizer.inXML =
  21. currentOptions.ns === Namespaces.SVG ||
  22. currentOptions.ns === Namespaces.MATH_ML
  23. const delimiters = options?.delimiters
  24. if (delimiters) {
  25. tokenizer.delimiterOpen = toCharCodes(delimiters[0])
  26. tokenizer.delimiterClose = toCharCodes(delimiters[1])
  27. }
  28. const root = (currentRoot = createRoot([], input))
  29. tokenizer.parse(currentInput)
  30. root.loc = getLoc(0, input.length)
  31. root.children = condenseWhitespace(root.children)
  32. currentRoot = null
  33. return root
  34. }

tokenizer.parse这个函数…emmm,我们不看了,这段东西非常的长,主要内容是把 vue 2 的正则匹配和 vue 3 早些版本的一长串 if-else 匹配模板改成了自动状态机,原理还是一样的。这里处理标签闭合还是用的栈结构,最后生成 AST。

匹配模板就不看了,来看 AST 节点的结构:

ts
  1. interface Node {
  2. type: NodeTypes
  3. loc: SourceLocation
  4. }
  5. interface BaseElementNode extends Node {
  6. type: NodeTypes.ELEMENT
  7. ns: Namespace
  8. tag: string
  9. tagType: ElementTypes
  10. props: Array<AttributeNode | DirectiveNode>
  11. children: TemplateChildNode[]
  12. isSelfClosing?: boolean
  13. innerLoc?: SourceLocation // only for SFC root level elements
  14. }

此外,部分节点会有codegenNode属性,会在trasnform时加入,用于调整 AST 节点,在generate中以此生成 VNode 的代码。

举个栗子,这样子的标签

html
  1. <span
  2. data1="test1"
  3. :data2="test2"
  4. @click="clickHandler"
  5. >{{ testText }}</span>

可以生成

json
  1. {
  2. "type": 1, // 1 为 标签类型,AST 节点的类型,这里每个标签、文本、属性、属性值等等都可以算一个节点。
  3. "tag": "span", // 标签名
  4. "ns": 0,
  5. "tagType": 0, // 标签类型,0 是普通的 HTML 标签
  6. "props": [
  7. {
  8. "type": 6, // 属性类型
  9. "name": "data1", // 属性名
  10. "value": {
  11. "type": 2, // 文本类型
  12. "content": "test1", // 内容
  13. },
  14. },
  15. {
  16. "type": 7, // 命令类型的 AST 节点
  17. "name": "bind", // 即为 v-bind 命令
  18. "rawName": ":data2",
  19. "exp": { // 命令的内容
  20. "type": 4, // 简单表达式类型的 AST 节点
  21. "content": "test2",
  22. "isStatic": false,
  23. "constType": 0
  24. },
  25. "arg": { // 命令的入参
  26. "type": 4,
  27. "content": "data2",
  28. "isStatic": true,
  29. "constType": 3
  30. },
  31. "modifiers": [],
  32. },
  33. {
  34. "type": 7, // 命令类型,即 v-on
  35. "name": "on",
  36. "rawName": "@click",
  37. "exp": {
  38. "type": 4,
  39. "content": "clickHandler",
  40. "isStatic": false,
  41. "constType": 0
  42. },
  43. "arg": {
  44. "type": 4,
  45. "content": "click",
  46. "isStatic": true,
  47. "constType": 3
  48. },
  49. "modifiers": [],
  50. }
  51. ],
  52. "children": [ // 子节点
  53. {
  54. "type": 5, // 双括号表达式类型
  55. "content": {
  56. "type": 4,
  57. "content": "testText",
  58. "isStatic": false,
  59. "constType": 0
  60. },
  61. }
  62. ],
  63. }

这里的 AST 节点和 2.x 版本还是有很多变化的。

transform

2.x 版本第二步叫optimize,做的事情主要是对特定的命令和 DOM 做 AST 的调整,还有就是标记静态的部分,优化后续的 diff。

ts
  1. export function transform(root: RootNode, options: TransformOptions) {
  2. const context = createTransformContext(root, options)
  3. traverseNode(root, context)
  4. if (options.hoistStatic) {
  5. hoistStatic(root, context)
  6. }
  7. if (!options.ssr) {
  8. createRootCodegen(root, context)
  9. }
  10. // finalize meta information
  11. root.helpers = new Set([...context.helpers.keys()])
  12. root.components = [...context.components]
  13. root.directives = [...context.directives]
  14. root.imports = context.imports
  15. root.hoists = context.hoists
  16. root.temps = context.temps
  17. root.cached = context.cached
  18. root.transformed = true
  19. if (__COMPAT__) {
  20. root.filters = [...context.filters!]
  21. }
  22. }

PatchFlag

baseCompile中,多个 transformXxx 函数被传入到transform中,来看transformElement。PatchFlag 会影响渲染时的 diff,优化渲染,在transformElement中:

ts
  1. const propsBuildResult = buildProps(
  2. node,
  3. context,
  4. undefined,
  5. isComponent,
  6. isDynamicComponent,
  7. )
  8. patchFlag = propsBuildResult.patchFlag

来看buildPropspatchFlag相关逻辑

ts
  1. export function buildProps(
  2. // ...
  3. ) {
  4. // patchFlag analysis
  5. let patchFlag = 0
  6. //...
  7. if (hasDynamicKeys) {
  8. patchFlag |= PatchFlags.FULL_PROPS
  9. } else {
  10. if (hasClassBinding && !isComponent) {
  11. patchFlag |= PatchFlags.CLASS
  12. }
  13. if (hasStyleBinding && !isComponent) {
  14. patchFlag |= PatchFlags.STYLE
  15. }
  16. if (dynamicPropNames.length) {
  17. patchFlag |= PatchFlags.PROPS
  18. }
  19. if (hasHydrationEventBinding) {
  20. patchFlag |= PatchFlags.NEED_HYDRATION
  21. }
  22. }
  23. if (
  24. !shouldUseBlock &&
  25. (patchFlag === 0 || patchFlag === PatchFlags.NEED_HYDRATION) &&
  26. (hasRef || hasVnodeHook || runtimeDirectives.length > 0)
  27. ) {
  28. patchFlag |= PatchFlags.NEED_PATCH
  29. }
  30. // ...
  31. return {
  32. patchFlag,
  33. // ...
  34. }
  35. }

很明显,PatchFlag 采用二进制的方式存储,在 diff 的时候,通过与运算判断 VNode 是否有某种 patchFlag 优化运算,例如,没有PatchFlags.CLASS说明没有动态类名,diff 的时候就不需要做相关处理。下面是 PatchFlag 的枚举:

ts
  1. export enum PatchFlags {
  2. // 动态文本
  3. TEXT = 1,
  4. // 动态类名
  5. CLASS = 1 << 1,
  6. // 动态样式
  7. STYLE = 1 << 2,
  8. // 拥有非 class 和 style 的其他动态 props(组件的 props 上写了的话也可以包含 class 和 style)
  9. PROPS = 1 << 3,
  10. // 全量 diff props
  11. FULL_PROPS = 1 << 4,
  12. // 注水用的,有这个 flag 或者 FULL_PROPS,注水时会 diff props
  13. NEED_HYDRATION = 1 << 5,
  14. // 子节点不会变化的 fragment
  15. STABLE_FRAGMENT = 1 << 6,
  16. // 有 key 或者子节点有 key 的 fragment
  17. KEYED_FRAGMENT = 1 << 7,
  18. // 没有 key 的 fragment
  19. UNKEYED_FRAGMENT = 1 << 8,
  20. // 不需要比较 props 的节点
  21. NEED_PATCH = 1 << 9,
  22. // 动态插槽
  23. DYNAMIC_SLOTS = 1 << 10,
  24. DEV_ROOT_FRAGMENT = 1 << 11,
  25. // 静态节点
  26. HOISTED = -1,
  27. // 不采用优化的 diff 算法
  28. BAIL = -2,
  29. }

静态提升

来看hoistStatic,调用walk函数进行静态提升,渲染时直接使用缓存中的内容,减少不必要的运算(后续在 generate 中处理)。

ts
  1. export function hoistStatic(root: RootNode, context: TransformContext) {
  2. walk(
  3. root,
  4. context,
  5. // Root node is unfortunately non-hoistable due to potential parent
  6. // fallthrough attributes.
  7. isSingleElementRoot(root, root.children[0]),
  8. )
  9. }

函数很长,就不看了,下面是静态提升逻辑图示:

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
  1. hoist(exp) {
  2. if (isString(exp)) exp = createSimpleExpression(exp)
  3. context.hoists.push(exp)
  4. const identifier = createSimpleExpression(
  5. `_hoisted_${context.hoists.length}`,
  6. false,
  7. exp.loc,
  8. ConstantTypes.CAN_HOIST,
  9. )
  10. identifier.hoisted = exp
  11. return identifier
  12. }
  13. // createSimpleExpression
  14. export function createSimpleExpression(
  15. content: SimpleExpressionNode['content'],
  16. isStatic: SimpleExpressionNode['isStatic'] = false,
  17. loc: SourceLocation = locStub,
  18. constType: ConstantTypes = ConstantTypes.NOT_CONSTANT,
  19. ): SimpleExpressionNode {
  20. return {
  21. type: NodeTypes.SIMPLE_EXPRESSION,
  22. loc,
  23. content,
  24. isStatic,
  25. constType: isStatic ? ConstantTypes.CAN_STRINGIFY : constType,
  26. }
  27. }

生成内容为_hoisted_xxxSIMPLE_EXPRESSION类型的 AST 节点。在generatecontext.hoists所含元素会被定义成render函数“顶层”作用域下,名为_hoisted_xxx的变量。而context.hoist生成的 AST 节点则会在render函数中拼接上_hoisted_xxx的变量名,这样子就可以引用被提升的 AST 节点所生成的 VNode 生成函数了。

当一个标签节点被处理完后,调用transformHoist函数进行字符串化,在渲染时以innerHTML的形式创建元素提高性能。注意到 packages/compiler-dom/src/index.ts 中:

ts
  1. export function compile(
  2. src: string | RootNode,
  3. options: CompilerOptions = {},
  4. ): CodegenResult {
  5. return baseCompile(
  6. src,
  7. extend({}, parserOptions, options, {
  8. // ...
  9. transformHoist: __BROWSER__ ? null : stringifyStatic,
  10. }),
  11. )
  12. }

节点静态提升时会调用stringifyStatic函数。太长就不看了,这个函数简单来说,它对当前标签节点的子元素进行解析,如果连续的被静态提升的子元素中,标签/文本/注释数量大于等于 20 或者有属性的标签大于等于 5 的时候,把context.hoists中首个子元素对应节点如下 AST 节点:

ts
  1. const staticCall = createCallExpression(context.helper(CREATE_STATIC), [
  2. JSON.stringify(
  3. currentChunk.map(node => stringifyNode(node, context)).join(''),
  4. ).replace(expReplaceRE, `" + $1 + "`),
  5. // the 2nd argument indicates the number of DOM nodes this static vnode
  6. // will insert / hydrate
  7. String(currentChunk.length),
  8. ])

createCallExpression就是创建一个函数调用的 JS 的 AST 节点。context.helper(CREATE_STATIC)generate会被转换成createStaticVNode,即:

ts
  1. export function createStaticVNode(
  2. content: string,
  3. numberOfNodes: number,
  4. ): VNode {
  5. // A static vnode can contain multiple stringified elements, and the number
  6. // of elements is necessary for hydration.
  7. const vnode = createVNode(Static, null, content)
  8. vnode.staticCount = numberOfNodes
  9. return vnode
  10. }

举个栗子🌰:

html
  1. <div>
  2. <div>
  3. <!-- test -->
  4. </div>
  5. <div>test</div>
  6. <div>test</div>
  7. <div>test</div>
  8. <div>test</div>
  9. <div>test</div>
  10. <div>test</div>
  11. <div>test</div>
  12. <div>test</div>
  13. <div>test</div>
  14. </div>

会被编译成

ts
  1. const _Vue = Vue
  2. const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode, createStaticVNode: _createStaticVNode } = _Vue
  3. 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)
  4. const _hoisted_11 = [
  5. _hoisted_1
  6. ]
  7. return function render(_ctx, _cache) {
  8. with (_ctx) {
  9. const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
  10. return (_openBlock(), _createElementBlock("div", null, _hoisted_11))
  11. }
  12. }

事件缓存

模板编译器baseCompile函数如果传入cacheHandlers的配置(在单文件组件中默认 true),就可以缓存方法,(在 generate 中)事件回调函数被缓存起来,当使用行内函数的时候,diff 的时候每次拿到的是同一个函数,就不会引起不必要的的重新渲染。见transformOn

ts
  1. export const transformOn: DirectiveTransform = (
  2. dir,
  3. node,
  4. context,
  5. augmentor,
  6. ) => {
  7. // ...
  8. if (shouldCache) {
  9. // cache handlers so that it's always the same handler being passed down.
  10. // this avoids unnecessary re-renders when users use inline handlers on
  11. // components.
  12. ret.props[0].value = context.cache(ret.props[0].value)
  13. }
  14. // ...
  15. }

context.cache

ts
  1. cache(exp, isVNode = false) {
  2. return createCacheExpression(context.cached++, exp, isVNode)
  3. }

以及createCacheExpression

ts
  1. export function createCacheExpression(
  2. index: number,
  3. value: JSChildNode,
  4. isVNode: boolean = false,
  5. ): CacheExpression {
  6. return {
  7. type: NodeTypes.JS_CACHE_EXPRESSION,
  8. index,
  9. value,
  10. isVNode,
  11. loc: locStub,
  12. }
  13. }

generate

根据上面 AST 节点来生成代码,以及 sourceMap。

静态提升

先来看genHoists,看起来是把静态提升的内容直接声明了一个变量。

ts
  1. function genHoists(hoists: (JSChildNode | null)[], context: CodegenContext) {
  2. if (!hoists.length) {
  3. return
  4. }
  5. context.pure = true
  6. const { push, newline, helper, scopeId, mode } = context
  7. const genScopeId = !__BROWSER__ && scopeId != null && mode !== 'function'
  8. newline()
  9. // generate inlined withScopeId helper
  10. if (genScopeId) {
  11. push(
  12. `const _withScopeId = n => (${helper(
  13. PUSH_SCOPE_ID,
  14. )}("${scopeId}"),n=n(),${helper(POP_SCOPE_ID)}(),n)`,
  15. )
  16. newline()
  17. }
  18. for (let i = 0; i < hoists.length; i++) {
  19. const exp = hoists[i]
  20. if (exp) {
  21. const needScopeIdWrapper = genScopeId && exp.type === NodeTypes.VNODE_CALL
  22. push(
  23. `const _hoisted_${i + 1} = ${
  24. needScopeIdWrapper ? `${PURE_ANNOTATION} _withScopeId(() => ` : ``
  25. }`,
  26. )
  27. genNode(exp, context)
  28. if (needScopeIdWrapper) {
  29. push(`)`)
  30. }
  31. newline()
  32. }
  33. }
  34. context.pure = false
  35. }

也就是上文中_hoisted_1之类的变量声明

事件缓存

如果是JS_CACHE_EXPRESSION类型的节点,调用genCacheExpression。可见,函数被存到一个叫做_cache的数组里面了,就算我们用内联代码作为回调函数,也可以每次都去到同一个函数的引用。

ts
  1. function genCacheExpression(node: CacheExpression, context: CodegenContext) {
  2. const { push, helper, indent, deindent, newline } = context
  3. push(`_cache[${node.index}] || (`)
  4. if (node.isVNode) {
  5. indent()
  6. push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
  7. newline()
  8. }
  9. push(`_cache[${node.index}] = `)
  10. genNode(node.value, context)
  11. if (node.isVNode) {
  12. push(`,`)
  13. newline()
  14. push(`${helper(SET_BLOCK_TRACKING)}(1),`)
  15. newline()
  16. push(`_cache[${node.index}]`)
  17. deindent()
  18. }
  19. push(`)`)
  20. }

总结

对 Vue 3 模板编译流程重要的地方进行了简介。

0 条评论未登录用户
Ctrl or + Enter 评论
🌸 Run