参考及抄袭自下列文章
compile——优化静态内容.md
Vue中优化器为什么需要标记静态根节点?
Vue3 模板编译原理

vue2 compilerimage.png

模板编译分为三个阶段:生成ast、优化静态内容、生成render

ast

  1. // flow/compiler.js
  2. declare type ASTNode = ASTElement | ASTText | ASTExpression;
  3. declare type ASTElement = {
  4. type: 1;
  5. tag: string;
  6. attrsList: Array<ASTAttr>;
  7. attrsMap: { [key: string]: any };
  8. rawAttrsMap: { [key: string]: ASTAttr };
  9. parent: ASTElement | void;
  10. children: Array<ASTNode>;
  11. start?: number;
  12. end?: number;
  13. processed?: true;
  14. static?: boolean;
  15. staticRoot?: boolean;
  16. staticInFor?: boolean;
  17. staticProcessed?: boolean;
  18. hasBindings?: boolean;
  19. text?: string;
  20. attrs?: Array<ASTAttr>;
  21. dynamicAttrs?: Array<ASTAttr>;
  22. props?: Array<ASTAttr>;
  23. plain?: boolean;
  24. pre?: true;
  25. ns?: string;
  26. // ...省略
  27. };
  28. declare type ASTExpression = {
  29. type: 2;
  30. expression: string;
  31. text: string;
  32. tokens: Array<string | Object>;
  33. static?: boolean;
  34. // 2.4 ssr optimization
  35. ssrOptimizability?: number;
  36. start?: number;
  37. end?: number;
  38. };
  39. declare type ASTText = {
  40. type: 3;
  41. text: string;
  42. static?: boolean;
  43. isComment?: boolean;
  44. // 2.4 ssr optimization
  45. ssrOptimizability?: number;
  46. start?: number;
  47. end?: number;
  48. };

optimize

  1. const genStaticKeysCached = cached(genStaticKeys)
  2. export function optimize (root: ?ASTElement, options: CompilerOptions) {
  3. if (!root) return
  4. isStaticKey = genStaticKeysCached(options.staticKeys || '')
  5. isPlatformReservedTag = options.isReservedTag || no
  6. // first pass: mark all non-static nodes.
  7. markStatic(root)
  8. // second pass: mark static roots.
  9. markStaticRoots(root, false)
  10. }
  1. // 标记所有的静态和非静态结点
  2. function markStatic (node: ASTNode) {
  3. node.static = isStatic(node)
  4. if (node.type === 1) {
  5. // do not make component slot content static. this avoids
  6. // 1. components not able to mutate slot nodes
  7. // 2. static slot content fails for hot-reloading
  8. if (
  9. !isPlatformReservedTag(node.tag) && // 是指node.tag不是保留标签,即我们自定义的标签时返回true
  10. node.tag !== 'slot' &&
  11. node.attrsMap['inline-template'] == null // 是指node不是一个内联模板容器
  12. ) {
  13. return
  14. }
  15. for (let i = 0, l = node.children.length; i < l; i++) {
  16. const child = node.children[i]
  17. markStatic(child) // 递归标记子节点
  18. if (!child.static) {
  19. node.static = false // 如果子节点有不是静态的,当前结点node.static = false
  20. }
  21. }
  22. }
  23. }
  1. function isStatic (node: ASTNode): boolean {
  2. if (node.type === 2) { // expression 表达式
  3. return false
  4. }
  5. if (node.type === 3) { // text 静态文本
  6. return true
  7. }
  8. return !!(node.pre || ( // 元素上有v-pre指令,结点的子内容是不做编译的,返回true
  9. !node.hasBindings && // no dynamic bindings 结点没有动态属性,即没有任何指令、数据绑定、事件绑定等
  10. !node.if && !node.for && // not v-if or v-for or v-else
  11. !isBuiltInTag(node.tag) && // not a built-in 不是内置的标签,内置的标签有slot和component
  12. isPlatformReservedTag(node.tag) && // not a component 是平台保留标签,即HTML或SVG标签
  13. !isDirectChildOfTemplateFor(node) && // 不是template标签的直接子元素且没有包含在for循环中
  14. Object.keys(node).every(isStaticKey)
  15. ))
  16. }
  • 无动态绑定
  • 没有 v-if 和 v-for 指令
  • 不是内置的标签
  • 是平台保留标签(html和svg标签)
  • 不是 template 标签的直接子元素并且没有包含在 for 循环中
  • 结点包含的属性只能有isStaticKey中指定的几个

    1. function markStaticRoots (node: ASTNode, isInFor: boolean) { // 标示ast是否在for循环中
    2. if (node.type === 1) {
    3. // 如果node.static为true,则会添加node.staticInFor
    4. if (node.static || node.once) {
    5. node.staticInFor = isInFor
    6. }
    7. // For a node to qualify as a static root, it should have children that
    8. // are not just static text. Otherwise the cost of hoisting out will
    9. // outweigh the benefits and it's better off to just always render it fresh.
    10. if (node.static && node.children.length && !(
    11. node.children.length === 1 &&
    12. node.children[0].type === 3
    13. )) {
    14. node.staticRoot = true
    15. return
    16. } else {
    17. node.staticRoot = false
    18. }
    19. if (node.children) {
    20. for (let i = 0, l = node.children.length; i < l; i++) {
    21. markStaticRoots(node.children[i], isInFor || !!node.for)
    22. }
    23. }
    24. if (node.ifConditions) {
    25. walkThroughConditionsBlocks(node.ifConditions, isInFor)
    26. }
    27. }
    28. }

    一个节点要成为静态根节点,需要满足以下条件:

  • 自身为静态节点,并且有子节点

  • 子节点不能仅为一个文本节点

