【js】JavaScript parser实现浅析
最近笔者的团队迁移了webpack2,在迁移过程中,笔者发现webpack2中有相当多的兼容代码,虽然外界有很多声音一直在质疑作者为什么要破坏性更新,其实大家也都知道webpack1那种过于“灵活”的配置方式是有待商榷的,所以作者才会在webpack2上进行了很多规范,但是,笔者却隐隐的觉得,等到webpack3的时候,估计会有更多的破坏性更新,不然也不会有这个webpack2了。于是心中有关webpack的话题便也搁置了,且等它更稳定一些,再谈不迟,今天先来讲讲在剧烈的版本变化中,不变的部分。
大家都知道,webpack是做模块绑定用的,那么就不得不牵涉到语法解析的内容,而且其极高的扩展性,也往往需要依赖于语法解析,而在webpack内部使用acorn做语法解析,类似的还有babel使用的babylon,今天就带来两者的简要分析。
官方给两者的定义都叫做JavaScript parser,内部也一致的使用了AST(Abstract syntax tree,即抽象语法树)的概念。如果对这个概念不明白的同学可以参考WIKIAST的解释
因为babylon引用了flow,eslint等一些checker,所以整个项目结构相当的规范,笔者仅已7.0.0为例:
文件夹目录如下:
index.js //程序入口,会调用parser进行初始化
types.js //定义了基本类型和接口
options.js //定义获取配置的方法以及配置的缺省值
parser //所有的parser都在此
index.js //parser入口类,继承自 StatementParser 即 ./statement.js
statement.js //声明StatementParser 继承自 ExpressionParser 即 ./expression.js
expression.js //声明ExpressionParser 继承自 LValParser 即 ./lval.js
lval.js //声明 LValParser 继承自 NodeUtils 即 ./node.js
node.js //声明 NodeUtils 继承自 UtilParser 即 ./util.js, 同时还实现了上一级目录中types.js 的nodebase接口为Node类
util.js //声明 UtilParser 继承自 Tokenizer 即 ../tokenizer/index.js
location.js //声明 LocationParser 主要用于抛出异常 继承自 CommentsParser 即./comments.js
comments.js //声明 CommentsParser 继承自 BaseParser 即./base.js
base.js //所有parser的基类
plugins
tokenizer
index.js //定义了 Token类 继承自上级目录parser的LocationParser 即 ../parser/location.js
util
大概流程是这样的:
1、首先调用index.js的parse;
2、实例化一个parser对象,调用parser对象的parse方法,开始转换;
3、初始化node开始构造ast;
1) node.js 初始化node
2) tokenizer.js 初始化token
3) statement.js 调用 parseBlockBody,开始解析。这个阶段会构造File根节点和program节点,并在parse完成之后闭合
4) 执行parseStatement, 将已经合法的节点插入到body中。这个阶段会产生各种*Statement type的节点
5)分解statement, parseExpression。这个阶段除了产生各种expression的节点以外,还将将产生type为Identifier的节点
6) 将上步骤中生成的原子表达式,调用toAssignable ,将其参数归类
4、迭代过程完成后,封闭节点,完成body闭合
不过在笔者看来,babylon的parser实现似乎并不能称得上是一个很好的实现,而实现中往往还会使用的forward declaration(类似虚函数的概念),如下图
一个“+”在方法前面的感觉就像是要以前的IIFE一样。。
有点扯远了,总的来说依然是传统语法分析的几个步骤,不过笔者在读源码的时候一直觉得蛮奇怪的为何他们内部要使用继承来实现parser,parser的场景更像是mixin或者高阶函数的场景,不过后者在具体处理中确实没有继承那样清晰的结构。
说了这么多,babylon最后会生成什么呢?以es2016的幂运算“3 ** 2”为例:
{
"type": "File",
"start": 0,
"end": 7,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 7
}
},
"program": {
"type": "Program",
"start": 0,
"end": 7,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 7
}
},
"sourceType": "script",
"body": [
{
"type": "ExpressionStatement",
"start": 0,
"end": 7,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 7
}
},
"expression": {
"type": "BinaryExpression",
"start": 0,
"end": 6,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 6
}
},
"left": {
"type": "NumericLiteral",
"start": 0,
"end": 1,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 1
}
},
"extra": {
"rawValue": 3,
"raw": "3"
},
"value": 3
},
"operator": "**",
"right": {
"type": "NumericLiteral",
"start": 5,
"end": 6,
"loc": {
"start": {
"line": 1,
"column": 5
},
"end": {
"line": 1,
"column": 6
}
},
"extra": {
"rawValue": 2,
"raw": "2"
},
"value": 2
}
}
}
],
"directives": []
}
}
完整的列表看着未免有些可怕,笔者将有关location信息的去除之后,构造了以下这个对象:
{
"type": "File",
"program": {
"type": "Program",
"sourceType": "script",
"body": [{
"type": "ExpressionStatement",
"expression": {
"type": "BinaryExpression",
"left": {
"type": "NumericLiteral",
"value": 3
},
"operator": "**",
"right": {
"type": "NumericLiteral",
"value": 2
}
}
}]
}
}
可以看出,这个类AST的的对象是内部,大部分内容是其实是有关位置的信息,因为很大程度上,需要以这些信息去描述这个node的具体作用。
然后让我们再来看看webpack使用的acorn:
也许是acorn的作者和笔者有类似阅读babylon的经历,觉得这种实现不太友好。。于是,acorn的作者用了更为简单直接的实现:
index.js //程序入口 引用了 ./state.js 的Parser类
state.js //构造Parser类
parseutil.js //向Parser类 添加有关 UtilParser 的方法
statement.js //向Parser类 添加有关 StatementParser 的方法
lval.js //向Parser类 添加有关 LValParser 的方法
expression.js //向Parser类 添加有关 ExpressionParser 的方法
location.js //向Parser类 添加有关 LocationParser 的方法
scope.js //向Parser类 添加处理scope的方法 identifier.js
locutil.js
node.js
options.js
tokencontext.js
tokenize.js
tokentype.js
util.js
whitespace.js
虽然内部实现基本是类似的,有很多连方法名都是一致的(注释中使用的类名在acorn中并没有实现,只是表示具有某种功能的方法的集合),但是在具体实现上,acorn不可谓不暴力,连多余的目录都没有,所有文件全在src目录下,其中值得一提的是它并没有使用继承的方式,而是使用了对象扩展的方式来实现的Parser类,如下图:
在具体的文件中,直接扩展Paser的prototype
没想到笔者之前戏谈的mixin的方式真的就这样被使用了,然而mixin的可读性一定程度上还要差,经历过类似ReactComponentWithPureRenderMixin的同学想必印象尤深。
不过话说回来,acorn内部实现与babylon并无二致,连调用的方法名都是类似的,不过acorn多实现了一个scope的概念,用于限制作用域。
紧接着我们来看一下acorn生成的结果,以“x ** y”为例:
{
type: "Program",
body: [{
type: "ExpressionStatement",
expression: {
type: "BinaryExpression",
left: {
type: "Identifier",
name: "x",
loc: {
start: {
line: 1,
column: 0
},
end: {
line: 1,
column: 1
}
}
},
operator: "**",
right: {
type: "Identifier",
name: "y",
loc: {
start: {
line: 1,
column: 5
},
end: {
line: 1,
column: 6
}
}
},
loc: {
start: {
line: 1,
column: 0
},
end: {
line: 1,
column: 6
}
}
},
loc: {
start: {
line: 1,
column: 0
},
end: {
line: 1,
column: 6
}
}
}],
loc: {
start: {
line: 1,
column: 0
},
end: {
line: 1,
column: 6
}
}
}, {
ecmaVersion: 7,
locations: true
}
可以看出,大部分内容依然是位置信息,我们照例去掉它们:
{
type: "Program",
body: [{
type: "ExpressionStatement",
expression: {
type: "BinaryExpression",
left: {
type: "Identifier",
name: "x",
},
operator: "**",
right: {
type: "Identifier",
name: "y", }
}
}]
}
除去一些参数上的不同,最大的区别可能就是最外层babylon还有一个File节点,而acorn的根节点就是program了,毕竟babel和webpack的工作场景还是略有区别的。
也许,仅听笔者讲述一切都那么简单,然而这只是理想情况,现实的复杂远超我们的想象,简单的举个印象比较深的例子,在两个parser都有有关whitespace的抽象,主要是用于提供一些匹配换行符的正则,通常都想到的是:
/\r\n?|\n/
但实际上完整的却是
/\r\n?|\n|\u2028|\u2029/
而且考虑到ASCII码的情况,还需要很纠结的枚举出非空格的情况
/[\u1680\u180e\u2000-\u200a\u202f\u205f\u3000\ufeff]/
因为parse处理的是我们实际开发中自己coding的代码,不同的人不同的风格,会有怎么样的奇怪的方式其实是非常考验完备性思维的一项工作,而且这往往比我们日常的业务工作的场景更为复杂,它很多时候甚至是接近一个可能性的全集,而并非“大概率可能”的一个集合。虽然我们日常工作这种parser几乎是透明的,我们在init的前端项目时基本已经部署好了开发环境,但是对于某些情况下的实际问题定位,却又有非凡的意义,而且,这还在一定时间内是一个常态,虽然可能在不久的未来,就会有更加智能更加强大的前端IDE。
有关ast的实验,可以试一下这个网站:https://astexplorer.net/
【js】JavaScript parser实现浅析的更多相关文章
- JavaScript parser
JavaScript parser 和上面功能有点像,折叠JS代码,快速找到JS中类,方法的工具
- javascript订阅模式浅析和基础实例
前言 最近在开发redux或者vux的时候,状态管理当中的createStore,以及我们在组件中调用的dispatch传递消息给状态管理中心,去处理一些操作的时候,有些类似我们常见到订阅模式 于是写 ...
- Atitit.js javascript的rpc框架选型
Atitit.js javascript的rpc框架选型 1. Dwr1 2. 使用AJAXRPC1 2.2. 数据类型映射表1 3. json-rpc轻量级远程调用协议介绍及使用2 3.1. 2.3 ...
- Immutable.js – JavaScript 不可变数据集合
不可变数据是指一旦创建就不能被修改的数据,使得应用开发更简单,允许使用函数式编程技术,比如惰性评估.Immutable JS 提供一个惰性 Sequence,允许高效的队列方法链,类似 map 和 f ...
- Js(javaScript)的闭包原理
问题?什么是js(javaScript)的闭包原理,有什么作用? 一.定义 官方解释:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. 小编 ...
- 【转】Eclipse去除js(JavaScript)验证错误
这篇文章主要是对Eclipse去除js(JavaScript)验证错误进行了介绍.在Eclipse中,js文件常常会报错.可以通过如下几个步骤解决 第一步:去除eclipse的JS验证:将window ...
- thrift js javascript C# Csharp webservice
http://www.cnblogs.com/xxxteam/archive/2013/04/15/3023159.html 利用thrift实现js与C#通讯的例子 关键字:thrift js ja ...
- Atitit.js javascript异常处理机制与java异常的转换 多重catc hDWR 环境 .js exception process Vob7
Atitit.js javascript异常处理机制与java异常的转换 多重catc hDWR 环境 .js exception processVob7 1. 1. javascript异常处理机制 ...
- Atitit.js javascript异常处理机制与java异常的转换.js exception process Voae
Atitit.js javascript异常处理机制与java异常的转换.js exception processVoae 1. 1. javascript异常处理机制 1 2. 2. Web前后台异 ...
随机推荐
- SpringBoot入门系列~Spring-Data-JPA自动建表
1.pom.xml引入Spring-Data-Jpa和mysql依赖 <!-- Spring-data-jpa依赖 --> <dependency> <groupId&g ...
- Codeforces870F. Paths
n<=10000000的图,满足:如果(i,j)>1就连一条边权1的无相变,问所有d(u,v) (u<=v)--u到v的最短路之和. 首先1和>n/2的质数都是孤立的点.然后两 ...
- Delphi:Indy9的IdFTP完全使用
Delphi 7自带的INDY控件,其中包含了IdFTP,可以方便的实现FTP客户端程序,参考自带的例子,其中有上传.下载.删除文件,但是不包含对文件夹的操作,得自己实现上传.下载.删除整个文件夹(带 ...
- 动态演示冒泡排序java
动态演示冒泡排序java //冒泡排序是一种简单的交换排序,基本思路,从数列左边开始扫描元素,在扫描过程中依次对相邻元素进行比较,将较大元素后移. public class NumberSort { ...
- activiti自己定义流程之Spring整合activiti-modeler5.16实例(四):部署流程定义
注:(1)环境搭建:activiti自己定义流程之Spring整合activiti-modeler5.16实例(一):环境搭建 (2)创建流程模型:activiti自己定义流程之Spr ...
- AutoCAD如何编辑块,打散块
选中块之后,点击最右侧的最后一个工具"分解"即可.
- swift编程语言基础教程 中文版
swift编程语言基础教程 中文版 http://download.csdn.net/detail/u014036026/7845491
- 【版本号公布】Jeecg-P3 1.0 公布,J2EE微服务框架(插件开发)
JEECG-P3 1.0 公布了! JEECG-P3 1.0是一个J2EE微服务框架(插件开发). 特点:业务组件以JAR方式提供,插件模式.松耦合.可插拔.支持独立部署,也能够无缝集成Jeecg平台 ...
- AVAudioSessionCategory的选择
AVAudioSessionCategoryAmbient 或 kAudioSessionCategory_AmbientSound --用于非以语音为主的应用,使用这个category的应用会随着静 ...
- 在对象内部尽量直接訪问实例变量 --Effictive Objective-C 抄书
在对象之外訪问实例变量时,应该总是通过属性来做.在那么在对象内部訪问实例变量的时候,又该怎样呢? 这是 OCer们一直激烈讨论的问题.有人觉得,不管什么情况,都应该通过属性来訪问实例变量;也有人说,& ...