7

本文基于Vue 3.2.30版本源码进行分析

为了增加可读性,会对源码进行删减、调整顺序、改变部分分支条件的操作,文中所有源码均可视作为伪代码
由于ts版本代码携带参数过多,不利于展示,大部分伪代码会取编译后的js代码,而不是原来的ts代码

文章内容

《Vue3源码-整体渲染流程》这篇文章中,我们可以知道,一开始会将我们编写的<template></template>内容最终会被编译成为render()函数,render()函数中执行createBaseVNode(_createElementVNode)/createVNode(_createVNode)返回渲染结果形成vnode数据,本文将分析和总结:

  • 从源码的角度,分析运行时编译<template></template>render()的流程
  • 从源码的角度,进行Vue3相关编译优化的总结
  • 从源码的角度,进行Vue2相关编译优化的总结

前置知识

单文件组件

Vue 的单文件组件 (即 *.vue 文件,英文 Single-File Component,简称 SFC) 是一种特殊的文件格式,使我们能够将一个 Vue 组件的模板、逻辑与样式封装在单个文件中,如:

<script>
  export default {
    data() {
      return {
        greeting: 'Hello World!'
      }
    }
  }
</script>

<template>
  <p class="greeting">{{ greeting }}</p>
</template>

<style>
  .greeting {
    color: red;
    font-weight: bold;
  }
</style>

Vue SFC 是一个框架指定的文件格式,因此必须交由 @vue/compiler-sfc 编译为标准的 JavaScript 和 CSS,一个编译后的 SFC 是一个标准的 JavaScript(ES) 模块,这也意味着在构建配置正确的前提下,你可以像导入其他 ES 模块一样导入 SFC:

import MyComponent from './MyComponent.vue'

export default {
  components: {
    MyComponent
  }
}

在实际项目中,我们一般会使用集成了 SFC 编译器的构建工具,比如 Vite 或者 Vue CLI (基于 webpack)

运行时编译模板

当以无构建步骤方式使用 Vue 时,组件模板要么是写在页面的 HTML 中,或者是内联的 JavaScript 字符串。在这些场景中,为了执行动态模板编译,Vue 需要将模板编译器运行在浏览器中。相对的,如果我们使用了构建步骤,由于提前编译了模板,那么就无须再在浏览器中运行了。为了减小打包出的客户端代码体积,Vue 提供了多种格式的“构建文件”以适配不同场景下的优化需求:

  • 前缀为 vue.runtime.* 的文件是只包含运行时的版本:不包含编译器,当使用这个版本时,所有的模板都必须由构建步骤预先编译
  • 名称中不包含 .runtime 的文件则是完全版:即包含了编译器,并支持在浏览器中直接编译模板。然而,体积也会因此增长大约 14kb

截屏2023-05-04 00.04.08.png

如上图所示,vue3源码最后打包生成的文件中,有vue.xxx.jsvue.runtime.xxx.js,比如vue.global.jsvue.runtime.global.js

注意:名称中不包含 .runtime 的文件包含了编译器,并支持在浏览器中直接编译模板,仅仅是支持编译模板,不是支持编译.vue文件,.vue文件还是需要构建工具,比如webpack集成vue-loader解析.vue文件

vue.runtime.global.js中,我们可以发现这样的注释,如下所示,该版本不支持运行时编译

  const compile$1 = () => {
      {
          warn$1(`Runtime compilation is not supported in this build of Vue.` +
              (` Use "vue.global.js" instead.`
                          ) /* should not happen */);
      }
  };

vue.global.js中,如下面代码所示,会进行compile方法的注册

function registerRuntimeCompiler(_compile) {
    compile = _compile;
    installWithProxy = i => {
        if (i.render._rc) {
            i.withProxy = new Proxy(i.ctx, RuntimeCompiledPublicInstanceProxyHandlers);
        }
    };
}
registerRuntimeCompiler(compileToFunction);

在之前的文章Vue3源码-整体渲染流程浅析中,如下图所示,在触发渲染时,会触发finishComponentSetup()的执行

Vue3首次渲染-整体流程.svg

finishComponentSetup()中,会检测是否存在instance.render以及compile()方法是否存在,如果不存在instance.render并且compile()方法存在,则会触发compile()方法执行编译流程

 function finishComponentSetup(instance, isSSR, skipOptions) {
      const Component = instance.type;
      // template / render function normalization
      // could be already set when returned from setup()
      if (!instance.render) {
          // only do on-the-fly compile if not in SSR - SSR on-the-fly compilation
          // is done by server-renderer
          if (!isSSR && compile && !Component.render) {
              const template = Component.template;
              if (template) {
                  {
                      startMeasure(instance, `compile`);
                  }
                  const { isCustomElement, compilerOptions } = instance.appContext.config;
                  const { delimiters, compilerOptions: componentCompilerOptions } = Component;
                  const finalCompilerOptions = extend(extend({
                      isCustomElement,
                      delimiters
                  }, compilerOptions), componentCompilerOptions);
                  Component.render = compile(template, finalCompilerOptions);
                  {
                      endMeasure(instance, `compile`);
                  }
              }
          }
          instance.render = (Component.render || NOOP);
          // for runtime-compiled render functions using `with` blocks, the render
          // proxy used needs a different `has` handler which is more performant and
          // also only allows a whitelist of globals to fallthrough.
          if (installWithProxy) {
              installWithProxy(instance);
          }
      }
  }
接下来我们将针对compile()方法执行编译流程进行具体的源码分析

1. Vue3的编译流程

不会执迷于具体的编译细节,只是对一个整体的流程做一个分析

1.1 整体概述

从下面的代码块可以知道,整个编译流程只要经历了三个阶段

  • baseParse():转化<template></template>ast语法树
  • transform():转化ast数据
  • generate():根据上面两个流程的ast数据生成render()函数
function compile(template, options = {}) {
    return baseCompile(template, extend({}, parserOptions, options, { ...}));
}
function baseCompile(template, options = {}) {
    const ast = isString(template) ? baseParse(template, options) : template;
    transform(ast, extend({}, options, { ...}));
    return generate(ast, extend({}, options, { ...}));
}

注:抽象语法树(abstract syntax tree或者缩写为AST),是源代码的抽象语法结构的树状表现形式,比如下面代码块,它可以描述一段HTML的语法结构

const ast = {
    type: 0,
    children: [
        {
            type: 1,
            tag: "div",
            props:[{type: 3, name: "class", value: {}}],
            children: []
        },
        {
            type: 1,
            content: "这是一段文本"
        }
    ],
    loc: {}
}

1.2 baseParse():生成AST

function baseParse(content, options = {}) {
    const context = createParserContext(content, options);
    const start = getCursor(context);
    const root = parseChildren(context, 0 /* DATA */, []);
    return createRoot(root, getSelection(context, start));
}

1.2.1 createParserContext():创建上下文context

本质上是创建一个类似全局的上下文context,将一些方法和状态放入这个context中,然后将这个context传入每一个构建AST的方法中,后面每一次子节点的AST构建,都会维护和更新这个context以及使用它所声明的一些基础方法

