「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」
前言
在前几个月的时候,从0开始学习vue源码。侧重点在于vue的组件化实现,响应式原理,及渲染实现,主要分析的是vue的运行时代码逻辑。从现在开始我们将学习vue编译相关的代码,因为编译相关的代码主要是和AST的生成,转化有关,所以可能比较晦涩和考验JS操作。我们主要是了解其实现过程,知道其大概的实现逻辑和实现流程即可。
本篇文章先从入口开始,先简单分析下其入口逻辑
入口
我们之前在分析vue实例化的时候知道vue的入口文件是从entry-runtime-with-compiler.js开始的,而我们的render函数也是从那边开始的
if (template) {
// ...
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
// ...
}
可以发现当我们输入为template时,是通过compileToFunctions来生成render函数的,而在这传入了用户可选配置delimiters分隔符及comments是否保留注释。
compileToFunctions
接下来我们分析下compileToFunctions的实现,其定义在platform/web/compiler中
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
其代码看起来很简单,就是引入了createCompiler并传入参数baseOptions。实际上这边就是将与平台相关的baseOptions定义在了platform目录下面,通过柯里化的实现将平台相关的配置分离在不同目录下,最后再调用统一的编译函数createCompiler。
baseOptions中定义了和平台相关的配置,如modules(clsaa style model的编译),directives(text html model的编译)及平台相关的保留标签,特殊行为标签等。
createCompiler
接着我们进入到createCompiler实现的分析
export const createCompiler = createCompilerCreator(function baseCompile (
template,
options
) {
// 1
const ast = parse(template.trim(), options)
// 2
if (options.optimize !== false) {
optimize(ast, options)
}
// 3
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
在createCompiler函数中实际可以看到我们编译的全貌,编译三步曲
-
parse:将字符粗模板template解析为AST
-
optimize:优化AST,实际就是操作AST,对其内容进行转化
-
generate:生成代码字符串,就是将AST再转化为字符串
实际上这三步也对应着我之前分析过的babel编译原理中的解析->转化->生成。
当然,我们分析的是baseCompile中的逻辑,我们最终的函数实际由createCompilerCreator生成的,所以我们还是回到入口的分析上。
createCompilerCreator
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template,
options
): CompiledResult {
// 1
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}
if (options) {
// ...
// 2
// 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
)
}
// 3
// copy other options
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
finalOptions.warn = warn
// 4
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)
}
}
}
createCompilerCreator的代码算不上简短,但是逻辑和步骤比较明确且简单,主要是对参数做些预处理,最后再返回新的函数compileToFunctions
-
拷贝平台编译的默认配置
baseOptions -
合并开发者传入的配置选项
modules及directives -
开发者配置替代默认配置除
modules和directives,可以看出不同的配置有不同的策略 -
将处理好的最终配置传入
baseCompile并进行上面提到的编译三步曲
createCompileToFunctionFn
在上面我们分析的操作都是定义在compile函数中的逻辑,在最终的返回值中,实际还会将compile作为参数传给createCompileToFunctionFn。
我们接下来再看看createCompileToFunctionFn的实现
export function createCompileToFunctionFn (compile: Function): Function {
// 1
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
// ...
// 2
// check cache
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// 3
// compile
const compiled = compile(template, options)
// ...
// 4
// turn code into functions
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
// ...
return (cache[key] = res)
}
}
为了避免贴的源码过长,我省略了一些在开发环境中的错误提示代码,但感觉还是有必要说一说省略的三处逻辑
-
检查当前运行环境是否能运行
new Function,如果不能(配置了无法运行new Function的CSP)则报错。因为编译将字符串转化成函数就是通过new Function实现的。 -
检查
compiled的errors/tips配置,应该是和sourceMap相关。 -
编译中出现错误的抛出,例如
Failed to generate render function
说完了被我们打上省略号的步骤,我们再来分析下createCompileToFunctionFn的主要逻辑。
-
定义了闭包变量
cache用于存储模板编译结果,因为template是实际是不可变的字符串,无论数据如何变化,模板是一样的,所以我们可以存下它的编译结果,只在第一次进行编译就行。 -
模板的缓存逻辑,在这可以看到对于同一段模板,我们将用户配置
delimiters也存进缓存的key,因为这有可能会影响同一个模板编译结果。 -
实际是将上步骤中定义的函数
compile在此进行真正的执行 -
将
compile的结果字符串通过new Function生成函数
我们最先在入口看到的函数实际就是执行了本步返回值compileToFunctions,通过函数名就能知道。只是vue通过了不断的闭包,函数返回函数,将不同的步骤拆分到不同的函数中执行,所以刚开始分析的时候会感觉藏得挺深的。
梳理
我们前面从入口开始,通过一步步的分析最后纠出最终的编译函数compileToFunctions,有些层层递进的感觉,也有点云里雾里的感觉。那我们再来做个全面的梳理,进一步理解各函数的逻辑关系。
结语
编译入口的分析比较简单,主要是了解下vue中不同的模块中对入口进行了不同的预处理及配置传入。后面我们将继续分析下vue中编译实现的三个主要逻辑。