Skip to content

模板编译

接上一篇,当options.render(渲染函数)没有的时候,会进入判断options.template的流程

不同的 template 写法

TIP

开发中,template 可以有以下几种方式

  • 传入一个 ID 选择器,例如#app
  • 传入一个 DOM 节点,document.getElementById('app')
  • 没有template属性, 但是得有 el属性,内部会用el身上的内容作为tamplate

对于不同的 template 写法,vue 内部做了不同的处理

ts
let template = options.template;
if (template) {
  if (typeof template === "string") {
    if (template.charAt(0) === "#") {
      template = idToTemplate(template); // 通过innerHTML获取内容
      /* istanbul ignore if */
      if (__DEV__ && !template) {
        warn(
          `Template element not found or is empty: ${options.template}`,
          this
        );
      }
    }
  } else if (template.nodeType) {
    template = template.innerHTML;
  } else {
    if (__DEV__) {
      warn("invalid template option:" + template, this);
    }
    return this;
  }
} else if (el) {
  // @ts-expect-error
  template = getOuterHTML(el);
}

getOuterHTML

没有template属性时候,内部会用el身上的内容作为tamplate

MDN 介绍提示

element DOM 接口的 outerHTML 属性获取描述元素(包括其后代)的序列化 HTML 片段。它也可以设置为用从给定字符串解析的节点替换元素。

要仅获取元素内容的 HTML表示形式 或替换元素的内容,请使用 innerHTML 属性

直白一点就是

  • element.outerHTML取到的是 包含 element 本身在内html 字符串
  • element.outerHTML='<div>xxxxxxxx</div>' 作用是将element 本身包括自己的子元素都 替换成<div>xxxxxxxx</div>

有了这个前置知识,template = getOuterHTML(el); 就很好理解了,作用就是取到包括 el 元素本身在内的所有 html 字符串

compileToFunctions 编译模板为 render 函数

源码位置:src/plantforms/web/runtime-with-compiler.ts

上一步获得了模板种的 html 字符串,就可以着手开始进行编译解析了

ts
if (template) {
  /* istanbul ignore if */
  if (__DEV__ && config.performance && mark) {
    mark("compile");
  }

  const { render, staticRenderFns } = compileToFunctions(
    template,
    {
      outputSourceRange: __DEV__,
      shouldDecodeNewlines, // 是否对一般html元素属性中换行符进行编码
      shouldDecodeNewlinesForHref, // 是否对a标签中换行符进行编码
      delimiters: options.delimiters,
      comments: options.comments
    },
    this
  );
  options.render = render;
  options.staticRenderFns = staticRenderFns;

  /* istanbul ignore if */
  if (__DEV__ && config.performance && mark) {
    mark("compile end");
    measure(`vue ${this._name} compile`, "compile", "compile end");
  }
}

首先,判断了是由有模板,有模板才会进行编译,否则什么也不做

模板编译完后会返回一个对象,里面包含renderstaticRenderFns两个函数, 并挂载到options身上

compileToFunctions来自于src/plantforms/web/compiler/index.ts, 源码如下:

ts
import { baseOptions } from "./options";
import { createCompiler } from "compiler/index";

// 用baseOptions创建编译
const { compile, compileToFunctions } = createCompiler(baseOptions);

export { compile, compileToFunctions };

baseCompile

compileToFunctions仅仅是createCompiler函数返回值中的一个函数,createCompiler来自于src/compiler/index.ts,源码如下:

ts
import { parse } from "./parser/index";
import { optimize } from "./optimizer";
import { generate } from "./codegen/index";
import { createCompilerCreator } from "./create-compiler";
import { CompilerOptions, CompiledResult } from "types/compiler";
// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 1. 解析:解析 HTML 字符串(模板)得到 ast抽象语法树
  const ast = parse(template.trim(), options);
  // 2. 优化:标记静态节点,diff 时直接跳过
  if (options.optimize !== false) {
    optimize(ast, options);
  }
  // 3. 生成代码:将 ast 转换成代码字符串,将来通过new Function(code) 就能达到 render 函数
  const code = generate(ast, options);
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  };
});

看到 createCompilercreateCompilerCreator函数的返回值,createCompilerCreator函数接收一个函数作为参数,返回一增强后的新个函数,妥妥的高阶函数。这种思想在开发中非常值得借鉴,可以在不改变原函数的基础上,对原函数做丰富的拓展。

我们先来分析createCompilerCreator传入的函数baseCompile的内部逻辑:

baseCompile 函数是真正的模板编译的方法

  1. 解析:解析template得到 ast(抽象语法树),这里的 template 就是上一步得到的 HTML 字符串,解析template的逻辑非常多,这里先略过,后面再来分析
  2. 优化:标记静态节点,diff 时直接跳过
  3. 生成代码:将 ast 转换成代码字符串,将来通过 new Function(code) 就能达到 render 函数