function createParserContext(content, rawOptions) {
    const options = extend({}, defaultParserOptions);
    let key;
    for (key in rawOptions) {
        options[key] = rawOptions[key] === undefined
                ? defaultParserOptions[key]
                : rawOptions[key];
    }
    return {
        options,
        column: 1,
        line: 1,
        offset: 0,
        source: content
    };
}
function getCursor(context) {
    const { column, line, offset } = context;
    return { column, line, offset };
}

1.2.2 parseChildren-根据不同情况构建nodes数组

parseChildren主要是根据不同的条件,比如以"<"开头、以">"结尾,比如以"<"开头,以"/>"结尾,比如以"<"开头,以"/>"结尾,然后构建出对应的node数组存放在nodes数组中,最后再处理空白字符

整体概述
function parseChildren(context, mode, ancestors) {
    const parent = last(ancestors);
    const ns = parent ? parent.ns : 0 /* HTML */;
    const nodes = [];
    while (!isEnd(context, mode, ancestors)) {
        const s = context.source;
        let node = undefined;
        if (mode === 0 /* DATA */ || mode === 1 /* RCDATA */) {
            // 根据不同条件情况去调用不同的解析方法
            node = xxxxxx
        }
        if (!node) {
            node = parseText(context, mode);
        }
        if (isArray(node)) {
            for (let i = 0; i < node.length; i++) {
                pushNode(nodes, node[i]);
            }
        }
        else {
            pushNode(nodes, node);
        }
    }

    // ...省略处理nodes每一个元素空白字符的逻辑

    return removedWhitespace ? nodes.filter(Boolean) : nodes;
}

如下面流程图所示,主要是根据不同的条件,调用不同的方法进行node的构建,主要分为:

  • 标签以及标签对应的属性解析,比如<div class="test1"></div>
  • 插值的解析,如{{refCount.count}}
  • 注释节点的解析,如<!-- 这是一段注释内容 -->
  • 纯文本内容的解析,如<div>我是一个纯文本</div>中的我是一个纯文本

Vue3编译流程.svg

在经过不同条件的解析之后,我们可以得到每一种类型的AST数据,比如下面代码块所示,最终<template></template>中的每一个元素都会转化为一个AST节点数据

AST节点会根据<template></template>的父子关系进行数据的构建,比如children也是一个AST数组集合
{
    type: 1,
    tag, 
    tagType,
    props,
    children: []
}
为了更好理解每一个条件的处理逻辑,我们下面将对其中一种情况调用的方法parseElement()进行具体的分析
parseElement分析
暂时移除Pre和VPre相关逻辑

这个方法是为了处理<div class="test1"><p></p><span></span>,为了处理这种标签,我们一步一步地解析

  • 处理Start tag:使用parseTag()处理<div class="test1">,包括标签名div、属性值class="test1"
  • 处理childrenancestors压入当前的element(为了后续解析时能正确建立父子关系),然后递归调用parseChildren()处理<p></p><span></span>,获取nodes数组,弹出ancestors压入的element,将nodes赋值给element.children
  • 处理End tag:检测end Tag是否跟element.tag对应,如果可以对应,则更新context存储的解析状态(类似指针一样,将指针后移到没有解析的部分)
function parseElement(context, ancestors) {
    // Start tag.
    const parent = last(ancestors); //ancestors[ancestors.length - 1];
    const element = parseTag(context, 0 /* Start */, parent);
    if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
        return element;
    }
    // Children.
    ancestors.push(element);
    const mode = context.options.getTextMode(element, parent);
    const children = parseChildren(context, mode, ancestors);
    ancestors.pop();
    element.children = children;
    // End tag.
    if (startsWithEndTagOpen(context.source, element.tag)) {
        parseTag(context, 1 /* End */, parent);
    } else {
        // 找不到endTag,进行错误的emit
    }
    element.loc = getSelection(context, element.loc.start);
    return element;
}
处理空白字符

主要使用正则表达式进行空白字符的剔除

空白字符也是一个文本内容,剔除空白字符可以减少后续对这些没有意义内容的编译处理效率
function parseChildren(context, mode, ancestors) {
    // ...省略构建nodes数组的逻辑

    // Whitespace handling strategy like v2
    let removedWhitespace = false;
    if (mode !== 2 /* RAWTEXT */ && mode !== 1 /* RCDATA */) {
        const shouldCondense = context.options.whitespace !== 'preserve';
        for (let i = 0; i < nodes.length; i++) {
            const node = nodes[i];
            // 删除空白字符...              
        }
        if (context.inPre && parent && context.options.isPreTag(parent.tag)) {
            // 根据 html 规范删除前导换行符
            const first = nodes[0];
            if (first && first.type === 2 /* TEXT */) {
                first.content = first.content.replace(/^\r?\n/, '');
            }
        }
    }
    return removedWhitespace ? nodes.filter(Boolean) : nodes;
}

1.2.3 createRoot-返回root对象的AST数据

从上面parseChildren()获取成功nodes数组后,作为children参数传入到createRoot(),构建出一个ROOTAST对象返回

function baseParse(content, options = {}) {
    const context = createParserContext(content, options);
    const start = getCursor(context);
    const root = parseChildren(context, 0 /* DATA */, []);
    return createRoot(root, getSelection(context, start));
}
function createRoot(children, loc = locStub) {
    return {
        type: 0 /* ROOT */,
        children,
        helpers: [],
        components: [],
        directives: [],
        hoists: [],
        imports: [],
        cached: 0,
        temps: 0,
        codegenNode: undefined,
        loc
    };
}

1.3 transform()

从一开始的编译阶段,如下面的代码块所示,我们可以知道,当我们执行完成ast的构建,我们会执行transform()进行ast的转化工作

function baseCompile(template, options = {}) {
    const ast = ...

    const [nodeTransforms, directiveTransforms] = getBaseTransformPreset();
    transform(ast, extend({}, options, {
        prefixIdentifiers,
        nodeTransforms: [
            ...nodeTransforms,
            ...(options.nodeTransforms || []) // user transforms
        ],
        directiveTransforms: extend({}, directiveTransforms, options.directiveTransforms || {} // user transforms
        )
    }));

    return ...
}
function getBaseTransformPreset(prefixIdentifiers) {
    return [
        [
            transformOnce,
            transformIf,
            transformMemo,
            transformFor,
            ...([]),
            ...([transformExpression]
            ),
            transformSlotOutlet,
            transformElement,
            trackSlotScopes,
            transformText
        ],
        {
            on: transformOn,
            bind: transformBind,
            model: transformModel
        }
    ];
}

1.3.1 整体概述

在经过baseParse()<template></template>转化为基础的AST数据后,会进入一个AST语法分析阶段

在这个语法分析阶段中,我们会为AST数据进行转化,为其添加编译优化相关的属性,比如静态提升、Block的构建等等

语法分析阶段会构建出信息更加丰富的AST数据,为后面的generate()render()函数的生成)做准备

function transform(root, options) {
    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;
}

1.3.2 createTransformContext()

建立一个transform的上下文,本质也是一个贯穿transform()流程的一个全局变量,用来提供全局的辅助方法和存储对应的状态