为什么要markStaticRoots,markStatic不够吗

  1. export function renderStatic (
  2. index: number,
  3. isInFor: boolean
  4. ): VNode | Array<VNode> {
  5. const cached = this._staticTrees || (this._staticTrees = [])
  6. let tree = cached[index]
  7. // if has already-rendered static tree and not inside v-for,
  8. // we can reuse the same tree.
  9. if (tree && !isInFor) {
  10. return tree
  11. }
  12. // otherwise, render a fresh tree.
  13. tree = cached[index] = this.$options.staticRenderFns[index].call(
  14. this._renderProxy,
  15. null,
  16. this // for render fns generated for functional component templates
  17. )
  18. markStatic(tree, `__static__${index}`, false)
  19. return tree
  20. }

关于这部分的优化是当一个节点staticRoots为true,并且不在v-for中,那么第一次render的时候会对以这个节点为根的子树进行缓存,等到下次再render的时候直接从缓存中拿,避免再次render。所以标记静态根节点是为了缓存一棵子树用的。

vue3 compiler

image.png

ast

baseParse处理后

  1. export interface RootNode extends Node {
  2. type: NodeTypes.ROOT
  3. children: TemplateChildNode[]
  4. helpers: symbol[]
  5. components: string[]
  6. directives: string[]
  7. hoists: (JSChildNode | null)[]
  8. imports: ImportItem[]
  9. cached: number
  10. temps: number
  11. ssrHelpers?: symbol[]
  12. codegenNode?: TemplateChildNode | JSChildNode | BlockStatement
  13. // v2 compat only
  14. filters?: string[]
  15. }
  16. {
  17. cached: 0,
  18. children: [{…}],
  19. codegenNode: undefined,
  20. components: [],
  21. directives: [],
  22. helpers: [],
  23. hoists: [],
  24. imports: [],
  25. loc: {start: {…}, end: {…}, source: "<div><div>hi vue3</div><div>{{msg}}</div></div>"},
  26. temps: 0,
  27. type: 0
  28. }
  • children 中存放的就是最外层 div 的后代。
  • loc 则用来描述这个 AST Element 在整个字符串(template)中的位置信息。
  • type 则是用于描述这个元素的类型(例如 5 为插值、2 为文本)等等。

