主题
模板编译
接上一篇,当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");
}
}
首先,判断了是由有模板,有模板才会进行编译,否则什么也不做
模板编译完后会返回一个对象,里面包含render
、staticRenderFns
两个函数, 并挂载到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
};
});
看到 createCompiler
是 createCompilerCreator
函数的返回值,createCompilerCreator
函数接收一个函数作为参数,返回一增强后的新个函数,妥妥的高阶函数。这种思想在开发中非常值得借鉴,可以在不改变原函数的基础上,对原函数做丰富的拓展。
我们先来分析createCompilerCreator
传入的函数baseCompile
的内部逻辑:
baseCompile 函数是真正的模板编译的方法
- 解析:解析
template
得到ast
(抽象语法树),这里的template
就是上一步得到的 HTML 字符串,解析template
的逻辑非常多,这里先略过,后面再来分析 - 优化:标记静态节点,
diff
时直接跳过 - 生成代码:将
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.$mount
中compileToFunctions
的调用的时候
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
使用