其中nodeTransforms, directiveTransforms是在baseCompile()中通过getBaseTransformPreset()得到的节点和指令转化辅助方法,比如transformIfon:transformOn等辅助方法,对应<template></template>v-ifv-on形成的AST基础数据的transform的转化
function createTransformContext(...) {
    const context = {
        //...
        hoists: [],
        parent: null,
        currentNode: root,
        nodeTransforms, 
          directiveTransforms,
        // methods
        helper(name) {
            const count = context.helpers.get(name) || 0;
            context.helpers.set(name, count + 1);
            return name;
        },
        replaceNode(node) {
            context.parent.children[context.childIndex] = context.currentNode = node;
        }
        //...
    };
    return context;
}

1.3.3 traverseNode()

如下面代码块所示,traverseNode()逻辑主要分为三个部分:

  • 执行nodeTransforms函数进行node数据的处理,会根据目前node的类型,比如是v-for而触发对应的transformFor()转化方法,执行完毕后,拿到对应的onExit方法,存入数组中
  • 处理node.children,更新context.parentcontext.childIndex,然后递归调用traverseNode(childNode)
  • 处理一些node需要等待children处理完毕后,再处理的情况,调用parentNodeonExit方法(onExit方法也就是第一部分我们缓存的数组)
function transform(root, options) {
    const context = createTransformContext(root, options);
    traverseNode(root, context);
    if (options.hoistStatic) {
        hoistStatic(root, context);
    }
    if (!options.ssr) {
        createRootCodegen(root, context);
    }
    //...
}
function traverseNode(node, context) {
    context.currentNode = node;
    const { nodeTransforms } = context;
    for (let i = 0; i < nodeTransforms.length; i++) {
        // 执行nodeTransforms函数进行node数据的处理
        const onExit = nodeTransforms[i](node, context);
        // 缓存onExit方法
        // 处理被删除或者被替换的情况
    }
  
    // 根据不同条件处理node.children,本质是递归调用traverseNode()
    traverseChildren(node, context); //node.type=IF_BRANCH/FOR/ELEMENT/ROOT

    // 处理一些node需要等待children处理完毕后,再处理的情况
    // 即调用parentNode的onExit方法
    context.currentNode = node;
    let i = exitFns.length;
    while (i--) {
        exitFns[i]();
    }
}
traverseNode第1部分:nodeTransforms
nodeTransforms到底是什么东西?又是如何找到指定的node?

在一开始的初始化中,我们就可以知道,会默认初始化getBaseTransformPreset(),如下面代码块所示,会初始化几个类型的处理方法

function baseCompile(template, options = {}) {
    const ast = ...

    const [nodeTransforms, directiveTransforms] = getBaseTransformPreset();
}
function getBaseTransformPreset(prefixIdentifiers) {
    return [
        [
            transformOnce,
            transformIf,
            transformMemo,
            transformFor,
            ...([]),
            ...([transformExpression]
            ),
            transformSlotOutlet,
            transformElement,
            trackSlotScopes,
            transformText
        ],
        {
            on: transformOn,
            bind: transformBind,
            model: transformModel
        }
    ];
}

我们随机取一个,比如transformFor这个处理方法

const transformFor = createStructuralDirectiveTransform('for', (node, dir, context) => {
    const { helper, removeHelper } = context;
    return processFor(node, dir, context, forNode => {
       
        return () => {
          
        };
    });
});

从下面代码块中,我们可以发现,每一个处理方法都会传入一个name字符串,而只有node.prop.name跟处理方法传入的name匹配上(matches(prop.name))才会执行createStructuralDirectiveTransform()的第二个参数fn,也就是上面代码块中的return processFor(xxx,xxx,xxx)所返回的方法

function createStructuralDirectiveTransform(name, fn) {
    // name = "for"
    const matches = isString(name)
        ? (n) => n === name
        : (n) => name.test(n);
    return (node, context) => {
        if (node.type === 1 /* ELEMENT */) {
            const { props } = node;
            if (node.tagType === 3 /* TEMPLATE */ && props.some(isVSlot)) {
                return;
            }
            const exitFns = [];
            for (let i = 0; i < props.length; i++) {
                const prop = props[i];
                if (prop.type === 7 /* DIRECTIVE */ && matches(prop.name)) {
                    props.splice(i, 1);
                    i--;
                    const onExit = fn(node, prop, context);
                    if (onExit)
                        exitFns.push(onExit);
                }
            }
            return exitFns;
        }
    };
}

而执行processFor()最后一个参数processCodegen也是一个方法,从下面两个代码块,我们可以发现processFor的执行顺序为

  • 进行forNode的数据的初始化,然后调用processCodegen()方法触发createVNodeCall()构建forNode.codegenNode属性
  • 使用context.replaceNode(forNode)替换当前的node对应的AST基础数据
  • 最后返回const onExit = nodeTransforms[i](node, context)对应的onExit方法,进行缓存
在下面traverseNode第3部分的逻辑中,会执行上面缓存的onExit方法,实际上调用的是processForforNode参数中所对应的返回的箭头函数,如下面所示
const transformFor = createStructuralDirectiveTransform('for', (node, dir, context) => {
    const { helper, removeHelper } = context;
    // processCodegen()就是forNode=>{}
    return processFor(node, dir, context, forNode => {
        // 下面代码块processFor()执行完成后,在执行这个createVNodeCall()方法构建forNode.codegenNode
        forNode.codegenNode = createVNodeCall(context, helper(FRAGMENT), ...);
        return () => {
            // 实际onExit方法执行的地方
        };
    });
});
function processFor(node, dir, context, processCodegen) {
    const parseResult = parseForExpression(dir.exp, context);
    const { source, value, key, index } = parseResult;
    const forNode = {
        type: 11 /* FOR */,
        loc: dir.loc,
        source,
        ...
        parseResult,
        children: isTemplateNode(node) ? node.children : [node]
    };
    //context.parent.children[context.childIndex] = context.currentNode = node;
    context.replaceNode(forNode);
    scopes.vFor++;
    const onExit = processCodegen && processCodegen(forNode);
    return () => {
        scopes.vFor--;
        if (onExit)
            onExit();
    };
}

function createVNodeCall(...) {
    //...
    return {
        type: 13 /* VNODE_CALL */,
        tag,
        props,
        children,
        patchFlag,
        dynamicProps,
        directives,
        isBlock,
        disableTracking,
        isComponent,
        loc
    };
}
traverseNode第2部分:traverseChildren

根据不同条件处理node.children,当node.type=IF_BRANCH/FOR/ELEMENT/ROOT时调用traverseChildren(),本质上是递归调用traverseNode()

function traverseNode(node, context) {
    //...
    switch (node.type) {
        case 3 /* COMMENT */:
            if (!context.ssr) {
                context.helper(CREATE_COMMENT);
            }
            break;
        case 5 /* INTERPOLATION */:
            if (!context.ssr) {
                context.helper(TO_DISPLAY_STRING);
            }
            break;
        case 9 /* IF */:
            for (let i = 0; i < node.branches.length; i++) {
                traverseNode(node.branches[i], context);
            }
            break;
        case 10 /* IF_BRANCH */:
        case 11 /* FOR */:
        case 1 /* ELEMENT */:
        case 0 /* ROOT */:
            traverseChildren(node, context);
            break;
    }
    //...
}
function traverseChildren(parent, context) {
    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);
    }
}
traverseNode第3部分:exitFns