可以看到的是不同于「Vue2.x」的 AST,这里我们多了诸如 helpers、codegenNode、hoists 等属性。而,这些属性会在 transform 阶段进行相应地赋值,进而帮助 generate 阶段生成更优的可执行代码。

相比之下,「Vue2.x」的编译阶段没有完整的 transform,只是 optimize 优化了一下 AST

transform

  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 = [...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. if (__COMPAT__) {
  19. root.filters = [...context.filters!]
  20. }
  21. }

PatchFlags

  1. export const enum PatchFlags {
  2. // 动态文本节点
  3. TEXT = 1,
  4. // 动态 class
  5. CLASS = 1 << 1, // 2
  6. // 动态 style
  7. STYLE = 1 << 2, // 4
  8. // 动态属性,但不包含类名和样式
  9. // 如果是组件,则可以包含类名和样式
  10. PROPS = 1 << 3, // 8
  11. // 具有动态 key 属性,当 key 改变时,需要进行完整的 diff 比较。
  12. FULL_PROPS = 1 << 4, // 16
  13. // 带有监听事件的节点
  14. HYDRATE_EVENTS = 1 << 5, // 32
  15. // 一个不会改变子节点顺序的 fragment
  16. STABLE_FRAGMENT = 1 << 6, // 64
  17. // 带有 key 属性的 fragment 或部分子字节有 key
  18. KEYED_FRAGMENT = 1 << 7, // 128
  19. // 子节点没有 key 的 fragment
  20. UNKEYED_FRAGMENT = 1 << 8, // 256
  21. // 一个节点只会进行非 props 比较
  22. NEED_PATCH = 1 << 9, // 512
  23. // 动态 slot
  24. DYNAMIC_SLOTS = 1 << 10, // 1024
  25. // 静态节点
  26. HOISTED = -1,
  27. // 指示在 diff 过程应该要退出优化模式
  28. BAIL = -2
  29. }

generate

generate是 compile 阶段的最后一步,它的作用是将 transform转换后的 AST 生成对应的可执行代码,从而在之后 Runtime 的 Render阶段时,就可以通过可执行代码生成对应的 VNode Tree,然后最终映射为真实的 DOM Tree 在页面上。

  1. export function generate(
  2. ast: RootNode,
  3. options: CodegenOptions & {
  4. onContextCreated?: (context: CodegenContext) => void
  5. } = {}
  6. ): CodegenResult {
  7. const context = createCodegenContext(ast, options)
  8. if (options.onContextCreated) options.onContextCreated(context)
  9. const {
  10. mode,
  11. push,
  12. prefixIdentifiers,
  13. indent,
  14. deindent,
  15. newline,
  16. scopeId,
  17. ssr
  18. } = context
  19. // ........太长,删了
  20. if (isSetupInlined) {
  21. push(`(${signature}) => {`)
  22. } else {
  23. push(`function ${functionName}(${signature}) {`)
  24. }
  25. indent()
  26. // ........太长,删了
  27. return {
  28. ast,
  29. code: context.code,
  30. preamble: isSetupInlined ? preambleContext.code : ``,
  31. // SourceMapGenerator does have toJSON() method but it's not in the types
  32. map: context.map ? (context.map as any).toJSON() : undefined
  33. }
  34. }

执行 genHoists(ast.hoists, context),将 transform 生成的静态节点数组 hoists 作为第一个参数

  1. // genHoists(ast.hoists, context)
  2. hoists.forEach((exp, i) => {
  3. if (exp) {
  4. push(`const _hoisted_${i + 1} = `);
  5. genNode(exp, context);
  6. newline();
  7. }
  8. })
  1. const _hoisted_1 = { name: "test" }
  2. const _hoisted_2 = /*#__PURE__*/_createTextVNode(" 一个文本节点 ")
  3. const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "good job!", -1 /* HOISTED */)

render function

