「这是我参与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中编译实现的三个主要逻辑。