由下面代码块可以知道,nodeTransforms()会返回一个onExit()方法,然后在处理完成node.children后,再进行逐步调用

function traverseNode(node, context) {
    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);
            }
        }
    }
    //...省略处理node.children的逻辑
    
    // exit transforms
    context.currentNode = node;
    let i = exitFns.length;
    while (i--) {
        exitFns[i]();
    }
}

而这种onExit()执行的内容,则根据不同条件而不同,正如我们上面使用transformFor作为nodeTransforms[i]其中一个方法分析一样,最终的onExit()执行的内容就是processForforNode方法中所返回的方法所执行的内容

这里不再详细分析每一个nodeTransforms[i]所对应的onExit()执行的内容以及作用,请读者自行参考其它文章
const transformFor = createStructuralDirectiveTransform('for', (node, dir, context) => {
    const { helper, removeHelper } = context;
    return processFor(node, dir, context, forNode => {
        // 下面代码块processFor()执行完成后,在执行这个createVNodeCall()方法构建forNode.codegenNode
        forNode.codegenNode = createVNodeCall(context, helper(FRAGMENT), ...);
        return () => {
            // 实际onExit方法执行的地方
        };
    });
});

1.3.4 hoistStatic()

function transform(root, options) {
    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;
}
function hoistStatic(root, context) {
    walk(root, context,
        // Root node is unfortunately non-hoistable due to potential parent
        // fallthrough attributes.
        isSingleElementRoot(root, root.children[0]));
}
getConstantType()返回一共有4种类型,为0(NOT_CONSTANT)、1(CAN_SKIP_PATCH)、2(CAN_HOIST)、3(CAN_STRINGIFY)

walk()方法会进行所有node数据的遍历,然后根据条件判断是否需要静态提升,根据下面的代码块,需要条件判断的情况可以总结为:

  • 根结点不会静态提升
  • 如果node数据是一个ELEMENT类型,会通过getConstantType()获取常量类型

    • 如果getConstantType()>=2,则进行静态提升child.codegenNode = context.hoist(child.codegenNode)
    • 如果getConstantType()返回0node数据不能进行静态提升(可能它的children存在动态改变的数据等等),检测它的props是否可以静态提升
  • 如果node数据是一个文本类型,则可以进行静态提升
  • 如果node数据是v-for类型,并且只有一个child,不进行静态提升,因为它必须是一个Block
  • 如果node数据是v-if类型,并且只有一个child,不进行静态提升,因为它必须是一个Block
function walk(node, context, doNotHoistNode = false) {
    const { children } = node;
    const originalCount = children.length;
    let hoistedCount = 0;
    for (let i = 0; i < children.length; i++) {
        const child = children[i];
        if (child.type === 1 /* ELEMENT */ && child.tagType === 0 /* ELEMENT */) {
           // constantType>=2,可以静态提升,执行下面语句
           child.codegenNode.patchFlag = -1 /* HOISTED */ + (` /* HOISTED */`);
           child.codegenNode = context.hoist(child.codegenNode);
          
          // constantType === 0,本身无法静态提升,但是props可能可以
          // ...省略检测node.props能否静态提升的代码

        } else if (child.type === 12 /* TEXT_CALL */ && 
                   getConstantType(child.content, context) >= 2 /* CAN_HOIST */) {
            // 文本内容
            //...省略静态提升的代码
        }
        // 递归调用walk,进行child.children的静态提升
        if (child.type === 1 /* ELEMENT */) {
            walk(child, context);
        } else if (child.type === 11 /* FOR */) {
            // 如果for只有一个children,doNotHoistNode置为true,不进行静态提升
            walk(child, context, child.children.length === 1);
        } else if (child.type === 9 /* IF */) {
            for (let i = 0; i < child.branches.length; i++) {
                // 如果if只有一个children,doNotHoistNode置为true,不进行静态提升
                walk(child.branches[i], context, child.branches[i].children.length === 1);
            }
        }
    }
    // 所有node.children[i].codegenNode如果都静态提升,node的codegenNode.children也全部进行静态提升
    if (hoistedCount &&
        hoistedCount === originalCount &&
        node.type === 1 /* ELEMENT */ &&
        node.tagType === 0 /* ELEMENT */ &&
        node.codegenNode &&
        node.codegenNode.type === 13 /* VNODE_CALL */ &&
        isArray(node.codegenNode.children)) {
        node.codegenNode.children = context.hoist(createArrayExpression(node.codegenNode.children));
    }
}

而静态提升调用的是context.hoist,从下面的代码块可以知道,本质是利用createSimpleExpression()创建了新的对象数据,然后更新目前的node.codegenNode数据

// child.codegenNode = context.hoist(child.codegenNode);
hoist(exp) {
    if (isString(exp))
        exp = createSimpleExpression(exp);
    context.hoists.push(exp);
    const identifier = createSimpleExpression(`_hoisted_${context.hoists.length}`, false, exp.loc, 2 /* CAN_HOIST */);
    identifier.hoisted = exp;
    return identifier;
}
function createSimpleExpression(content, isStatic = false, loc = locStub, constType = 0 /* NOT_CONSTANT */) {
    return {
        type: 4 /* SIMPLE_EXPRESSION */,
        loc,
        content,
        isStatic,
        constType: isStatic ? 3 /* CAN_STRINGIFY */ : constType
    };
}

1.3.5 createRootCodegen()

function transform(root, options) {
    const context = createTransformContext(root, options);
    traverseNode(root, context);
    if (options.hoistStatic) {
        hoistStatic(root, context);
    }
    if (!options.ssr) {
        createRootCodegen(root, context);
    }
    //...
}
tagType: 0(ELEMENT)、1(COMPONENT)、2(SLOT)、3(TEMPLATE)...

从下面的代码块可以知道,主要是为了root创建对应的codegenNode

  • 如果目前root对应的模板内容<template><div id="id1"></div></template>只有一个childdiv#id1,那么就将其转化为Block,然后将child.codegenNode赋值给root.codegenNode
  • 如果目前root对应的模板内容有多个子节点(Vue3允许一个<template></template>有多个子节点),需要为root创建一个fragment类型的codegenNode代码
function createRootCodegen(root, context) {
    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 === 13 /* 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 = 64 /* STABLE_FRAGMENT */;
        let patchFlagText = PatchFlagNames[64 /* STABLE_FRAGMENT */];
        // check if the fragment actually contains a single valid child with
        // the rest being comments
        if (children.filter(c => c.type !== 3 /* COMMENT */).length === 1) {
            patchFlag |= 2048 /* DEV_ROOT_FRAGMENT */;
            patchFlagText += `, ${PatchFlagNames[2048 /* DEV_ROOT_FRAGMENT */]}`;
        }
        root.codegenNode = createVNodeCall(context, helper(FRAGMENT), undefined, root.children, patchFlag + (` /* ${patchFlagText} */`), undefined, undefined, true, undefined, false /* isComponent */);
    }
}

