Vue 在实现挂载的过程中,如果用户传入的是模板 template,那么会执行编译操作,即将模板编译生成 render 函数,以便后续可以将 Vue 实例通过 render 函数转换成虚拟 DOM,进而通过 patch 将虚拟 DOM 转换成真实 DOM,完成挂载。那么,涉及到的编译过程则是本文要分析的。
编译入口
沿着主线将其实现逻辑整理成一张逻辑图如下:
在 $mount 函数定义过程中,有一段逻辑是用来处理模板编译的,具体代码实现如下:
// src/platform/web/entry-runtime-with-compiler.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
那么,根据编译代码实现,一步一步地来分析模板编译入口究竟是怎样的?
首先,调用函数 compileToFunctions ,传入的参数有三个:编译模板 template、编译配置 options 和 Vue 实例 vm,最终返回对象,包含两个属性:render 和 staticRenderFns。
compileToFunctions 代码实现如下:
// src/platforms/web/compiler/index.js
const { compile, compileToFunctions } = createCompiler(baseOptions)
可见,函数 compileToFunctions 是调用 createCompiler 返回的,传入参数 baseOptions,具体如下:
// src/platforms/web/compiler/options.js
export const baseOptions: CompilerOptions = {
expectHTML: true,
modules,
directives,
isPreTag,
isUnaryTag,
mustUseProp,
canBeLeftOpenTag,
isReservedTag,
getTagNamespace,
staticKeys: genStaticKeys(modules)
}
那么 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
}
})
调用函数 createCompilerCreator ,传入函数 baseCompile 作为参数,需要注意的是函数 baseCompile 是整个编译过程的核心逻辑,最终返回函数 createCompiler。
那么,再来看下函数 createCompilerCreator 是如何实现的?
// src/complier/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 = []
let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}
if (options) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
// $flow-disable-line
const leadingSpaceLength = template.match(/^\s*/)[0].length
warn = (msg, range, tip) => {
const data: WarningMessage = { 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]
}
}
}
finalOptions.warn = warn
const compiled = baseCompile(template.trim(), finalOptions)
if (process.env.NODE_ENV !== 'production') {
detectErrors(compiled.ast, warn)
}
compiled.errors = errors
compiled.tips = tips
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
从代码实现可看出,最终返回函数 createCompiler;那么,当执行函数 createCompiler 时,其结果是返回对象,包含两个属性:compile 和 compileToFunctions。而对于 compileToFunctions 则是由调用函数 createCompileToFunctionFn 返回,传入的参数为 compile。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) {
if (options.outputSourceRange) {
compiled.errors.forEach(e => {
warn(
`Error compiling template:\n\n${e.msg}\n\n` +
generateCodeFrame(template, e.start, e.end),
vm
)
})
} else {
warn(
`Error compiling template:\n\n${template}\n\n` +
compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
vm
)
}
}
if (compiled.tips && compiled.tips.length) {
if (options.outputSourceRange) {
compiled.tips.forEach(e => tip(e.msg, vm))
} else {
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;也就是说,在 $mount 挂载过程中,处理模板编译逻辑调用的函数 compileToFunctions 的实现逻辑正在这里,接着来其是如何实现的。
函数接收三个参数:
template:编译模板;options:编译配置;vm:Vue 实例。
函数实现逻辑主要有以下几点:
- 判断是否有缓存,有的话则直接返回,无需编译;
- 调用函数
compile执行编译核心逻辑; - 检查编译产物是否存在错误;
- 调用函数
createFuncition将编译产物转换成render函数 - 设置属性
staticRenderFns; - 缓存编译结果,并将其返回。
对于编译核心逻辑 compile,其作为参数传入,定义在 src/complier/create-compiler.js,具体实现如上;接着来分析其核心实现。
函数 compile 接收两个参数:
template:编译模板;options:编译配置。
其实现逻辑主要有以下几点:
- 合并编译配置
options; - 调用函数
baseCompile执行编译核心逻辑。
baseCompile 是在调用函数 createCompilerCreator 函数时传入,其定义在 src/compiler/index.js 文件中,具体实现如下:
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
}
})
由此可见,代码实现主要有三步:
- 解析模板,生成 AST(parse);
- 优化 AST(optimize);
- 生成代码(generate)
这三步也是整个模板编译过程中的核心逻辑,后续会一一分析。
回到函数 compileToFunctions,来看下如何把编译产物转换成函数,即 createFunction 的实现:
// src/compiler/to-function.js
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}
至此,模板编译入口已分析完,