接下来的核心就是分析createCompilerCreator内部实现

createCompilerCreator

ts
import { extend } from "shared/util";
import {
  CompilerOptions,
  CompiledResult,
  WarningMessage
} from "types/compiler";
import { detectErrors } from "./error-detector";
import { createCompileToFunctionFn } from "./to-function";

export function createCompilerCreator(baseCompile: Function): Function {
  return function createCompiler(baseOptions: CompilerOptions) {
    function compile(
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      const finalOptions = Object.create(baseOptions);
      const errors: WarningMessage[] = [];
      const tips: WarningMessage[] = [];

      let warn = (
        msg: WarningMessage,
        range: { start: number; end: number },
        tip: string
      ) => {
        (tip ? tips : errors).push(msg);
      };

      if (options) {
        if (__DEV__ && options.outputSourceRange) {
          // $flow-disable-line
          const leadingSpaceLength = template.match(/^\s*/)![0].length;

          warn = (
            msg: WarningMessage | string,
            range: { start: number; end: number },
            tip: string
          ) => {
            const data: WarningMessage =
              typeof msg === "string" ? { msg } : msg;
            if (range) {
              if (range.start != null) {
                data.start = range.start + leadingSpaceLength;
              }
              if (range.end != null) {
                data.end = range.end + leadingSpaceLength;
              }
            }
            (tip ? tips : errors).push(data);
          };
        }
        // merge custom modules
        if (options.modules) {
          finalOptions.modules = (baseOptions.modules || []).concat(
            options.modules
          );
        }
        // merge custom directives
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives || null),
            options.directives
          );
        }
        // copy other options
        for (const key in options) {
          if (key !== "modules" && key !== "directives") {
            finalOptions[key] = options[key as keyof CompilerOptions];
          }
        }
      }

      finalOptions.warn = warn;

      const compiled = baseCompile(template.trim(), finalOptions);
      if (__DEV__) {
        detectErrors(compiled.ast, warn);
      }
      compiled.errors = errors;
      compiled.tips = tips;
      return compiled;
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    };
  };
}

内部函数createCompiler的逻辑就是 内部定义的函数compile, 最后返回的对象中,compileToFunctions的值为用createCompileToFunctionFn函数包装之后的compile, 因此,核心代码就是compile函数,以及将编译代码进行转换的包装函数createCompileToFunctionFn, 先来分析compile函数

compile

源代码见上一节

经过上文的分析,这里的compile的调用根源是在Vue.prototype.$mountcompileToFunctions的调用的时候

compileToFunctions 来自于 createCompiler函数的返回值解构, createCompiler函数是 createCompilerCreator函数的返回值, 因此找到createCompilerCreator函数中 compileToFunctions来源就能找到根源,compileToFunctions 又是来自于 createCompileToFunctionFn 函数通过包装 compile 函数得到,因此进到 createCompileToFunctionFn 中可以看到 compileToFunctions 的定义, 最终发现 compileToFunctions 函数内部有这样一行代码 const compiled = compile(template, options), 因此,compileToFunctions 函数的调用会触发 compile 的调用,且入参都一样均为 emplate, options

看一下最开始compileToFunctions调用, 因为compile的调用与这里前两个参数一样, 主要是第二个参数

ts
const { render, staticRenderFns } = compileToFunctions(
  template,
  {
    outputSourceRange: __DEV__,
    shouldDecodeNewlines, // 是否对一般html元素属性中换行符进行编码
    shouldDecodeNewlinesForHref, // 是否对a标签中换行符进行编码
    delimiters: options.delimiters,
    comments: options.comments
  },
  this
);

compile源码中,先创建了一个对象,且继承自 baseOptions对象

ts
const finalOptions = Object.create(baseOptions);

export const baseOptions: CompilerOptions = {
  expectHTML: true,
  modules,
  directives,
  isPreTag,
  isUnaryTag,
  mustUseProp,
  canBeLeftOpenTag,
  isReservedTag,
  getTagNamespace,
  staticKeys: genStaticKeys(modules)
};

在 options 存在的情况下,接着做了三件事

  • 合并自定义模块
  • 合并自定义指令
  • 复制其他选项

上面所有逻辑都是处理参数配置, 核心代码就是这一句:

ts
const compiled = baseCompile(template.trim(), finalOptions);

baseCompile 就是 createCompilerCreator 传入的函数,它也是编译的入口

总结

从传入模板(HTML)字符串到编译成render函数的过程经历了一系列高阶函数的转换,可以说其他函数都是进行配置参数的转换,最终在baseCompile函数内部进行核心代码的调用,包含解析 html 字符串为 ast;标记静态节点,使得 diff 时候可以跳过;将 ast 生成代码字符串,可以供new Function使用