1.3.6 同步context数据赋值到root中

function transform(root, options) {
    //....
    //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;
}

1.4 generate()

经过transform()后的ast具备更多的属性,可以用来进行代码的生成

function baseCompile(template, options = {}) {
    const ast = isString(template) ? baseParse(template, options) : template;
    const [nodeTransforms, directiveTransforms] = getBaseTransformPreset();
    transform(ast, extend({}, options, {...});

    return generate(ast, extend({}, options, {
        prefixIdentifiers
    }));
}

实际上generate()也是按照从上到下的顺利,一步一步生成代码,虽然代码逻辑并不难懂,但是非常多,为了更好的记忆和理解,将结合示例代码进行分块分析

1.4.1 示例代码

调试的html代码放在github html调试代码, 形成的render()函数如下所示
const _Vue = Vue
const { createVNode: _createVNode, createElementVNode: _createElementVNode, createTextVNode: _createTextVNode } = _Vue

const _hoisted_1 = { id: "app-wrapper" }
const _hoisted_2 = { id: "app-content1" }
const _hoisted_3 = /*#__PURE__*/_createTextVNode(" 这是第一个item ")
const _hoisted_4 = /*#__PURE__*/_createElementVNode("input", { type: "button" }, null, -1 /* HOISTED */)
const _hoisted_5 = /*#__PURE__*/_createElementVNode("div", null, "这是测试第二个item", -1 /* HOISTED */)

return function render(_ctx, _cache) {
    with (_ctx) {
        const { createElementVNode: _createElementVNode, resolveComponent: _resolveComponent, createVNode: _createVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

        const _component_InnerComponent = _resolveComponent("InnerComponent")
        const _component_second_component = _resolveComponent("second-component")

        return (_openBlock(), _createElementBlock("div", _hoisted_1, [
            _createElementVNode("div", _hoisted_2, [
                _hoisted_3,
                _hoisted_4,
                _hoisted_5,
                _createVNode(_component_InnerComponent)
            ]),
            _createVNode(_component_second_component, {
                "stat-fat": {'label': '3333'},
                test: "333"
            }, null, 8 /* PROPS */, ["stat-fat"])
        ]))
    }
}

1.4.2 createCodegenContext

跟上面两个流程一样,都是生成一个贯穿这个流程的上下文,带有一些状态,比如context.code保持着代码生成
当然还带有一些贯穿这个流程的全局辅助方法,比如context.indent()进行缩进的递增等等

function createCodegenContext(ast, {...}) {
    const context = {
        ...
        mode, 
        code: ``,
        column: 1,
        line: 1,
        offset: 0,
        push(code, node) {
            context.code += code;
        },
        indent() {
            newline(++context.indentLevel);
        },
        deindent(withoutNewLine = false) {
            if (withoutNewLine) {
                --context.indentLevel;
            } else {
                newline(--context.indentLevel);
            }
        },
        newline() {
            newline(context.indentLevel);
        }
    };
    function newline(n) {
        context.push('\n' + `  `.repeat(n));
    }
    return context;
}

1.4.3 genFunctionPreamble:生成最外层依赖和静态提升代码

Vue3-编译流程-生成代码1.png

1.4.4 生成渲染函数render、with(Web端运行时编译)、资源声明代码

如下面图所示,由于目前只有ast.components的属性,因此资源声明只会创建Component相关的声明代码,而不会创建ast.directives/ast.temps等相关代码

Vue3-编译流程-生成代码2.png

1.4.5 根据ast.codegenNode进行genNode()代码生成

在上面的分析中,我们目前已经生成到return的位置了,下面会触发genNode()方法进行return后面语句的生成

function generate(ast, options = {}) {
    const context = createCodegenContext(ast, options);
    genFunctionPreamble(ast, preambleContext);
    //...省略其它生成代码
    if (ast.codegenNode) {
        genNode(ast.codegenNode, context);
    } else {
        push(`null`);
    }
    if (useWithBlock) {
        deindent();
        push(`}`);
    }
    deindent();
    push(`}`);
    return {
        ast,
        code: context.code,
        preamble: ``,
        // SourceMapGenerator does have toJSON() method but it's not in the types
        map: context.map ? context.map.toJSON() : undefined
    };
}

从下面代码块可以知道,根据ast.codegenNode进行genNode()代码生成本质也是根据不同的node.type,然后调用不同的生成代码函数,我们示例中触发的是node.type=13genVNodeCall()方法

function genNode(node, context) {
    if (isString(node)) {
        context.push(node);
        return;
    }
    if (isSymbol(node)) {
        context.push(context.helper(node));
        return;
    }
    switch (node.type) {
        // ...省略非常非常多的条件判断
        case 2 /* TEXT */:
            genText(node, context);
            break;
        case 4 /* SIMPLE_EXPRESSION */:
            genExpression(node, context);
            break;
        case 13 /* VNODE_CALL */:
            genVNodeCall(node, context);
            break;
        }
    }
}
举例分析genVNodeCall()

Vue3-编译流程-生成代码3.png

2. Vue3编译优化总结

2.1 Block收集动态节点

2.1.1 原理

diff算法是一层一层进行比较,如果层级过深+可以动态数据的节点占据很少数,每一次响应式更新会造成很大的性能开销
为了尽可能提高运行时的性能,Vue3在编译阶段,会尽可能区分动态节点和静态内容,并且解析出动态节点的数据,绑定在dynamicChildren中,diff更新时可以根据dynamicChildren属性,快速找到变化更新的地方,进行快速更新

2.1.2 示例

如下面代码块所示,我们会将层级较深的div#app-content2_child2_child2的动态节点数据复制一份放在div#app-wrapperdynamicChildren属性中

具体如何使用dynamicChildren进行快速更新在下面的小点会展开分析
<div id="app-wrapper">
    <div id="app-content1">
        这是第1个item
        <input type="button"/>
        <div>这是测试第2个item</div>
    </div>
    <div id="app-content2">
        <div>这是child2的child1</div>
        <div id="app-content2_child2">
            <span>这是child2的child2</span>
            <div id="app-content2_child2_child2">{{proxy.count}}</div>
        </div>
    </div>
</div>
const vnode = {
    "type": "div",
    "props": {
        "id": "app-wrapper"
    },
    "children": [...],
    "shapeFlag": 17,
    "patchFlag": 0,
    "dynamicProps": null,
    "dynamicChildren": [
        {
            "type": "div",
            "props": {
                "id": "app-content2_child2_child2"
            },
            "shapeFlag": 9,
            "patchFlag": 1,
            "dynamicProps": null,
            "dynamicChildren": null
        }
    ]
}

2.1.3 怎么收集动态节点

将上面示例代码转化为render()的函数如下所示,最终会触发openBlock()createElementBlock()等多个方法,而在这些方法中就进行vnode.dynamicChildren的构建

//...省略多个静态变量的语句
return function render(_ctx, _cache) {
    with (_ctx) {
        const {
            createElementVNode: _createElementVNode,
            createTextVNode: _createTextVNode,
            toDisplayString: _toDisplayString,
            openBlock: _openBlock,
            createElementBlock: _createElementBlock
        } = _Vue

        return (_openBlock(), _createElementBlock("div", _hoisted_1, [
            _hoisted_2,
            _createElementVNode("div", _hoisted_3, [
                _hoisted_4,
                _createElementVNode("div", _hoisted_5, [
                    _hoisted_6,
                    _createElementVNode("div", _hoisted_7, _toDisplayString(proxy.count), 1 /* TEXT */)
                ])
            ])
        ]))
    }
}
示例代码为了尽可能简单,没有囊括Component的方法createBlock()

上面的代码涉及到三个方法:

  • openBlock()
  • _createElementVNode()
  • _createElementBlock()

openBlock()分析

如下面代码块所示,openBlock()会创建一个新的block数组,即currentBlock=[]

function openBlock(disableTracking = false) {
    blockStack.push((currentBlock = disableTracking ? null : []));
}

_createElementBlock()分析
_createElementBlock()就是createElementBlock(),示例代码为了尽可能简单,没有囊括Component的方法createBlock()

Component相关的createVNode(),经过_createVNode()方法的整理,最终调用也是createBaseVNode()方法

因此无论是createElementBlock()还是createBlock(),调用都是setupBlock(createBaseVNode())

// 普通元素Block
function createElementBlock(type, props, children, patchFlag, dynamicProps, shapeFlag) {
    return setupBlock(createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, true /* isBlock */));
}
// 组件Block
function createBlock(type, props, children, patchFlag, dynamicProps) {
    return setupBlock(createVNode(type, props, children, patchFlag, dynamicProps, true /* isBlock: prevent a block from tracking itself */));
}
function _createVNode() {
  //...
  return createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true);
}
下面将展开对createBaseVNode()的分析

