Vue 的编译器初探
至此,我们对 Vue.prototype._init
方法所做的初始化工作基本全部讲解到了,在讲解渲染函数的观察者时,我们也讲解了渲染函数是如何生成的以及渲染函数的作用。接下来我们将开启新的篇章,即看一看渲染函数是如何通过编译器生成的。我们打开 src/platforms/web/entry-runtime-with-compiler.js
文件,找到 $mount
方法,该方法中有这样一段代码:
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
我们知道渲染函数 render
就是通过 compileToFunctions
函数生成的,传递给该函数的第一个参数就是模板字符串,compileToFunctions
函数会把模板字符串编译为渲染函数,本章的内容将以 compileToFunctions
函数为切入点研究编译器。
寻找 compileToFunctions
接下来我们的主要工作,就是搞清楚 compileToFunctions
函数,根据 platforms/web/entry-runtime-with-compiler.js
文件头部的 import
引用关系可知,compileToFunctions
函数来自于当前目录下的 ./compiler/index.js
文件,打开 ./compiler/index.js
文件,可以发现这样一句代码:
const { compile, compileToFunctions } = createCompiler(baseOptions)
上面的代码中 compileToFunctions
函数是从 createCompiler
函数的返回值中解构出来的。
由此可知 compileToFunctions
函数是通过以 baseOptions
为参数调用 createCompiler
函数创建出来的。createCompiler
函数顾名思义他的作用就是创建一个编译器,那么到底是怎么创建出来的呢?想搞清楚这个问题我们就需要具体看一下 createCompiler
函数了,根据引用关系可知 createCompiler
函数来自于 compiler/index.js
文件,注意这里的 compiler/index.js
可不是 ./compiler/index.js
,这里的 compiler/index.js
指的是 src/compiler/index.js
文件,我们打开这个文件看一下:
/* @flow */
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 {
const ast = parse(template.trim(), options)
optimize(ast, options)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
以上是 src/compiler/index.js
文件的全部代码,可知这个文件唯一的作用就是导出一个函数,即 createCompiler
函数,该函数就是用来创建编译器的,或者我们可以称该函数为 编译器的创建者
。那么 createCompiler
函数的内容是什么呢?仔细查看代码,我们发现 createCompiler
函数也是通过一个函数创建出来的,这个函数就是 createCompilerCreator
,并且传递了 baseCompile
函数作为参数。也就说 createCompiler
函数的内容是 createCompilerCreator
函数的返回值,其实这么看的话我们倒是可以把 createCompilerCreator
函数称作 '编译器创建者' 的创建者
,我们整理一下思路如下图:
接下来我们需要看一看 '编译器创建者' 的 创建者
是怎么创建出编译器创建者的,也就是 createCompilerCreator
函数的内容,该函数来自于 create-compiler.js
文件,打开该文件找到 createCompilerCreator
函数如下:
/* @flow */
import { extend } from 'shared/util'
import { detectErrors } from './error-detector'
import { createCompileToFunctionFn } from './to-function'
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
// ...
}
}
以上代码是 create-compiler.js
文件的全部内容,只不过做了简化,去掉了 createCompiler
函数的函数体。我们可以发现 createCompilerCreator
函数直接返回了 createCompiler
函数,而这个函数就是我们所说的 编译器的创建者
。那么传递给 createCompilerCreator
函数的参数 baseCompile
在哪里调用的呢?肯定是在 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)
optimize(ast, options)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
我们已经知道一件事,那就是这里的 createCompiler
就是 createCompilerCreator
函数的返回值,也就是 src/compiler/create-compiler.js
文件内的 createCompiler
函数:
export function createCompilerCreator (baseCompile: Function): Function {
// 也就是这个 createCompiler 函数
return function createCompiler (baseOptions: CompilerOptions) {
// ...
}
}
那么现在再看 platforms/web/compiler/index.js
文件下的这句代码:
const { compile, compileToFunctions } = createCompiler(baseOptions)
其实这里调用的 createCompiler
也就是 src/compiler/create-compiler.js
文件的 createCompiler
函数。我们查看一下 src/compiler/create-compiler.js
文件的 createCompiler
函数如下:
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
// ...
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
可以发现 createCompiler
函数的返回值就是一个包含 compileToFunctions
属性的对象:
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
而这里的 compileToFunctions
属性就是 platforms/web/compiler/index.js
文件中解构出来的 compileToFunctions
:
// 这里通过 createCompiler 函数的返回值解构出 compileToFunctions
const { compile, compileToFunctions } = createCompiler(baseOptions)
所以上面代码中执行的 createCompiler
函数实际上就是 compiler/create-compiler.js
文件中的 createCompiler
函数,该函数的返回值包含了真正的编译器 compileToFunctions
,接下来我们就看看 createCompiler
都做了什么,打开 compiler/create-compiler.js
文件找到 createCompiler
函数如下:
export function createCompilerCreator (baseCompile: Function): Function {
// createCompiler 函数作为 createCompilerCreator 函数的返回值
return function createCompiler (baseOptions: CompilerOptions) {
// 定义 compile 函数
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
// ...
}
// 返回 compile 和 compileToFunctions
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
从上面的代码可以看到 createCompiler
函数的内容其实很简单,就是定义了 compile
函数,然后返回一个对象,这个对象包含了 compile
函数本身,同时包含了 compileToFunctions
函数。这就是 createCompiler
所做的内容,但是这就完了吗?还没有,因为我们发现 compileToFunctions
这个函数是通过以 compile
函数作为参数调用 createCompileToFunctionFn
函数生成的,所以我们一直所说的 compileToFunctions
函数其实准确的讲它应该是 createCompileToFunctionFn
函数的返回值,那么我们看看 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 {
...
}
}
以上是 createCompileToFunctionFn
函数的代码,我们发现这个函数的返回值是一个函数,该函数才是我们真正想要的 compileToFunctions
,在返回这个函数之前定义了常量 cache
,所以 cache
常量肯定是被 compileToFunctions
函数引用的,那么这里可以理解为创建了一个闭包,其实如果大家留意的话,在上面的讲解中我们已经遇到了很多利用闭包引用变量的场景,还是拿上面的代码为例,createCompileToFunctionFn
函数接收一个参数 compile
,而这个参数其实也是被 compileToFunctions
闭包引用的。
至此我们经历了一波三折,终于找到了 compileToFunctions
函数,src/platforms/web/entry-runtime-with-compiler.js
文件中执行的 compileToFunctions
函数,其实就是在执行 src/compiler/to-function.js
文件中 createCompileToFunctionFn
函数返回的 compileToFunctions
函数。
compileToFunctions 的作用
经过前面的讲解,我们已经知道了 entry-runtime-with-compiler.js
文件中调用的 compileToFunctions
的真正来源,可以说为了创建 compileToFunctions
函数经历了一波三折,现在大家也许会有疑问,比如为什么要弄的这么复杂?我们在本章的最后为大家解答这个问题。
这个小节我们就以 entry-runtime-with-compiler.js
文件中调用的 compileToFunctions
开始,去探索其背后所做的事情。打开 entry-runtime-with-compiler.js
文件找到这段代码:
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
上面这段代码存在于 Vue.prototype.$mount
函数体内,我们已经知道 compileToFunctions
函数的作用是把传入的模板字符串(template
)编译成渲染函数(render
)的。所以传递给 compileToFunctions
的第一个参数就是模板字符串(template
),而第二个参数则是一些选项(options
),接下来我们先把这里传递的选项对象搞清楚,选项对象如下:
{
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}
其中 shouldDecodeNewlines
和 shouldDecodeNewlinesForHref
这两个变量来自于 platforms/web/util/compat.js
文件,大家可以在附录 platforms/web/util 目录下的工具方法全解 中查看这两个变量的作用,其目的是对浏览器的怪癖做兼容,具体在附录中都有讲到,并且这两个变量的类型都是布尔值。
对于 options.delimiters
和 options.comments
,其中 options
就是当前 Vue
实例的 $options
属性,并且 delimiters
和 comments
都是 Vue
提供的选项。所以这里只是简单的将这两个选项透传了过去。
另外 delimiters
和 comments
这两个选项大家在 Vue
的官方文档都能够找到讲解。而这里我要强调的是在 Vue
官方文档中有特殊说明,即这两个选项只在完整版的 Vue
中可用。这是为什么呢?可能有的同学已经知道了,其原因是这两个选项只有在创建完整版 Vue
的时候才会用到,大家不要忘了 entry-runtime-with-compiler.js
这个文件是完整版 Vue
的入口,也就是说运行时版的 Vue
压根不存在这些内容所以自然不会起作用。
现在我们知道了传递给 compileToFunctions
的选项参数都包括些什么了,同时我们也知道这里的 compileToFunctions
函数实际上就是 src/compiler/to-function.js
文件中的 compileToFunctions
,所以下一步我们将视线转移到 src/compiler/to-function.js
文件中的 compileToFunctions
函数,不过在这之前我还要啰嗦一句,大家注意 compileToFunctions
函数是接收三个参数的,第三个参数是当前 Vue
实例。
打开 src/compiler/to-function.js
文件,找到 compileToFunctions
函数,首先是这三行代码:
// 使用 extend 函数将 options 的属性混合到新的对象中并重新赋值 options
options = extend({}, options)
// 检查选项参数中是否包含 warn,如果没有则使用 baseWarn
const warn = options.warn || baseWarn
// 将 options.warn 属性删除
delete options.warn
首先,使用 extend
函数将选项参数混合到一个新的对象中,然后定义了 warn
常量,其值为 options.warn
或 baseWarn
,如果选项参数中没有 warn
则使用 baseWarn
,其中 baseWarn
是来自于 core/util/debug.js
文件中 warn
的别名,最后将 options.warn
移除。这三行代码的作用主要是用来处理一下选项参数 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.'
)
}
}
}
首先这段代码是在非生产环境下执行的,然后使用 try catch
语句块对 new Function('return 1')
这句代码进行错误捕获,如果有错误发生且错误的内容中包含诸如 'unsafe-eval'
或者 'CSP'
这些字样的信息时就会给出一个警告。我们知道 CSP
全称是内容安全策略,如果你的策略比较严格,那么 new Function()
将会受到影响,从而不能够使用。但是将模板字符串编译成渲染函数又依赖 new Function()
,所以解决方案有两个:
- 1、放宽你的CSP策略
- 2、预编译
总之这段代码的作用就是检测 new Function()
是否可用,并在某些情况下给你一个有用的提示。
接下来是这段代码:
// check cache
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
首先定义常量 key
,其值为一个字符串,我们知道 options.delimiters
是一个数组,如果 options.delimiters
存在,则使用 String
方法将其转换成字符串并与 template
拼接作为 key
的值,否则直接使用 template
字符串作为 key
的值,然后判断 cache[key]
是否存在,如果存在直接返回 cache[key]
。这么做的目的是缓存字符串模板的编译结果,防止重复编译,提升性能,我们再看一下 compileToFunctions
函数的最后一句代码:
return (cache[key] = res)
这句代码在返回编译结果的同时,将结果缓存,这样下一次发现如果 cache
中存在相同的 key
则不需要再次编译,直接使用缓存的结果就可以了。
那么 cache
这个变量是哪里来的?这个变量定义在 compileToFunctions
的前面,也就是 createCompileToFunctionFn
函数的开头,如下:
const cache = Object.create(null)
可知 cache
就是一个通过 Object.create(null)
创建出来的空对象而已。
接下来是这句代码:
// compile
const compiled = compile(template, options)
可以说这句代码才是整个函数最核心的代码,虽然它只要一句,但是它做的事情最多。compile
是通过闭包引用了来自 createCompileToFunctionFn
函数的形参,所以这里的 compile
就是调用 createCompileToFunctionFn
函数时传递过来的函数,打开 src/compiler/create-compiler.js
文件如下:
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
// 函数体...
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
其实前面我们已经提到过,传递给 createCompileToFunctionFn
函数的 compile
参数,就是定义在 createCompiler
函数开头的 compile
函数。所以:
// compile
const compiled = compile(template, options)
这里的 compile
函数就是定义在 src/compiler/create-compiler.js
文件中 createCompiler
函数开头的 compile
函数。
现在大家只需要知道真正的编译工作是依托于 compile
函数的即可,我们后面会详细解析 compile
。接下来我们继续查看 compileToFunctions
代码,下面是这段:
// 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))
}
}
我们知道,在使用 compile
函数对模板进行编译后会返回一个结果 compiled
,通过上面这段代码我们能够猜到,返回结果 compiled
是一个对象且这个对象可能包含两个属性 errors
和 tips
。通过这两个属性的名字可知,这两个属性分别包含了编译过程中的错误和提示信息。所以上面那段代码的作用就是用来检查使用 compile
对模板进行编译的过程中是否存在错误和提示的,如果存在那么需要将其打印出来。
另外,这段代码也是运行在非生产环境的,且错误信息 compiled.errors
和提示信息 compiled.tips
都是数组,需要遍历打印,不同的是错误信息使用 warn
函数进行打印,而提示信息使用 tip
函数进行打印,其中 tip
函数也来自于 core/util/debug.js
文件。
再往下是这样一段代码:
// turn code into functions
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
定义了两个常量 res
以及 fnGenErrors
,其中 res
是一个空对象且它就是最终的返回值,fnGenErrors
是一个空数组。然后在 res
对象上添加一个 render
属性,这个 render
属性,实际上就是最终生成的渲染函数,它的值是通过 createFunction
创建出来的,其中 createFunction
函数就定义在 to-function.js
文件的开头,源码如下:
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}
createFunction
函数接收两个参数,第一个参数 code
为函数体字符串,该字符串将通过 new Function(code)
的方式创建为函数。第二个参数 errors
是一个数组,作用是当采用 new Function(code)
创建函数发生错误时用来收集错误的。我们再查看一下调用 createFunction
那句代码:
res.render = createFunction(compiled.render, fnGenErrors)
可知,传递给 createFunction
函数的第一个参数是 compiled.render
,所以 compiled.render
应该是一个函数体字符串,且我们知道 compiled
是 compile
函数的返回值,这说明:compile
函数编译模板字符串后所得到的是字符串形式的函数体。传递给 createFunction
函数的第二个参数是之前声明的 fnGenErrors
常量,也就是说当创建函数出错时的错误信息被 push
到这个数组里了。
在这句代码之后,又在 res
对象上添加了 staticRenderFns
属性:
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
由这段代码可知 res.staticRenderFns
是一个函数数组,是通过对 compiled.staticRenderFns
遍历生成的,这说明:compiled
除了包含 render
字符串外,还包含一个字符串数组 staticRenderFns
,且这个字符串数组最终也通过 createFunction
转为函数。staticRenderFns
的主要作用是渲染优化,我们后面详细讲解。
再接下来就是 compileToFunctions
函数的最后一段代码:
// 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
)
}
}
这段代码同样是在非生产环境下执行的,这段代码主要的作用是用来打印在生成渲染函数过程中的错误,也就是上面定义的常量 fnGenErrors
中所收集的错误。注释中写的很清楚,这段代码的作用主要是用于开发 codegen
功能时使用,一般是编译器本身的错误,所以对于我们来讲基本用不到。
最后一句代码我们前面已经讲过:return (cache[key] = res)
返回结果的同时将结果缓存。
现在我们回顾一下 src/compiler/to-function.js
文件的整个内容,可以发现这个文件的主要作用有以下几点:
- 1、缓存编译结果,通过
createCompileToFunctionFn
函数内声明的cache
常量实现。 - 2、调用
compile
函数将模板字符串转成渲染函数字符串 - 3、调用
createFunction
函数将渲染函数字符串转成真正的渲染函数 - 4、打印编译错误,包括:模板字符串 -> 渲染函数字符串 以及 渲染函数字符串 -> 渲染函数 这两个阶段的错误
最后,真正的 模板字符串
到 渲染函数字符串
的编译工作实际上是通过调用 compile
函数来完成的,所以接下来我们的任务就是弄清楚 compile
函数。
compile 的作用
回顾一下 src/compiler/to-function.js
文件中的 compileToFunctions
函数调用 compile
函数的方式:
const compiled = compile(template, options)
很简单的一段代码,其中模板字符串 template
被透传了过去,选项参数 options
经过简单处理后继续作为第二个参数传递给 compile
函数,前面我们分析过,这里传递过去的 options
如下:
{
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters,
comments,
warn // 被 delete
}
其中 warn
属性被 delete
操作符删除。这里只是给大家做一个简短的回顾,并且我们对 Vue
的编译器所接收的参数进行归纳,并整理了附录 编译器选项整理,后面遇到的任何编译器选项都会整理到该附录里,大家可以在这里查阅 Vue
编译器所接收的选项。
知道了这些我们就可以去看 compile
函数的代码了,我们知道 compile
函数是 createCompileToFunctionFn
函数的形参,也就是说,compile
函数是被从其他地方传递过来了,其实前面我们都分析过,这里的 compile
函数就是 src/compiler/create-compiler.js
文件中定义在 createCompiler
函数内的 compile
函数,如下:
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
// 就是这个 compile 函数
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
// 函数体 ...
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
可以发现,compile
函数接收两个参数,分别是模板字符串(template
)和选项参数(options
)。我们顺序的查看其函数体代码,首先是这句代码:
const finalOptions = Object.create(baseOptions)
这句代码通过 Object.create
函数以 baseOptions
为原型创建 finalOptions
常量,finalOptions
才是最终的编译选项参数。这里的 baseOptions
是 createCompiler
函数的形参,也就是在 src/platforms/web/compiler/index.js
文件中调用 createCompiler
传递过来的参数:
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
可以看到 baseOptions
来自于 src/platforms/web/compiler/options.js
文件,下面是该文件的全部代码:
/* @flow */
import {
isPreTag,
mustUseProp,
isReservedTag,
getTagNamespace
} from '../util/index'
import modules from './modules/index'
import directives from './directives/index'
import { genStaticKeys } from 'shared/util'
import { isUnaryTag, canBeLeftOpenTag } from './util'
export const baseOptions: CompilerOptions = {
expectHTML: true,
modules,
directives,
isPreTag,
isUnaryTag,
mustUseProp,
canBeLeftOpenTag,
isReservedTag,
getTagNamespace,
staticKeys: genStaticKeys(modules)
}
还是比较简短的,这个文件的主要作用就是导出一个对象,即我们说到的 baseOptions
,所以下面我们就把 baseOptions
这个对象的内容搞清楚。
对象如下:
{
expectHTML: true,
modules,
directives,
isPreTag,
isUnaryTag,
mustUseProp,
canBeLeftOpenTag,
isReservedTag,
getTagNamespace,
staticKeys: genStaticKeys(modules)
}
我们一个一个看,第一个属性 expectHTML
被设置为 true
。第二个属性是 modules
,根据引用关系可知它来自于 platforms/web/compiler/modules/index.js
文件,打开这个文件:
import klass from './class'
import style from './style'
import model from './model'
export default [
klass,
style,
model
]
以上是该文件的全部代码,可以发现 modules
实际上就是一个数组,数组有三个元素 klass
、style
以及 model
,并且这三个元素来自于当前目录下的三个相应名称的 js
文件。简单查看这三个文件的输出,如下:
// klass.js 的输出
export default {
staticKeys: ['staticClass'],
transformNode,
genData
}
// style.js 的输出
export default {
staticKeys: ['staticStyle'],
transformNode,
genData
}
// model.js 的输出
export default {
preTransformNode
}
可以看到这三个文件输出的都是对象,且 klass.js
文件与 style.js
文件的输出基本相同,只有 staticKeys
字段有所区别,而 model.js
文件输出的对象只包含 preTransformNode
属性。最终 platforms/web/compiler/modules/index.js
文件将这三个文件的输出综合为一个数组进行输出,所以其输出的内容为:
[
{
staticKeys: ['staticClass'],
transformNode,
genData
},
{
staticKeys: ['staticStyle'],
transformNode,
genData
},
{
preTransformNode
}
]
以上就是 baseOptions
对象第二个属性 modules
的内容。baseOptions
对象的第三个属性是 directives
,类似 modules
只不过 directives
来自于 platforms/web/compiler/directives/index.js
文件,该文件源码如下:
import model from './model'
import text from './text'
import html from './html'
export default {
model,
text,
html
}
同样类似于 modules
输出,只不过 directives
最终输出的不是数组,而是一个对象,这个对象包含三个属性 model
、text
以及 html
,这三个属性同样来自于当前目录下的三个文件:model.js
、text.js
以及 html.js
文件,我们分别查看这三个文件的输出:
// model.js 的输出
export default function model (
el: ASTElement,
dir: ASTDirective,
_warn: Function
): ?boolean {
// 函数体...
}
// html.js 的输出
export default function html (el: ASTElement, dir: ASTDirective) {
if (dir.value) {
addProp(el, 'innerHTML', `_s(${dir.value})`)
}
}
// text.js 的输出
export default function text (el: ASTElement, dir: ASTDirective) {
if (dir.value) {
addProp(el, 'textContent', `_s(${dir.value})`)
}
}
可以发现,这个三个文件分别输出了三个函数,所以最终 baseOptions
对象的 directives
属性如下:
{
model: function(){},
html: function(){},
text: function(){}
}
它是一个包含三个属性的对象,且属性的值都是函数。
baseOptions
的第四个属性是 isPreTag
,它是一个函数,可以在附录 platforms/web/util 目录下的工具方法全解 中查看其实现讲解,其作用是通过给定的标签名字检查标签是否是 'pre'
标签。
baseOptions
的第五个属性是 isUnaryTag
,它来自于与 options.js
文件同级目录下的 util.js
文件,即 src/platforms/web/compiler/util.js
文件,打开这个文件,找到 isUnaryTag
如下:
export const isUnaryTag = makeMap(
'area,base,br,col,embed,frame,hr,img,input,isindex,keygen,' +
'link,meta,param,source,track,wbr'
)
可以看到 isUnaryTag
是一个通过 makeMap
生成的函数,该函数的作用是检测给定的标签是否是一元标签。
baseOptions
的第六个属性是 mustUseProp
,它是一个函数,可以在附录 platforms/web/util 目录下的工具方法全解 中查看其实现讲解,其作用是用来检测一个属性在标签中是否要使用 props
进行绑定。
baseOptions
的第七个属性是 canBeLeftOpenTag
,它也是一个函数,来自于 src/platforms/web/compiler/util.js
文件,源码如下:
// Elements that you can, intentionally, leave open
// (and which close themselves)
export const canBeLeftOpenTag = makeMap(
'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source'
)
该函数也是一个使用 makeMap
生成的函数,它的作用是检测一个标签是否是那些虽然不是一元标签,但却可以自己补全并闭合的标签。比如 p
标签是一个双标签,你需要这样使用 <p>Some content</p>
,但是你依然可以省略闭合标签,直接这样写:<p>Some content
,且浏览器会自动补全。但是有些标签你不可以这样用,它们是严格的双标签。
baseOptions
的第八个属性是 isReservedTag
,它是一个函数,可以在附录 platforms/web/util 目录下的工具方法全解 中查看其实现讲解,其作用是检查给定的标签是否是保留的标签。
baseOptions
的第九个属性是 getTagNamespace
,它也是一个函数,同样可以在附录 platforms/web/util 目录下的工具方法全解 中查看其实现讲解,其作用是获取元素(标签)的命名空间。
baseOptions
的第十个属性是 staticKeys
,它的值是通过以 modules
为参数调用 genStaticKeys
函数的返回值得到的。其中 modules
就是 baseOptions
的第二个属性,而 genStaticKeys
来自于 src/shared/util.js
文件,大家可以在附录 shared/util.js 文件工具方法全解 中查看该函数的讲解,其作用是根据编译器选项的 modules
选项生成一个静态键字符串。
现在我们已经弄清楚 baseOptions
对象的各个属性都是什么了,这些属性作为编译器的基本参数选项,但是我们还不清楚其各个属性的意义,比如 modules
数组和 directives
对象等,不过不急,随着后面的深入,这些疑惑都将慢慢解开。
现在我们再回到 compile
继续看下面的代码,在创建完 finalOptions
属性之后,又定义了两个常量:errors
和 tips
且它们的值都是数组:
const errors = []
const tips = []
在这之后,是这样一段代码:
finalOptions.warn = (msg, tip) => {
(tip ? tips : errors).push(msg)
}
上面的代码在 finalOptions
上添加了 warn
函数,该函数接收两个参数:1、msg
错误或提示的信息,2、tip
用来标示 msg
是错误还是提示。可以猜想的到 warn
选项主要用在编译过程中的错误和提示收集,如果收集的信息是错误信息就将错误信息添加到前面定义的 errors
数组里,如果是提示信息就将其添加到 tips
数组里。
再往下,是这段代码:
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]
}
}
}
这段代码检查 options
是否存在,这里的 options
就是使用编译器编译模板时传递的选项参数,或者可以简单理解为调用 compileToFunctions
函数时传递的选项参数。其实我们可以把 baseOptions
理解为编译器的默认选项或者基本选项,而 options
是用来提供定制能力的扩展选项。而上面这段代码的作用,就是将 options
对象混合到 finalOptions
中,我们看一下它具体是如何做的。
首先检查 options.modules
是否存在:
// merge custom modules
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
如果存在,就在 finalOptions
对象上添加 modules
属性,其值为 baseOptions.modules
和 options.modules
这两个数组合并后的新数组。
然后检查是否有 options.directives
:
// merge custom directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
)
}
由于 directives
是对象而不是数组,所以不能采用与 modules
相同的处理方式,对于 directives
采用原型链的原理实现扩展属性对基本属性的覆盖。首先通过 Object.create(baseOptions.directives || null)
创建一个以 baseOptions.directives
对象为原型的新对象,然后使用 extend
方法将 options.directives
的属性混合到新创建出来的对象中,并将该对象作为 finalOptions.directives
的值。
最后对于 options
中既不是 modules
又不是 directives
的其他属性,采用直接复制过去的方式进行处理:
// copy other options
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
经过以上步骤,最终的 finalOptions
就已经成型了,我们再看接下来的这句代码:
const compiled = baseCompile(template, finalOptions)
上面的代码调用了 baseCompile
函数,并分别将字符串模板(template
),以及最终的编译器选项(finalOptions
)传递了过去。这说明什么?这说明 compile
函数对模板的编译是委托 baseCompile
完成的。baseCompile
函数是 createCompilerCreator
函数的形参,是在 src/compiler/index.js
文件中调用 createCompilerCreator
创建 '编译器创建者' 的创建者时
传递过来的:
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
optimize(ast, options)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
如上代码 baseCompile
作为 createCompilerCreator
的参数传递过来。不过现在还不是具体查看 baseCompile
代码的时候,我们还是回到 compile
继续查看剩余的代码,再调用 baseCompile
函数之后是这样一段代码:
if (process.env.NODE_ENV !== 'production') {
errors.push.apply(errors, detectErrors(compiled.ast))
}
compiled
是 baseCompile
对模板的编译结果,该结果中包含了模板编译后的抽象语法树(AST),可以通过 compiled.ast
访问该语法树,所以上面这段代码的作用是用来通过抽象语法树来检查模板中是否存在错误表达式的,通过 detectErrors
函数实现,将 compiled.ast
作为参数传递给 detectErrors
函数,该函数最终返回一个数组,该数组中包含了所有错误的收集,最终通过这句代码将错误添加到 errors
数组中:
errors.push.apply(errors, detectErrors(compiled.ast))
最后的一段代码如下:
compiled.errors = errors
compiled.tips = tips
return compiled
将收集到的错误(errors
)和提示(tips
)添加到 compiled
上并返回。至此 compile
函数的工作就结束了。我们做一个简短的回顾,通过上面的分析我们可以明白 compile
函数的作用,它的作用主要有三个:
- 1、生成最终编译器选项
finalOptions
- 2、对错误的收集
- 3、调用
baseCompile
编译模板
补充:上面的分析中,我们并没有深入讲解 detectErrors
函数是如何根据抽象语法树(AST)检查模板中是否存在表达式错误的,这是因为现在对于大家来讲还不清楚抽象语法树的模样,且这并不会对大家的理解造成障碍,所以我们将这部分的讲解后移,等我们对 AST 心知肚明之后再来看这部分内容也不迟。
理解编译器代码的组织方式
如果你看到了这里,也许心里还有一个疑问,好好的代码为什么感觉如此繁琐。实际上你之所以会有繁琐的感觉,是因为你还没有理解源码为什么这么做的原因,当你明白了源码的动机之后就不会有这种感觉了。而本节的内容就是让你进一步理解为什么这样创建编译器。
首先我们来看一下 Vue
源码中编译器的目录结构:
├── src
│ ├── compiler -------------------------- 编译器代码的存放目录
│ ├── ├── codegen ----------------------- 根据AST生成目标平台代码
│ ├── ├── parser ------------------------ 解析原始代码并生成AST
如上目录结构中有两个比较重要的目录,一个是 codegen
目录,另一个是 parser
目录。其中 parser
目录内主要会导出一个叫做 parse
的函数,该函数是一个解析器,它的作用是将模板字符串解析为对应的抽象语法树(AST
),通常我们会像如下代码这样使用 parse
函数:
// 从 parser 目录下的 index.js 文件中导入 parse 函数
import { parse } from './parser/index'
// 使用 parse 函数将模板解析为 AST
const ast = parse(template.trim(), options)
有了 AST
之后我们就可以根据这个 AST
生成不同平台的目标代码,而 codegen
目录内的代码就是用来做这件事情的,codegen
目录内的代码会导出一个叫做 generate
的函数,这个函数的作用就是根据给定的AST生成最终的目标平台的代码,通常我们会像如下代码这样使用 generate
函数:
// 从 codegen 目录下的 index.js 文件中导入 generate 函数
import { generate } from './codegen/index'
// 根据给定的AST生成目标平台的代码
const code = generate(ast, options)
有了这些我们就可以封装一个编译器函数供外部使用:
export function myCompiler (template: string, options: CompilerOptions) {
const ast = parse(template.trim(), options)
const code = generate(ast, options)
return code
}
当然了,在编译的过程中可能会收集一些错误,我们还需要对错误进行处理,所以我们可能会在上面的代码中添加一些用来处理编译错误的代码:
export function myCompiler (template: string, options: CompilerOptions) {
const ast = parse(template.trim(), options)
const code = generate(ast, options)
// 一些处理编译错误的代码
return code
}
这样我们封装的 myCompiler
函数就可以导出供给其他部分的代码使用了,假设我们的 myCompiler
函数用来将模板编译为可以在 web
平台下运行的代码,但是突然有一天你想要根据同样的AST生成其他平台的代码,这时你可以选择再创建一个函数,假设它叫 otherCompiler
:
export function otherCompiler (template: string, options: CompilerOptions) {
const ast = parse(template.trim(), options)
const code = otherGenerate(ast, options)
// 一些处理编译错误的代码
return code
}
如上高亮的代码所示,既然要生成其他平台的代码,那么代码生成部分就需要重写,比如上面的代码中我们使用 otherGenerate
函数代替了原来的 generate
函数。但是AST还是原来的AST,并且用来处理编译错误的代码可能也不会变动,这时 myCompiler
函数和 otherCompiler
函数中就存在了冗余的代码,为了解决这个问题,我们可以封装一个叫做 createCompilerCreator
函数,把通用的代码封装起来,如下:
function createCompilerCreator (baseCompile) {
return customCompiler function (template: string, options: CompilerOptions) {
// 一些处理编译错误的代码
return baseCompile(template, options)
}
}
这样我们就可以使用 createCompilerCreator
函数创建出针对于不同平台的编译器了,如下代码所示:
// 创建 web 平台的编译器
const webCompiler = createCompilerCreator(function baseCompile (template, options) {
const ast = parse(template.trim(), options)
const code = generate(ast, options)
return code
})
// 创建其他平台的编译器
const otherCompiler = createCompilerCreator(function baseCompile (template, options) {
const ast = parse(template.trim(), options)
const code = otherGenerate(ast, options)
return code
})
看到这里相信聪明的你已经明白了为什么会有 src/compiler/create-compiler.js
文件的存在,以及它的作用,实际上该文件中的 createCompilerCreator
函数与我们如上例子中的 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
}
})
实际上这段代码所创建的就是 web
平台下的编译器,大家可以打开 src/server/optimizing-compiler/index.js
文件,你会看到如下这段代码:
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
optimize(ast, options)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
而这段代码是用来创建服务端渲染环境的编译器,注意如上代码中的 generate
函数和 optimize
函数已经是来自 src/server
目录下的相关文件了。
另外与我们前面举的例子不同,/src/compiler/create-compiler.js
文件中的 createCompilerCreator
函数所返回的函数接收的参数是 baseOptions
,所以 src/compiler/index.js
文件中导出的 createCompiler
函数就会接收 baseOptions
参数,这就是为什么在 src/platforms/web/compiler/index.js
会像如下这样调用 createCompiler
函数:
const { compile, compileToFunctions } = createCompiler(baseOptions)
如上代码中传递的 baseOptions
将作为编译器的基本参数,另外我们注意如上代码中 createCompiler
函数的返回值,它返回的是一个对象,对象中包含两个元素,分别是 compile
和 compileToFunctions
,实际上 compile
函数与 compileToFunctions
函数的区别就在于 compile
函数生成的是字符串形式的代码,而 compileToFunctions
生成的才是真正可执行的代码,并且 compileToFunctions
函数本身是使用 src/compiler/to-function.js
文件中的 createCompileToFunctionFn
函数根据 compile
生成的:
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
而且 compileToFunctions
函数中调用了 compile
函数,如下:
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
// compile
const compiled = compile(template, options)
}
}
如上高亮的代码所示,在调用 compile
函数时传递了 template
参数和 options
参数。这两个参数都是通过 compileToFunctions
函数传递过来的。我们找到 src/platforms/web/entry-runtime-with-compiler.js
文件,注意如下代码:
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
大家注意如上代码中调用 compileToFunctions
函数时传递的第二个选项参数,还记得在 src/platforms/web/compiler/index.js
中创建 compileToFunctions
函数时传递的基本选项吗:
const { compile, compileToFunctions } = createCompiler(baseOptions)
所以看到这里,你应该知道的是:在创建编译器的时候传递了基本编译器选项参数,当真正使用编译器编译模板时,依然可以传递编译器选项,并且新的选项和基本选项会以合适的方式融合或覆盖。