Vue.js源码学习 —— 初识编译器

如果您是刚开始准备阅读Vue.js的源码,建议先看一下本系列的Vue.js源码学习 —— 起步,相信会对您后面的阅读有很大帮助。

从今天这篇文章开始我们将分析 Vue 编译器部分的代码,这也是一个相对比较独立的模块。我们今天先通过编译器的入口函数一步一步找到它所以实现的地方,每到一步会说一下在这一步做的事情,这样对编译器的实现先建立一个整体印象。

我不会在文章中大量地粘贴源代码,您还是一定要将Vue工程clone到本地哦。

编译器的目录

项目中与编译器相关的有两部分,一部分是编译器的核心实现,代码位于文件夹compiler中。其中子文件夹中放置的内容如下:

  • codegen:生成render函数的代码。

  • directives:生成跨平台的指令的代码,包括:v-modelv-bindv-on

  • parser:解析模板字符串,生成 AST。

还有一部分是与平台特性相关的,每个平台的文件夹下也都有一个compiler文件夹,后面的分析我们都以web平台为主。文件夹platforms/web/compiler中的子文件夹的内容如下,下一节我们会看到这些功能是如何提供给编译器的核心实现部分的。

  • directives:生成与平台相关的指令的代码,包括:v-htmlv-modelv-text

  • modules:与平台相关的几个模块的特殊处理和代码生成,包括:v-bind:classv-bind:style和对input[v-model]动态类型绑定的转换。

找到compileToFunctions被定义的位置

web/entry-runtime-with-compiler.js

在将Vue实例的挂载过程时,在文件web/entry-runtime-with-compiler.js中我们曾遇到过compileToFunctions函数,经过一系列的判断拿到template后会,使用compileToFunctions函数转换成了render函数后放在了vm.$options中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const mount = Vue.prototype.$mount
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函数传了3个参数,这里直接说一下它们的作用。第1个是template,第3个是当前Vue实例,第二个参数是一些编译选项。

  • outputSourceRange:在开发环境下为true,这样生成的 AST 中的每个节点都会有startend字段,以便在编译出错时报告具体的信息。

  • shouldDecodeNewlinesshouldDecodeNewlinesForHref:都是布尔类型,具体的实现是在文件platforms/web/util/compat.js中,是因为有的浏览器会将DOM元素属性值内的换行符\n进行编码,所以需要判断是否需要对换行符解码。

  • delimiters:这个是开放给开发者可以指定的,纯文本插入分隔符,默认是[", "]。比如我们将其改为['${', '}']

    1
    2
    3
    new Vue({
    delimiters: ['${', '}']
    })

    在渲染模板中现在要像下面这样写:

    1
    2
    3
    <li v-for="(item) in pets">
    ${item.name} is a ${item.type}
    </li>
  • comments:这是也是开放给开发者可以指定的,标识渲染模板中的 HTML 注释是否保留。默认行为是舍弃它们。

我们在当前文件的上面找到compileToFunctions函数引入的位置:

1
import { compileToFunctions } from './compiler/index'

现在到./compiler/index中看一下。

web/compiler/index.js

这个文件的内容如下:

1
2
3
4
5
6
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'

const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }

可以看到是在这里调用createCompiler函数返回的compileToFunctions函数,同时传了baseOptions过去。baseOptions中的内容就是我们前面说的与平台相关的方法和变量,在这里提供给编译器的核心使用。打断点看一下它的内容:

我们再接着到compiler/index中看一下。

compiler/index.js

createCompiler函数是由createCompilerCreator函数返回的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// `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中做的才是真正地编译工作,整合了解析器、优化器、代码生成器。这提供了一种灵活性,其他地方要使用时完全可以替换成自己的解析器/优化器/代码生成器。

baseCompile函数的参数templateoptions也就是在文件web/entry-runtime-with-compiler.js中调用compileToFunctions函数时传过来的。

我们再接着到文件compiler/create-compiler.js中看一下。

compiler/create-compiler.js

createCompilerCreator函数的内容就是返回了createCompiler函数,所以这里也是createCompiler函数的实现,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 = []

// ...

// merge custom modules
// ...

// merge custom directives
// ...

// copy other options
// ...

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)
}
}
}

可以看到这里主要是内部定义的这个compile函数。它主要干了这么几件事:

  • 合并选项得到finalOptions。将与平台相关的选项baseOptions放到了finalOptions的对象原型上,然后合并在最开始的compileToFunctions函数中传过来的options,最后得到的finalOptions如下:

  • finalOptions中加上warn来提示错误信息。在这里看到了options.outputSourceRange的使用,如果它为true,会在异常信息中再加上startend,提示出出错范围。

  • 调用baseCompile函数编译模板,返回的结果赋值给了compiledbaseCompile函数的返回是:

    1
    2
    3
    4
    5
    {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
    }
  • 在开发环境下会调用detectErrors函数来检测 AST 中的错误,得到的errorstips会一起放到compiled中,最后将compiled作为compile函数的返回值返回。

createCompiler函数最终的返回是:

1
2
3
4
{
compile, // 返回编译后的原始内容
compileToFunctions: createCompileToFunctionFn(compile) // 会将 compile 返回的内容再转换成 render 函数
}

可以看到compileToFunctions函数最终是调用createCompileToFunctionFn函数得到的,那我们接着来看一下createCompileToFunctionFn函数的实现。

compiler/to-function.js

compileToFunctions函数作为createCompileToFunctionFn函数的返回,主要是利用闭包实现对编译结果的缓存。createCompileToFunctionFn函数的大致轮廓如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null)

return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
// ...

// check cache
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}

// compile
const compiled = compile(template, options)

// ...

// 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)
}
}

这里的compileToFunctions函数正是在web/entry-runtime-with-compiler.js使用的函数,传参也是在那里传进来的。compileToFunctions函数的处理流程很清晰了:

  1. 缓存在有值就从缓存中取,然后返回。

  2. 否则调用上一步传过来的compile函数进行编译。

  3. 将编译后的结果转换成render函数。

  4. 结果存入缓存中,同时返回该结果。

compileToFunctions函数中我们注意到有这样一段代码:

1
2
3
4
5
6
7
8
// detect possible CSP restriction
try {
new Function('return 1')
} catch (e) {
if (e.toString().match(/unsafe-eval|CSP/)) {
// ...
}
}

这主要是在测试new Function在当前环境下是否可用,因为受到网站内容安全策略(CSP)的限制,new Function可能是不允许使用的,此时 Vue 给出的建议是,要么放宽 CSP 的限制,要么预编译渲染模板。

所以将代码字符串转换为函数利用的就是new Function

1
2
3
4
5
6
7
8
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}

我将上面的方法调用关系画了一张图,这样看可能更清晰一些:

下一篇文章我们将分析解释器是如何生成 AST 的。

本文作者:意林
本文链接:http://shinancao.cn/2020/02/22/Vue-Source-Code-15/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!