createBaseVNode()下面代码所示,有两个点需要特别注意,当isBlockNode=false时,

  • vnode.patchFlag > 0代表有动态绑定的数据,进行当前vnode的收集
  • shapeFlag & 6COMPONENT类型,也就是说组件类型无论有没有动态绑定的数据,都需要标注为动态节点,进行当前vnode的收集
注:在上上面generate()流程的示例代码中,有一个组件虽然没有动态节点,但是由于是COMPONENT类型,因此还是会被收集到dynamicChildren
function createBaseVNode(..., isBlockNode) {
    const vnode = {...};
    // ....省略很多代码

    // track vnode for block tree
    if (isBlockTreeEnabled > 0 &&
        !isBlockNode &&
        currentBlock &&
        (vnode.patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) &&
        vnode.patchFlag !== 32 /* HYDRATE_EVENTS */) {
        currentBlock.push(vnode);
    }
    return vnode;
}

综合分析收集动态节点的逻辑

下面代码的具体执行顺序为:

  • openBlock()
  • _createElementVNode("div", _hoisted_7)
  • _createElementVNode("div", _hoisted_5)
  • ....
  • _createElementBlock()
//...省略多个静态变量的语句
return function render(_ctx, _cache) {
    with (_ctx) {
        const {
            createElementVNode: _createElementVNode,
            createTextVNode: _createTextVNode,
            toDisplayString: _toDisplayString,
            openBlock: _openBlock,
            createElementBlock: _createElementBlock
        } = _Vue

        return (_openBlock(), _createElementBlock("div", _hoisted_1, [
            _hoisted_2,
            _createElementVNode("div", _hoisted_3, [
                _hoisted_4,
                _createElementVNode("div", _hoisted_5, [
                    _hoisted_6,
                    _createElementVNode("div", _hoisted_7, _toDisplayString(proxy.count), 1 /* TEXT */)
                ])
            ])
        ]))
    }
}

openBlock(): 由于没有传入disableTracking,因此触发currentBlock=[]

function openBlock(disableTracking = false) {
    blockStack.push((currentBlock = disableTracking ? null : []));
}

_createElementVNode("div", _hoisted_7): 没有传入isBlockNodeisBlockNode默认为false,触发currentBlock.push(vnode)代码,收集当前的vnode,此时的currentBlock对应的是最外层div#app-wrapperBlock

exports.createElementVNode = createBaseVNode_createElementVNode就是createBaseVNode
function createBaseVNode(..., isBlockNode) {
    const vnode = {...};
    // ....省略很多代码

    // track vnode for block tree
    if (isBlockTreeEnabled > 0 &&
        !isBlockNode &&
        currentBlock &&
        (vnode.patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) &&
        vnode.patchFlag !== 32 /* HYDRATE_EVENTS */) {
        currentBlock.push(vnode);
    }
    return vnode;
}
function setupBlock(vnode) {
    vnode.dynamicChildren =
        isBlockTreeEnabled > 0 ? currentBlock || EMPTY_ARR : null;
    // function closeBlock() {
    //     blockStack.pop();
    //     currentBlock = blockStack[blockStack.length - 1] || null;
    // }
    closeBlock();
    if (isBlockTreeEnabled > 0 && currentBlock) {
        currentBlock.push(vnode);
    }
    return vnode;
}

createElementBlock(): createBaseVNode()的最后一个传入参数是true,即isBlockNode=true,因此不会触发下面createBaseVNode()中的currentBlock.push(vnode)代码,因此不会收集vnode

// 普通元素Block
function createElementBlock(type, props, children, patchFlag, dynamicProps, shapeFlag) {
    return setupBlock(createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, true /* isBlock */));
}
function createBaseVNode(..., isBlockNode) {
    const vnode = {...};
    // ....省略很多代码

    // track vnode for block tree
    if (isBlockTreeEnabled > 0 &&
        !isBlockNode &&
        currentBlock &&
        (vnode.patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) &&
        vnode.patchFlag !== 32 /* HYDRATE_EVENTS */) {
        currentBlock.push(vnode);
    }
    return vnode;
}

setupBlock()中,我们调用closeBlock()恢复currentBlock = blockStack[blockStack.length - 1],切换到上一个Block(每一个Block都需要openBlock()创建)

function setupBlock(vnode) {
    vnode.dynamicChildren =
        isBlockTreeEnabled > 0 ? currentBlock || EMPTY_ARR : null;
    // function closeBlock() {
    //     blockStack.pop();
    //     currentBlock = blockStack[blockStack.length - 1] || null;
    // }
    closeBlock();
    if (isBlockTreeEnabled > 0 && currentBlock) {
        currentBlock.push(vnode);
    }
    return vnode;
}
小结

在下面的语句执行顺序中:

  • openBlock(): 创建一个currentBlock空的数组
  • _createElementVNode("div", _hoisted_7): 触发createBaseVNode(),检测到vnode.patchFlag > 0,将当前node加入到currentBlock
  • _createElementVNode("div", _hoisted_5):触发createBaseVNode(),没有通过条件无法触发currentBlock.push(vnode)
  • ....
  • _createElementBlock(): 触发createBaseVNode(xxx,... true),由于最后一个参数为true,因此无法触发当前vnode加入到currentBlock,执行createBaseVNode()后执行setupBlock(),回滚currentBlock到上一个Block
