月季的花期竟然这么长,五月都过完了,竟然还开着。


先要了解的概念

​lisp-like function​​​ 和​​C-like function​

如果对这两个两个概念不熟悉,这里有个简单的示例。假设我们有两个方法​​add​​​和​​subtract​​,它们可能会被写成这样。

/**
* LISP C
*
* 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))
/

看起来很简单,虽然这个不是一个完整的LISP或C语法,它足以演示现代编译器的许多主要部分。

​编译器解析过程​

大多数编译器主要分为三个阶段:​​解析​​​,​​转换​​​和​​代码生成​​。

1.​​解析​​是将原始代码转换成更抽象的代码表示。

2.​​转换​​采用这种抽象表示和操作来完成编译器想的结果。

3.​​代码生成​​将转换后的内容解析成新代码。

​解析阶段​

解析阶段又可分为两个部分:​​词法分析​​​和​​句法分析​​。

  1. ​词法分析​

​Lexical Analysis(词法分析)​​​将原始代码拆分成​​Tokens​​​交给​​tokenizer(分词器)​​​调用。​​Tokens​​是用来描述独立语法部分的小数组,它可以是数字,标签,标点符号,运算符等等。

  1. ​句法分析​

​Syntactic Analysis(句法分析)​​​将​​Tokens​​​重新格式化为能够表示语法各个关系的一个东东。叫做​​abstract syntax tree(抽象语法树)​​。

​抽象语法树(AST)​​,是一个嵌套很深的对象。它可以以一种既易于使用,又能告诉我们大量的代码信息。

例如:对于这个语法​​(add 2 (subtract 4 2))​​​。​​Tokens​​可能是下面的内容

/*
[
{ type: 'paren', value: '(' },
{ type: 'name', value: 'add' },
{ type: 'number', value: '2' },
{ type: 'paren', value: '(' },
{ type: 'name', value: 'subtract' },
{ type: 'number', value: '4' },
{ type: 'number', value: '2' },
{ type: 'paren', value: ')' },
{ type: 'paren', value: ')' },
]
*/

同时​​抽象语法树​​可能像这样:

/*
* {
* type: 'Program',
* body: [{
* type: 'CallExpression',
* name: 'add',
* params: [{
* type: 'NumberLiteral',
* value: '2',
* }, {
* type: 'CallExpression',
* name: 'subtract',
* params: [{
* type: 'NumberLiteral',
* value: '4',
* }, {
* type: 'NumberLiteral',
* value: '2',
* }]
* }]
* }]
* }
*/

​转化阶段​

编译器接下来的阶段就是转化阶段了。这个阶段会将上一步获取的抽象语法树做一些改变。这个操作可以用同一种编成语言,也可以用其他编程语言。

你可能注意到,这个抽象语法树跟我们平时写的嵌套比较深的对象有些类似,每个对象都是一个抽象语法树的节点。

比如一个数字字面量的节点:

{
type: 'NumberLiteral',
value: '2',
}

或者一个表达式的节点:

{
type: 'CallExpression',
name: 'subtract',
params: [...nested nodes go here...],
}

当我们对抽象语法树进行转换的时候,我们开一动态的添加,删除或者替换这些节点,或者我们可以直接clone一份对复制的对象进行修改。

​Traversal(遍历)​​​。在转换的过程中,我们需要一深度优先的方式遍历这个抽象语法树的每个节点。假设我们需要遍历如下的​​AST​​:

{
type: 'Program',
body: [{
type: 'CallExpression',
name: 'add',
params: [{
type: 'NumberLiteral',
value: '2'
}, {
type: 'CallExpression',
name: 'subtract',
params: [{
type: 'NumberLiteral',
value: '4'
}, {
type: 'NumberLiteral',
value: '2'
}]
}]
}]
}

过程可能如下:

  1. ​Program​​从程序的顶端开始。
  2. ​CallExpression​​ 移动到程序体的第一个元素
  3. ​NumberLiteral (2)​​ 移动到程序参数的第一个元素
  4. ​CallExpression (subtract)​​ 移动到程序参数的第二个元素
  5. ​NumberLiteral (4)​​ 移动到​​CallExpression (subtract)​​的第一个参数
  6. ​NumberLiteral(2)​​ 移动到​​CallExpression (subtract)​​的第二个参数

代码生成

编译器的最后一步就是生成代码。大部分时候代码生成仅仅是将抽象语法树转为字符串形式的代码进行返回。

代码生成器以几种不同的方式进行工作,有的会重复使用​​Tokens​​,有的会重新创建一个代码块儿。

当然,这中间有一个递归的过程。(/^▽^)/

总结

这里大概讲了一下编译的过程。整体流程如下:

​词法分析​​​-->​​Tokens​​​-->​​句法分析​​​-->​​抽象语法树​​​-->​​转化阶段对抽象语法树进一步分析​​​-->​​最后是代码生成阶段​​​-->​​返回字符串形式的代码​​。

感觉跟Vue的流程大体差不多,vue是先将​​Dom​​​拆成​​vnode​​​虚拟dom,然后对​​vnode​​​重新​​解析​​​,​​编译​​​,最后重新渲染成​​Dom​​。

说到这里,又想到了一个概念​​DSL​​​。全称​​Domain System Lauguage​​​翻译过来是​​特定系统语言​​,前端似乎用在低代码平台的设计上比较多,个人的理解它似乎更像是一种自定义的代码格式。

比如之前的前端模板​​art-template​​​,或者说​​vue​​​本身也是依赖模板的框架,而它的​​{{xxxx}}​​​语法似乎也可以理解为一种​​DSL​​。

那么,如何自定义一个​​DSL​​,这个问题值得思考一下。

​点击加个关注呗​

最后说两句

  1. 动一动您发财的小手,​「点个赞吧」
  2. 都看到这里了,不妨 ​「加个关注」
  3. 不妨 ​「转发一下」​,好东西要记得分享