- 编译入口
- 总结
- 总结
编译入口
当我们使用 Runtime + Compiler 的 Vue.js,它的入口是 src/platforms/web/entry-runtime-with-compiler.js,看一下它对 $mount 函数的定义:
const mount = Vue.prototype.$mountVue.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 functionif (!options.render) {let template = options.templateif (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 = renderoptions.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 = renderoptions.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 modulesif (options.modules) {finalOptions.modules =(baseOptions.modules || []).concat(options.modules)}// merge custom directivesif (options.directives) {finalOptions.directives = extend(Object.create(baseOptions.directives || null),options.directives)}// copy other optionsfor (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 = errorscompiled.tips = tipsreturn 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 || baseWarndelete options.warn/* istanbul ignore if */if (process.env.NODE_ENV !== 'production') {// detect possible CSP restrictiontry {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 cacheconst key = options.delimiters? String(options.delimiters) + template: templateif (cache[key]) {return cache[key]}// compileconst compiled = compile(template, options)// check compilation errors/tipsif (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 functionsconst 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 modulesif (options.modules) {finalOptions.modules =(baseOptions.modules || []).concat(options.modules)}// merge custom directivesif (options.directives) {finalOptions.directives = extend(Object.create(baseOptions.directives || null),options.directives)}// copy other optionsfor (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 = errorscompiled.tips = tipsreturn 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) 的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开,这样的设计还是非常巧妙的。
原文: https://ustbhuangyi.github.io/vue-analysis/compile/entrance.html
