Pārlūkot izejas kodu

supplement: understanding the compiler

HcySunYang 7 gadi atpakaļ
vecāks
revīzija
5b9489029d
1 mainītis faili ar 200 papildinājumiem un 1 dzēšanām
  1. 200 1
      docs/art/80vue-compiler-start.md

+ 200 - 1
docs/art/80vue-compiler-start.md

@@ -194,7 +194,7 @@ export function createCompileToFunctionFn (compile: Function): Function {
 
 ## compileToFunctions 的作用
 
-经过前面的讲解,我们已经知道了 `entry-runtime-with-compiler.js` 文件中调用的 `compileToFunctions` 的真正来源,可以说为了创建 `compileToFunctions` 函数经历了一波三折,现在大家也许会有疑问,比如为什么要弄的这么复杂?我们暂时把这个疑问放在心里,随着我们的深入,大家将会慢慢理解其内涵
+经过前面的讲解,我们已经知道了 `entry-runtime-with-compiler.js` 文件中调用的 `compileToFunctions` 的真正来源,可以说为了创建 `compileToFunctions` 函数经历了一波三折,现在大家也许会有疑问,比如为什么要弄的这么复杂?我们在本章的最后为大家解答这个问题
 
 这个小节我们就以 `entry-runtime-with-compiler.js` 文件中调用的 `compileToFunctions` 开始,去探索其背后所做的事情。打开 `entry-runtime-with-compiler.js` 文件找到这段代码:
 
@@ -812,6 +812,205 @@ return compiled
 
 补充:上面的分析中,我们并没有深入讲解 `detectErrors` 函数是如何根据抽象语法树(AST)检查模板中是否存在表达式错误的,这是因为现在对于大家来讲还不清楚抽象语法树的模样,且这并不会对大家的理解造成障碍,所以我们将这部分的讲解后移,等我们对 AST 心知肚明之后再来看这部分内容也不迟。
 
+## 为什么编译器的创建如此繁琐
+
+如果你看到了这里,也许心里还有一个疑问,好好的代码为什么感觉如此繁琐。实际上你之所以会有繁琐的感觉,是因为你还没有理解源码为什么这么做的原因,当你明白了源码的动机之后就不会有这种感觉了。而本节的内容就是让你进一步理解为什么这样创建编译器。
+
+首先我们来看一下 `Vue` 源码中编译器的目录结构:
+
+```
+├── src
+│   ├── compiler -------------------------- 编译器代码的存放目录
+│   ├── ├── codegen ----------------------- 根据AST生成目标平台代码
+│   ├── ├── parser ------------------------ 解析原始代码并生成AST
+```
+
+如上目录结构中有两个比较重要的目录,一个是 `codegen` 目录,另一个是 `parser` 目录。其中 `parser` 目录内主要会导出一个叫做 `parse` 的函数,该函数是一个解析器,它的作用是将模板字符串解析为对应的抽象语法树(`AST`),通常我们会像如下代码这样使用 `parse` 函数:
+
+```js
+// 从 parser 目录下的 index.js 文件中导入 parse 函数
+import { parse } from './parser/index'
+
+// 使用 parse 函数将模板解析为 AST
+const ast = parse(template.trim(), options)
+```
+
+有了 `AST` 之后我们就可以根据这个 `AST` 生成不同平台的目标代码,而 `codegen` 目录内的代码就是用来做这件事情的,`codegen` 目录内的代码会导出一个叫做 `generate` 的函数,这个函数的作用就是根据给定的AST生成最终的目标平台的代码,通常我们会像如下代码这样使用 `generate` 函数:
+
+```js
+// 从 codegen 目录下的 index.js 文件中导入 generate 函数
+import { generate } from './codegen/index'
+
+// 根据给定的AST生成目标平台的代码
+const code = generate(ast, options)
+```
+
+有了这些我们就可以封装一个编译器函数供外部使用:
+
+```js
+export function myCompiler (template: string, options: CompilerOptions) {
+  const ast = parse(template.trim(), options)
+  const code = generate(ast, options)
+
+  return code
+}
+```
+
+当然了,在编译的过程中可能会收集一些错误,我们还需要对错误进行处理,所以我们可能会在上面的代码中添加一些用来处理编译错误的代码:
+
+```js {5}
+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`:
+
+```js {3}
+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` 函数,把通用的代码封装起来,如下:
+
+```js
+function createCompilerCreator (baseCompile) {
+  return customCompiler function (template: string, options: CompilerOptions) {
+
+    // 一些处理编译错误的代码
+
+    return baseCompile(template, options)
+  }
+}
+```
+
+这样我们就可以使用 `createCompilerCreator` 函数创建出针对于不同平台的编译器了,如下代码所示:
+
+```js
+// 创建 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` 文件中的如下这段代码:
+
+```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` 文件,你会看到如下这段代码:
+
+```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` 函数:
+
+```js
+const { compile, compileToFunctions } = createCompiler(baseOptions)
+```
+
+如上代码中传递的 `baseOptions` 将作为编译器的基本参数,另外我们注意如上代码中 `createCompiler` 函数的返回值,它返回的是一个对象,对象中包含两个元素,分别是 `compile` 和 `compileToFunctions`,实际上 `compile` 函数与 `compileToFunctions` 函数的区别就在于 **`compile` 函数生成的是字符串形式的代码,而 `compileToFunctions` 生成的才是真正可执行的代码**,并且 `compileToFunctions` 函数本身是使用 `src/compiler/to-function.js` 文件中的 `createCompileToFunctionFn` 函数根据 `compile` 生成的:
+
+```js
+return {
+  compile,
+  compileToFunctions: createCompileToFunctionFn(compile)
+}
+```
+
+而且 `compileToFunctions` 函数中调用了 `compile` 函数,如下:
+
+```js {12}
+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` 文件,注意如下代码:
+
+```js
+const { render, staticRenderFns } = compileToFunctions(template, {
+  shouldDecodeNewlines,
+  shouldDecodeNewlinesForHref,
+  delimiters: options.delimiters,
+  comments: options.comments
+}, this)
+```
+
+大家注意如上代码中调用 `compileToFunctions` 函数时传递的第二个选项参数,还记得在 `src/platforms/web/compiler/index.js` 中创建 `compileToFunctions` 函数时传递的基本选项吗:
+
+```js
+const { compile, compileToFunctions } = createCompiler(baseOptions)
+```
+
+所以看到这里,你应该知道的是:**在创建编译器的时候传递了基本编译器选项参数,当真正使用编译器变异模板时,依然可以传递编译器选项,并且新的选项和基本选项会以合适的方式融合或覆盖**。
+
+
+
+