编译In Vue
我们前面分析了Vue中的实例化过程和响应式原理,接下来我们会补全Vue在实例化最初的一部分内容,就是编译
最初我们举例子都是通过直接编写render进行的,这个其实和我们开发中遇到的情况不太一样。虽然我们可以直接编写render函数用于渲染一些比较复杂但是有规律的内容,但是大部分情况下,我们都是直接编写template模板来进行开发的,这也更符合我们的开发习惯
提到template和render函数的关系,这里就补充一下
Vue.js主要由两个内容构成:runtime和compiler。我们一般用vue-cli完成打包的项目,内部的vue都是runtime only的,compiler的工作是把template转换成render函数,这个转换逻辑在我们npm run build的时候,通过webpack的vue-loader就已经完成了,所以vue在打包的时候不用带上compiler,这个也是推荐的做法
但是这个也不是绝对的,我们在业务场景中,为了方便控制组件的按需加载,直接把使用的template拼接在html内,通过使用带compiler的vue在运行时进行模板编译,来实现我们想要的效果,这样大大降低了按需加在的实现成本,但是,随之带来的是页面性能的牺牲(compiler解析十分耗时)
ok那回到这里,下面我们就回到实例化最初的地方,看看Vue是在哪一步进行的template 编译工作
源码分析
入口
回忆一下我们第一期分享的实例化内容,我们在初始化的时候会调用$mount函数进行开始把vue内容渲染到我们的页面上,在流程图内我们会判断是否使用了template,如果是有了那么就执行下面的内容:
src/platforms/web/entry-runtime-with-compiler.js
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
compileToFunctions的作用就是把模板编译成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又是createCompilerCreator的返回值(禁止套娃)
这里比较有意思的是,createCompilerCreator传入的参数是一个function,所以整个调用链就像这样:
$mount => compileToFunctions => createCompiler => createCompilerCreator(function baseCompile() {})
根据类型系统,我们发现传入一个函数,返回一个函数,那么十有八九就是提前固化参数了,跟我们实例化时候讲到的的nodeOpts有点像,我这里也能发现,是提前固化了baseCompile的函数逻辑
那我们看看具体的createCompilerCreator的实现逻辑
src/compiler/create-compiler.js
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
// ...
function compile (template, options) {
if (options) {
// ...
const compiled = baseCompile(template, finalOptions)
return compiled
}
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
我们可以发现,用上了我们提前固化的baseCompile函数,但是返回compileToFunctions的过程也没有想象中的顺利,又是一个套娃,compileToFunctions是createCompileToFunctionFn的返回值
这个是不是为了把我们固化的baseCompile固化到compile内,再把compile固化到compileToFunctions内所做的工作呢?
我们看看createCompileToFunctionFn的源代码
src/compiler/to-function.js
export function createCompileToFunctionFn (compile) {
const cache = Object.create(null)
return function compileToFunctions(template, options, vm) {
// ...
const compiled = compile(template, options)
// ...
return (cache[key] = res)
}
}
其实归根结底就是compile函数的调用,这就是执行逻辑的所在,我们再回过头来,反向看看这个compile到底是啥,这个链条应该是这样的:
compile
<= createCompileToFunctionFn的入参
<= createCompiler传入变量function compile() {}
<= compile中调用baseCompile
<= createCompilerCreator中传入function baseCompile() {}
所以编译入口就是我们最开始传入的baseCompile...我还是没太搞懂为啥这么做,先继续看看把
主要执行的有以下逻辑:
- 解析模板字符串生成AST
- 优化语法树
- 生成代码
代码解释就是这些
// 1
const ast = parse(template.trim(), options)
// 2
optimize(ast, options)
// 3
const code = generate(ast, options)
我们终于找到了编译入口,之所以这么设计,和nodeOpts也是异曲同工之妙,还是为了固化baseOptions,把多平台的支持拆分开来,通过 createCompilerCreator(baseCompile) 的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开,这样的设计还是非常巧妙的