//...省略多个静态变量的语句
return function render(_ctx, _cache) {
    with (_ctx) {
        const {
            createElementVNode: _createElementVNode,
            createTextVNode: _createTextVNode,
            toDisplayString: _toDisplayString,
            openBlock: _openBlock,
            createElementBlock: _createElementBlock
        } = _Vue

        return (_openBlock(), _createElementBlock("div", _hoisted_1, [
            _hoisted_2,
            _createElementVNode("div", _hoisted_3, [
                _hoisted_4,
                _createElementVNode("div", _hoisted_5, [
                    _hoisted_6,
                    _createElementVNode("div", _hoisted_7, _toDisplayString(proxy.count), 1 /* TEXT */)
                ])
            ])
        ]))
    }
}

2.1.4 怎么在diff阶段利用Block数据快速更新

如下面代码所示,我们会直接比对新旧vnodedynamicChildren,直接取出他们的动态节点进行比较更新操作

  • 如果没有dynamicChildren,则还是走patchChildren(),也就是《Vue3源码-整体渲染流程》文章所分析的那样,直接新旧vnode一级一级地比较
  • 如果有dynamicChildren,则触发patchBlockChildren(),主要就是利用n1.dynamicChildrenn2.dynamicChildren快速找到那个需要更新的vnode数据,然后执行patch(oldVNode, newVNode)
这里不再对patchBlockChildren()具体的逻辑展开分析,请读者参考其它文章
const patchElement = (n1, n2) => {
    let { patchFlag, dynamicChildren, dirs } = n2;
    if (dynamicChildren) {
        patchBlockChildren(n1.dynamicChildren, dynamicChildren, el, parentComponent, parentSuspense, areChildrenSVG, slotScopeIds);
        if (parentComponent && parentComponent.type.__hmrId) {
            traverseStaticChildren(n1, n2);
        }
    } else if (!optimized) {
        // full diff
        patchChildren(n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG, slotScopeIds, false);
    }
}

const patchBlockChildren = (oldChildren, newChildren, fallbackContainer, parentComponent, parentSuspense, isSVG, slotScopeIds) => {
    for (let i = 0; i < newChildren.length; i++) {
        const oldVNode = oldChildren[i];
        const newVNode = newChildren[i];
        // Determine the container (parent element) for the patch.
        const container =
            oldVNode.el &&
                (oldVNode.type === Fragment ||
                    !isSameVNodeType(oldVNode, newVNode) ||
                    oldVNode.shapeFlag & (6 /* COMPONENT */ | 64 /* TELEPORT */))
                ? hostParentNode(oldVNode.el)
                : fallbackContainer;
        patch(oldVNode, newVNode, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, true);
    }
};

2.2 静态提升

2.2.1 原理

由于静态内容没有动态节点,是固定不变的内容,不需要参与每次的渲染更新
将静态内容进行提升,放在render()函数外部执行渲染创建,每次渲染更新都复用已经存在的固定的内容,提升性能

2.2.2 示例

如上面Block收集动态节点的示例一样,由于有很多固定文本的静态内容,因此会进行多个_hoisted_xxx的静态内容提升,如下面代码块所示

下面的代码块是运行时编译vnode形成的render()函数,为function模式,因此会有with语句
const _Vue = Vue
const { createElementVNode: _createElementVNode, createTextVNode: _createTextVNode } = _Vue
const _hoisted_1 = { id: "app-wrapper" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", { id: "app-content1" }, [
    /*#__PURE__*/
    _createTextVNode(" 这是第1个item "),
    /*#__PURE__*/
    _createElementVNode("input", { type: "button" }),
    /*#__PURE__*/
    _createElementVNode("div", null, "这是测试第2个item")],
    -1 /* HOISTED */
)
const _hoisted_3 = { id: "app-content2" }
const _hoisted_4 = /*#__PURE__*/_createElementVNode("div", null, "这是child2的child1", -1 /* HOISTED */)
const _hoisted_5 = { id: "app-content2_child2" }
const _hoisted_6 = /*#__PURE__*/_createElementVNode("span", null, "这是child2的child2", -1 /* HOISTED */)
const _hoisted_7 = { id: "app-content2_child2_child2" }
return function render(_ctx, _cache) {
    with (_ctx) {
        const {
            createElementVNode: _createElementVNode,
            createTextVNode: _createTextVNode,
            toDisplayString: _toDisplayString,
            openBlock: _openBlock,
            createElementBlock: _createElementBlock
        } = _Vue

        return (_openBlock(), _createElementBlock("div", _hoisted_1, [
            _hoisted_2,
            _createElementVNode("div", _hoisted_3, [
                _hoisted_4,
                _createElementVNode("div", _hoisted_5, [
                    _hoisted_6,
                    _createElementVNode("div", _hoisted_7, _toDisplayString(proxy.count), 1 /* TEXT */)
                ])
            ])
        ]))
    }
}

2.2.3 静态提升在哪里发挥作用

如示例展示,静态内容_hoisted_x放在render()函数外部执行渲染创建,在render()函数中持有对静态内容的引用,当重新渲染触发时,并不会创建新的静态内容,只是直接复用放在render()函数外部的内容

const _Vue = Vue
const { createElementVNode: _createElementVNode, createTextVNode: _createTextVNode } = _Vue
const _hoisted_1 = { id: "app-wrapper" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", { id: "app-content1" }, [
    /*#__PURE__*/
    _createTextVNode(" 这是第1个item "),
    /*#__PURE__*/
    _createElementVNode("input", { type: "button" }),
    /*#__PURE__*/
    _createElementVNode("div", null, "这是测试第2个item")],
    -1 /* HOISTED */
)
const _hoisted_3 = { id: "app-content2" }
const _hoisted_4 = /*#__PURE__*/_createElementVNode("div", null, "这是child2的child1", -1 /* HOISTED */)
const _hoisted_5 = { id: "app-content2_child2" }
const _hoisted_6 = /*#__PURE__*/_createElementVNode("span", null, "这是child2的child2", -1 /* HOISTED */)
const _hoisted_7 = { id: "app-content2_child2_child2" }
return function render(_ctx, _cache) {
    with (_ctx) {
        const {
            createElementVNode: _createElementVNode,
            createTextVNode: _createTextVNode,
            toDisplayString: _toDisplayString,
            openBlock: _openBlock,
            createElementBlock: _createElementBlock
        } = _Vue

        return (_openBlock(), _createElementBlock("div", _hoisted_1, [
            _hoisted_2,
            _createElementVNode("div", _hoisted_3, [
                _hoisted_4,
                _createElementVNode("div", _hoisted_5, [
                    _hoisted_6,
                    _createElementVNode("div", _hoisted_7, _toDisplayString(proxy.count), 1 /* TEXT */)
                ])
            ])
        ]))
    }
}

2.3 事件监听缓存

prefixIdentifiers=truecacheHandlers=true时,会启动事件监听缓存,避免每次重新渲染时重复创建onClick方法,造成额外的性能开销

