月季的花期竟然这么长,五月都过完了,竟然还开着。
先要了解的概念
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.代码生成
将转换后的内容解析成新代码。
解析阶段
解析阶段又可分为两个部分:词法分析
和句法分析
。
词法分析
Lexical Analysis(词法分析)
将原始代码拆分成Tokens
交给tokenizer(分词器)
调用。Tokens
是用来描述独立语法部分的小数组,它可以是数字,标签,标点符号,运算符等等。
句法分析
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',
* }]
* }]
* }]
* }
*/
转化阶段
编译器接下来的阶段就是转化阶段了。这个阶段会将上一步获取的抽象语法树做一些改变。这个操作可以用同一种编成语言,也可以用其他编程语言。
你可能注意到,这个抽象语法树跟我们平时写的嵌套比较深的对象有些类似,每个对象都是一个抽象语法树的节点。
比如一个数字字面量的节点:
代码语言:javascript复制 {
type: 'NumberLiteral',
value: '2',
}
或者一个表达式的节点:
代码语言:javascript复制 {
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'
}]
}]
}]
}
过程可能如下:
Program
从程序的顶端开始。CallExpression
移动到程序体的第一个元素NumberLiteral (2)
移动到程序参数的第一个元素CallExpression (subtract)
移动到程序参数的第二个元素NumberLiteral (4)
移动到CallExpression (subtract)
的第一个参数NumberLiteral(2)
移动到CallExpression (subtract)
的第二个参数
代码生成
编译器的最后一步就是生成代码。大部分时候代码生成仅仅是将抽象语法树转为字符串形式的代码进行返回。
代码生成器以几种不同的方式进行工作,有的会重复使用Tokens
,有的会重新创建一个代码块儿。
当然,这中间有一个递归的过程。(/^▽^)/
总结
这里大概讲了一下编译的过程。整体流程如下:
词法分析
-->Tokens
-->句法分析
-->抽象语法树
-->转化阶段对抽象语法树进一步分析
-->最后是代码生成阶段
-->返回字符串形式的代码
。
感觉跟Vue的流程大体差不多,vue是先将Dom
拆成vnode
虚拟dom,然后对vnode
重新解析
,编译
,最后重新渲染成Dom
。
说到这里,又想到了一个概念DSL
。全称Domain System Lauguage
翻译过来是特定系统语言
,前端似乎用在低代码平台的设计上比较多,个人的理解它似乎更像是一种自定义的代码格式。
比如之前的前端模板art-template
,或者说vue
本身也是依赖模板的框架,而它的{{xxxx}}
语法似乎也可以理解为一种DSL
。
那么,如何自定义一个DSL
,这个问题值得思考一下。
javascript基础知识总结