静态提升 hoistStatic

  • Vue2中的虚拟dom是进行全量的对比,即数据更新后在虚拟DOM中每个标签内容都会对比有没有发生变化
  • Vue3新增了静态标记(PatchFlag)
    • 在创建虚拟DOM的时候会根据DOM中的内容会不会发生变化添加静态标记
    • 数据更新后,只对比带有patch flag的节点

image.png
https://vue-next-template-explorer.netlify.app/

  1. <div id="foo" class="bar">
  2. {{ text }}
  3. </div>
  1. // 没有静态提升
  2. export function render(_ctx, _cache, $props, $setup, $data, $options) {
  3. return (_openBlock(), _createElementBlock("div", {
  4. id: "foo",
  5. class: "bar"
  6. }, _toDisplayString(_ctx.text), 1 /* TEXT */))
  7. }
  8. // props属性进行静态提升
  9. const _hoisted_1 = {
  10. id: "foo",
  11. class: "bar"
  12. }
  13. export function render(_ctx, _cache, $props, $setup, $data, $options) {
  14. return (_openBlock(), _createElementBlock("div", _hoisted_1, _toDisplayString(_ctx.text), 1 /* TEXT */))
  15. }
  1. <div>
  2. <div>
  3. <span class="foo"></span>
  4. <span class="foo"></span>
  5. </div>
  6. </div>
  1. // 没有静态提升的情况
  2. export function render(_ctx, _cache, $props, $setup, $data, $options) {
  3. return (_openBlock(), _createElementBlock("div", null, [
  4. _createElementVNode("div", null, [
  5. _createElementVNode("span", { class: "foo" }),
  6. _createElementVNode("span", { class: "foo" })
  7. ])
  8. ]))
  9. }
  10. // 静态提升
  11. const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, [
  12. /*#__PURE__*/_createElementVNode("span", { class: "foo" }),
  13. /*#__PURE__*/_createElementVNode("span", { class: "foo" })
  14. ], -1 /* HOISTED */)
  15. const _hoisted_2 = [
  16. _hoisted_1
  17. ]
  18. export function render(_ctx, _cache, $props, $setup, $data, $options) {
  19. return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
  20. }
  1. <div>
  2. <div>
  3. <span class="foo"></span>
  4. <span class="foo"></span>
  5. <span class="foo"></span>
  6. <span class="foo"></span>
  7. <span class="foo"></span>
  8. </div>
  9. </div>
  1. // StaticVNode 试了下,四个节点不会触发
  2. const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<div><span class=\"foo\"></span><span class=\"foo\"></span><span class=\"foo\"></span><span class=\"foo\"></span><span class=\"foo\"></span></div>", 1)
  3. const _hoisted_2 = [
  4. _hoisted_1
  5. ]
  6. export function render(_ctx, _cache, $props, $setup, $data, $options) {
  7. return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
  8. }

事件监听缓存 cacheHandlers

  1. <div>
  2. <button id="btn" @click="onClick">按钮</button>
  3. </div>
  1. // 关闭事件监听缓存
  2. export function render(_ctx, _cache, $props, $setup, $data, $options) {
  3. return (_openBlock(), _createElementBlock("div", null, [
  4. _createElementVNode("button", {
  5. id: "btn",
  6. onClick: _ctx.onClick
  7. }, "按钮", 8 /* PROPS */, ["onClick"])
  8. ]))
  9. }
  10. // 开启事件监听缓存
  11. export function render(_ctx, _cache, $props, $setup, $data, $options) {
  12. return (_openBlock(), _createElementBlock("div", null, [
  13. _createElementVNode("button", {
  14. id: "btn",
  15. onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.onClick && _ctx.onClick(...args)))
  16. }, "按钮")
  17. ]))
  18. }

vue3通过语法分析,可得知
1、id是个静态的prop,可以跳过,onClick是动态props,所以后面有标记["onClick"]
2、不开启事件监听缓存的情况,onClick会被视为props,所以会有标记,8 /* PROPS */, ["onClick"]
开启事件监听缓存,onClick会被缓存到_cache中