Vue3–编译流程-事件缓存.png

3. Vue2编译优化总结

vue 2.6.14的版本中,编译相关的步骤可以总结为

  • parse():解析<template></template>AST
  • optimize():优化AST语法树
  • generate():根据优化后的AST语法树生成对应的render()函数

    上面三个编译步骤对应下面的代码
var createCompiler = createCompilerCreator(function baseCompile(
    template,
    options
) {
    var ast = parse(template.trim(), options);
    if (options.optimize !== false) {
        optimize(ast, options);
    }
    var code = generate(ast, options);
    return {
        ast: ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
    }
});

本小节将集中在optimize()阶段进行Vue2编译优化总结

3.1 optimize整体概述

遍历生成的AST语法树,检测纯静态的子树,即永远不需要改变的DOM
一旦我们检测到这些子树,我们就可以:

  • 将它们提升为常量,这样我们就不需要在每次重新渲染时为它们创建新节点,即在触发响应式更新时,标记为静态节点不会重新生成新的节点,而是直接复用
  • patching过程完全跳过它们,提升性能,即在patch阶段,不会进行对比操作,直接跳过提升性能
function optimize (root, options) {
  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);
}
那么是如何标记为当前的节点为静态节点的呢?

按照上面代码和注释,主要进行两个步骤:

  1. markStatic():标记所有非静态节点
  2. markStaticRoots():标记静态根节点

3.2 标记所有非静态节点markStatic

使用isStatic(node)对目前node是否是静态节点判断,比如expression类型则不是静态节点,text则是静态节点

具体可以参考下面的isStatic()的分析

如果node.type === 1,即当前nodecomponent or element,那么就会判断node.children,由于if语句中的节点都不在children中,因此在遍历node.children后,还要遍历下node.ifConditions.length

遍历时,触发node.childrennode.ifConditions.lengthmarkStatic(),然后判断它们是否是静态的节点,如果它们有一个节点不是静态的,那么当前node就不是静态的

function markStatic (node) {
  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 (var i = 0, l = node.children.length; i < l; i++) {
      var child = node.children[i];
      markStatic(child);
      if (!child.static) {
        node.static = false;
      }
    }
    if (node.ifConditions) {
      for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
        var block = node.ifConditions[i$1].block;
        markStatic(block);
        if (!block.static) {
          node.static = false;
        }
      }
    }
  }
}

下面isStatic()的代码中,可以总结为:

  • 如果当前nodeexpression类型,不是静态节点
  • 如果当前nodetext类型,是静态节点
  • 如果当前node没有使用v-pre指令,是静态节点
  • 如果当前node使用v-pre指令,它要成为静态节点就得满足

    • 不能使用动态绑定,即不能使用v-xxx@xxx:等属性,比如@click
    • 不能使用v-ifv-forv-else指令
    • 不能是slotcomponent
    • 当前node.parent不能是带有v-fortemplate标签
    • 当前node的所有属性都必须是静态节点属性,即只能是这些type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap属性
function isStatic (node) {
  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)
  ))
}

小结

当前node必须满足一定条件以及当前node.children以及node.ifConditions都是静态的,当前node才能赋值node.static=true

3.3 标记静态根

从下面代码可以知道,node.staticRoot=true的条件为:

  • 当前node必须满足node.static=true
  • 当前node必须有子元素,即node.children.length > 0
  • 当前node的子元素不能只有一个文本节点,不然staticRoot也是false
 function markStaticRoots (node, isInFor) {
    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 (var i = 0, l = node.children.length; i < l; i++) {
                markStaticRoots(node.children[i], isInFor || !!node.for);
            }
        }
        if (node.ifConditions) {
            for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
                markStaticRoots(node.ifConditions[i$1].block, isInFor);
            }
        }
    }
}

3.5 根据node.staticRoot生成代码

第一步node.static是为了第二步node.staticRoot的赋值做准备
在生成代码generate()中,利用的是第二步的node.staticRoot属性,从而触发genStatic()进行静态提升代码的生成

function generate (
    ast,
    options
) {
    var state = new CodegenState(options);
    // fix #11483, Root level <script> tags should not be rendered.
    var code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")';
    return {
        render: ("with(this){return " + code + "}"),
        staticRenderFns: state.staticRenderFns
    }
}

function genElement (el, state) {
    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
        // ...
        return code
    }
}

genStatic()中会生成_m(xxxx)的静态代码,而_m()实际上就是renderStatic()方法
renderStatic()方法中,会使用cached[index]进行渲染内容的缓存,下一次渲染更新时会重新执行_m(xxxx)的静态代码,然后从cached中获取之前已经渲染的内容,不用再重新创建

function genStatic(el, state) {
    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.
    var 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' : '') + ")")
}

function renderStatic(
    index,
    isInFor
) {
    var cached = this._staticTrees || (this._staticTrees = []);
    var tree = cached[index];
    // if has already-rendered static tree and not inside v-for,
    // we can reuse the same tree.
    if (tree && !isInFor) {
        return tree
    }
    // otherwise, render a fresh tree.
    tree = cached[index] = this.$options.staticRenderFns[index].call(
        this._renderProxy,
        null,
        this // for render fns generated for functional component templates
    );
    markStatic(tree, ("__static__" + index), false);
    return tree
}

3.6 patch阶段如何跳过静态代码

patchVnode()阶段,一开始会进行oldVnode === vnode的比较,由于新旧vnode都是renderStatic()方法获取到的cached数据,因此会直接触发return,不再执行patchVnode()阶段剩余的diff流程

function patchVnode (
  oldVnode,
  vnode,
  ...
) {
  if (oldVnode === vnode) {
    return
  }
  //....
}

4. 暂时搁置的问题

后续有时间再回来解决下面的问题
  • 静态提升的类型总结:在上面1.3.4 hoistStatic()的阶段分析我们简单地分析了什么情况下要进行静态提升和如何生成静态提升代码,但是我们并没有对具体什么类型应该进行静态提升进行总结,主要涉及到getConstantType()的分析

参考

  1. Vue2.x利用template模板做了什么优化?

Vue系列其它文章

  1. Vue2源码-响应式原理浅析
  2. Vue2源码-整体流程浅析
  3. Vue2源码-双端比较diff算法 patchVNode流程浅析
  4. Vue3源码-响应式系统-依赖收集和派发更新流程浅析
  5. Vue3源码-响应式系统-Object、Array数据响应式总结
  6. Vue3源码-响应式系统-Set、Map数据响应式总结
  7. Vue3源码-响应式系统-ref、shallow、readonly相关浅析
  8. Vue3源码-整体流程浅析
  9. Vue3源码-diff算法-patchKeyChildren流程浅析
  10. Vue3相关源码-Vue Router源码解析(一)
  11. Vue3相关源码-Vue Router源码解析(二)
  12. Vue3相关源码-Vuex源码解析

白边
209 声望37 粉丝

源码爱好者,已经完成vue2和vue3的源码解析+webpack5整体流程源码+vite4开发环境核心流程源码+koa2源码