编译入口
当我们使用 Runtime + Compiler 的 Vue.js,它的入口是 src/platforms/web/entry-runtime-with-compiler.js
,看一下它对 $mount
函数的定义:
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el);
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== "production" &&
warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
);
return this;
}
const options = this.$options;
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template;
if (template) {
if (typeof template === "string") {
if (template.charAt(0) === "#") {
template = idToTemplate(template);
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
);
}
}
} else if (template.nodeType) {
template = template.innerHTML;
} else {
if (process.env.NODE_ENV !== "production") {
warn("invalid template option:" + template, this);
}
return this;
}
} else if (el) {
template = getOuterHTML(el);
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && config.performance && mark) {
mark("compile");
}
const { render, staticRenderFns } = compileToFunctions(
template,
{
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments,
},
this
);
options.render = render;
options.staticRenderFns = staticRenderFns;
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && config.performance && mark) {
mark("compile end");
measure(`vue ${this._name} compile`, "compile", "compile end");
}
}
}
return mount.call(this, el, hydrating);
};
这段函数逻辑之前分析过,关于编译的入口就是在这里:
const { render, staticRenderFns } = compileToFunctions(
template,
{
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments,
},
this
);
options.render = render;
options.staticRenderFns = staticRenderFns;
compileToFunctions
方法就是把模板 template
编译生成 render
以及 staticRenderFns
,它的定义在 src/platforms/web/compiler/index.js
中:
import { baseOptions } from "./options";
import { createCompiler } from "compiler/index";
const { compile, compileToFunctions } = createCompiler(baseOptions);
export { compile, compileToFunctions };
可以看到 compileToFunctions
方法实际上是 createCompiler
方法的返回值,该方法接收一个编译配置参数,接下来我们来看一下 createCompiler
方法的定义,在 src/compiler/index.js
中:
// `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 {
const ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
const code = generate(ast, options);
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns,
};
});
createCompiler
方法实际上是通过调用 createCompilerCreator
方法返回的,该方法传入的参数是一个函数,真正的编译过程都在这个 baseCompile
函数里执行,那么 createCompilerCreator
又是什么呢,它的定义在 src/compiler/create-compiler.js
中:
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 = [];
const tips = [];
finalOptions.warn = (msg, tip) => {
(tip ? tips : errors).push(msg);
};
if (options) {
// 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];
}
}
}
const compiled = baseCompile(template, finalOptions);
if (process.env.NODE_ENV !== "production") {
errors.push.apply(errors, detectErrors(compiled.ast));
}
compiled.errors = errors;
compiled.tips = tips;
return compiled;
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile),
};
};
}
可以看到该方法返回了一个 createCompiler
的函数,它接收一个 baseOptions
的参数,返回的是一个对象,包括 compile
方法属性和 compileToFunctions
属性,这个 compileToFunctions
对应的就是 $mount
函数调用的 compileToFunctions
方法,它是调用 createCompileToFunctionFn
方法的返回值,我们接下来看一下 createCompileToFunctionFn
方法,它的定义在 src/compiler/to-function/js
中:
export function createCompileToFunctionFn(compile: Function): Function {
const cache = Object.create(null);
return function compileToFunctions(
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
options = extend({}, options);
const warn = options.warn || baseWarn;
delete options.warn;
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production") {
// detect possible CSP restriction
try {
new Function("return 1");
} catch (e) {
if (e.toString().match(/unsafe-eval|CSP/)) {
warn(
"It seems you are using the standalone build of Vue.js in an " +
"environment with Content Security Policy that prohibits unsafe-eval. " +
"The template compiler cannot work in this environment. Consider " +
"relaxing the policy to allow unsafe-eval or pre-compiling your " +
"templates into render functions."
);
}
}
}
// check cache
const key = options.delimiters
? String(options.delimiters) + template
: template;
if (cache[key]) {
return cache[key];
}
// compile
const compiled = compile(template, options);
// check compilation errors/tips
if (process.env.NODE_ENV !== "production") {
if (compiled.errors && compiled.errors.length) {
warn(
`Error compiling template:\n\n${template}\n\n` +
compiled.errors.map((e) => `- ${e}`).join("\n") +
"\n",
vm
);
}
if (compiled.tips && compiled.tips.length) {
compiled.tips.forEach((msg) => tip(msg, vm));
}
}
// turn code into functions
const res = {};
const fnGenErrors = [];
res.render = createFunction(compiled.render, fnGenErrors);
res.staticRenderFns = compiled.staticRenderFns.map((code) => {
return createFunction(code, fnGenErrors);
});
// check function generation errors.
// this should only happen if there is a bug in the compiler itself.
// mostly for codegen development use
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production") {
if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
warn(
`Failed to generate render function:\n\n` +
fnGenErrors
.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`)
.join("\n"),
vm
);
}
}
return (cache[key] = res);
};
}
至此我们总算找到了 compileToFunctions
的最终定义,它接收 3 个参数、编译模板 template
,编译配置 options
和 Vue 实例 vm
。核心的编译过程就一行代码:
const compiled = compile(template, options);
compile
函数在执行 createCompileToFunctionFn
的时候作为参数传入,它是 createCompiler
函数中定义的 compile
函数,如下:
function compile(template: string, options?: CompilerOptions): CompiledResult {
const finalOptions = Object.create(baseOptions);
const errors = [];
const tips = [];
finalOptions.warn = (msg, tip) => {
(tip ? tips : errors).push(msg);
};
if (options) {
// 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];
}
}
}
const compiled = baseCompile(template, finalOptions);
if (process.env.NODE_ENV !== "production") {
errors.push.apply(errors, detectErrors(compiled.ast));
}
compiled.errors = errors;
compiled.tips = tips;
return compiled;
}
compile
函数执行的逻辑是先处理配置参数,真正执行编译过程就一行代码:
const compiled = baseCompile(template, finalOptions);
baseCompile
在执行 createCompilerCreator
方法时作为参数传入,如下:
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options);
optimize(ast, options);
const code = generate(ast, options);
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns,
};
});
所以编译的入口我们终于找到了,它主要就是执行了如下几个逻辑:
- 解析模板字符串生成 AST
const ast = parse(template.trim(), options);
- 优化语法树
optimize(ast, options);
- 生成代码
const code = generate(ast, options);
那么接下来的章节我会带大家去逐步分析这几个过程。
总结
编译入口逻辑之所以这么绕,是因为 Vue.js 在不同的平台下都会有编译的过程,因此编译过程中的依赖的配置 baseOptions
会有所不同。而编译过程会多次执行,但这同一个平台下每一次的编译过程配置又是相同的,为了不让这些配置在每次编译过程都通过参数传入,Vue.js 利用了函数柯里化的技巧很好的实现了 baseOptions
的参数保留。同样,Vue.js 也是利用函数柯里化技巧把基础的编译过程函数抽出来,通过 createCompilerCreator(baseCompile)
的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开,这样的设计还是非常巧妙的。