最近看到掘金、前端公众号好多 ES2020 的文章,想说一句:放开我,我还学得动!
先问大家一句,日常项目开发中你能离开 ES6 吗?
一、前言
对于前端同学来说,编译器可能适合神奇的魔盒🎁,表面普通,但常常给我们惊喜。
编译器,顾名思义,用来编译,编译什么呢?当然是编译代码咯🌹。
其实我们也经常接触到编译器的使用场景:
- React 中 JSX 转换成 JS 代码;
- 通过 Babel 将 ES6 及以上规范的代码转换成 ES5 代码;
- 通过各种 Loader 将 Less / Scss 代码转换成浏览器支持的 CSS 代码;
- 将 TypeScript 转换为 JavaScript 代码。
- and so on...
使用场景非常之多,我的双手都数不过来了。😄
虽然现在社区已经有非常多工具能为我们完成上述工作,但了解一些编译原理是很有必要的。接下来进入本文主题:200行JS代码,带你实现代码编译器。
二、编译器介绍
2.1 程序运行方式
现代程序主要有两种编译模式:静态编译和动态解释。推荐一篇文章《Angular 2 JIT vs AOT》介绍得非常详细。
静态编译
简称 AOT(Ahead-Of-Time)即 提前编译 ,静态编译的程序会在执行前,会使用指定编译器,将全部代码编译成机器码。
(图片来自:segmentfault.com/a/119000000…)
在 Angular 的 AOT 编译模式开发流程如下:
- 使用 TypeScript 开发 Angular 应用
- 运行 ngc 编译应用程序
- 使用 Angular Compiler 编译模板,一般输出 TypeScript 代码
- 运行 tsc 编译 TypeScript 代码
- 使用 Webpack 或 Gulp 等其他工具构建项目,如代码压缩、合并等
- 部署应用
动态解释
简称 JIT(Just-In-Time)即 即时编译 ,动态解释的程序会使用指定解释器,一边编译一边执行程序。
在 Angular 的 JIT 编译模式开发流程如下:
- 使用 TypeScript 开发 Angular 应用
- 运行 tsc 编译 TypeScript 代码
- 使用 Webpack 或 Gulp 等其他工具构建项目,如代码压缩、合并等
- 部署应用
AOT vs JIT
AOT 编译流程:
JIT 编译流程:
特性 | AOT | JIT |
编译平台 | (Server) 服务器 | (Browser) 浏览器 |
编译时机 | Build (构建阶段) | Runtime (运行时) |
包大小 | 较小 | 较大 |
执行性能 | 更好 | - |
启动时间 | 更短 | - |
除此之外 AOT 还有以下优点:
- 在客户端我们不需要导入体积庞大的 angular 编译器,这样可以减少我们 JS 脚本库的大小
- 使用 AOT 编译后的应用,不再包含任何 HTML 片段,取而代之的是编译生成的 TypeScript 代码,这样的话 TypeScript 编译器就能提前发现错误。总而言之,采用 AOT 编译模式,我们的模板是类型安全的。
2.2 现代编译器工作流程
摘抄维基百科中对 编译器工作流程介绍:
一个现代编译器的主要工作流程如下: 源代码(source code)→ 预处理器(preprocessor)→ 编译器(compiler)→ 汇编程序(assembler)→ 目标代码(object code)→ 链接器(linker)→ 可执行文件(executables),最后打包好的文件就可以给电脑去判读运行了。
这里更强调了编译器的作用:将原始程序作为输入,翻译产生目标语言的等价程序。
目前绝大多数现代编译器工作流程基本类似,包括三个核心阶段:
- 解析(Parsing) :通过词法分析和语法分析,将原始代码字符串解析成抽象语法树(Abstract Syntax Tree);
- 转换(Transformation):对抽象语法树进行转换处理操作;
- 生成代码(Code Generation):将转换之后的 AST 对象生成目标语言代码字符串。
三、编译器实现
本文将通过 The Super Tiny Compiler 源码解读,学习如何实现一个轻量编译器,最终实现将下面原始代码字符串(Lisp 风格的函数调用)编译成 JavaScript 可执行的代码。
Lisp 风格(编译前) | JavaScript 风格(编译后) | |
2 + 2 | (add 2 2) | add(2, 2) |
4 - 2 | (subtract 4 2) | subtract(4, 2) |
2 + (4 - 2) | (add 2 (subtract 4 2)) | add(2, subtract(4, 2)) |
话说 The Super Tiny Compiler 号称可能是有史以来最小的编译器,并且其作者 James Kyle 也是 Babel 活跃维护者之一。
让我们开始吧~
3.1 The Super Tiny Compiler 工作流程
现在对照前面编译器的三个核心阶段,了解下 The Super Tiny Compiler 编译器核心工作流程:
图中详细流程如下:
- 执行入口函数,输入原始代码字符串作为参数;
// 原始代码字符串 (add 2 (subtract 4 2))
- 进入解析阶段(Parsing),原始代码字符串通过词法分析器(Tokenizer)转换为词法单元数组,然后再通过 语法分析器(Parser)将词法单元数组转换为抽象语法树(Abstract Syntax Tree 简称 AST),并返回;
- 进入转换阶段(Transformation),将上一步生成的 AST 对象 导入转换器(Transformer),通过转换器中的遍历器(Traverser),将代码转换为我们所需的新的 AST 对象;
- 进入代码生成阶段(Code Generation),将上一步返回的新 AST 对象通过代码生成器(CodeGenerator),转换成 JavaScript Code;
- 代码编译结束,返回 JavaScript Code。
上述流程看完后可能一脸懵逼,不过没事,请保持头脑清醒,先有个整个流程的印象,接下来我们开始阅读代码:
3.2 入口方法
首先定义一个入口方法 compiler
,接收原始代码字符串作为参数,返回最终 JavaScript Code:
// 编译器入口方法 参数:原始代码字符串 input function compiler(input) { let tokens = tokenizer(input); let ast = parser(tokens); let newAst = transformer(ast); let output = codeGenerator(newAst); return output; }
3.3 解析阶段
在解析阶段中,我们定义词法分析器方法tokenizer
和语法分析器方法parser
然后分别实现:
// 词法分析器 参数:原始代码字符串 input function tokenizer(input) {}; // 语法分析器 参数:词法单元数组tokens function parser(tokens) {};