本文已参与「新人创作礼」活动,一起开启掘金创作之路。
Vue 编译(Compile)入口
Vue中的数据渲染到页面的整个流程,我们之前介绍过,不清楚可以点击这里,其中的编译流程,是将我们的template
模板编译生成render函数。那为什么需要编译呢,因为template模板中有很多指令(比如@click,v-for等),原生js是识别不了的。接下来我们来分析一下Vue编译的过程:
在我们之前分析的mount过程中有这样一段逻辑,compileToFunctions就是执行编译的函数,将我们定义的template作为参数传入,并生成render函数,下面我们来看下compileToFunctions:
if (template) {
......
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
......
}
compileToFunctions
compileToFunctions定义在src/platform/web/compiler/index.js中:
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
可以看到,compileToFunctions是通过调用createCompiler生成的,参数baseOptions是web平台的一些配置项,继续看createCompiler函数,定义在src/compiler/index.js中:
import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-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 {
// 解析成ast树
const ast = parse(template.trim(), options)
// 优化ast树
if (options.optimize !== false) {
optimize(ast, options)
}
// 生成代码
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
可以看到这里的createCompiler又是通过createCompilerCreator这个函数去生成的,createCompilerCreator这个函数的参数是一个函数baseCompile,这里面的逻辑是编译过程的核心,包含ast树的生成、ast树的优化、代码生成,这三个流程我们之后再去详细的分析。我们继续看createCompilerCreator这个函数,定义在src/compiler/create-compiler.js:
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
// 这个函数的作用是将一些配置项合并一下,再执行传入的核心编译函数baseCompile
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
// web平台的基础配置,利用原型继承
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)
}
}
}
在这里,我们看到,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 */
// 判断是否支持new Function语法
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 这儿执行传入的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
// 这儿将编译生成的字符串代码转换成render函数,通过createFunction函数
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)
}
}
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}
可以看到我们绕了很多圈,最终找到了compileToFunctions这个函数,现在我们来捋一下这个执行流程。
-
1.进入compileToFunctions函数: 执行compileToFunctions这个函数,这个函数首先会验证当前环境是否支持new Function语法,再利用闭包做了一个编译器缓存,然后执行传入的compile函数。
-
2.进入compile函数 compile函数将平台的配置拿到做了一个赋值,再结合当前传入的options参数,做了一些合并配置的操作,然后会执行传入的baseCompile函数,
-
3.进入baseCompile函数 baseCompile函数是编译的核心函数,这个地方会根据传入的template生成ast树、优化ast树、生成字符串代码,完成编译后,
-
4.完成baseCompile函数的执行,进入compile函数 对编译返回结果compiled对象挂载了errors、tips两个属性,
-
5.完成compile函数,进入compileToFunctions函数 进入compileToFunctions这个函数,检查编译过程有没有错误,再将生成的字符串代码利用 new Function语法,转换成匿名函数。
最终的这个匿名函数就是最终生成的render函数。关于模板编译的结果可以参考这个网址template-explorer.vuejs.org/。
到此为止,我们清楚了编译的入口了,下一节我们继续去分析编译的核心流程!