Vue 的编译器初探
寻找 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
函数, /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.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 的作用
回顾一下 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 心知肚明之后再来看这部分内容也不迟。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论