【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前后台异 ...
随机推荐
- HDU 4960 (水dp)
Another OCD Patient Problem Description Xiaoji is an OCD (obsessive-compulsive disorder) patient. Th ...
- elasticsearch 安装ik中文分词
https://blog.csdn.net/c5113620/article/details/79339541
- nyoj_176_队花的烦恼二_201404262008
队花的烦恼二 时间限制:3000 ms | 内存限制:65535 KB 难度:4 描述 ACM队队花C小+最近在X大OJ上做题,竟发现了一道做不出来的…水题!她快郁闷死了……也许是最近状态不太 ...
- 学习日常笔记<day09>Http协议
1 Http协议入门 1.1 什么是http协议 http协议: 对浏览器客户端 和 服务器端 之间数据传输的格式规范 1.2 查看http协议的工具 1)使用火狐的firebug插件(右键-> ...
- DTRACE简介(2)
By samwan on 三月 21, 2007 通过上一次的介绍,相信大家对DTRACE已经有了一个初步的认识.上一次结束时专门留了一个例子,可能大家第一次看有很多不明白的地方,没有关系,随着我们对 ...
- Android GIS开发系列-- 入门季(12) 显示载天地图
在项目中可以经常需要动态加载一些图层,像投影地图服务.投影地图服务器.其实网上有大量这样的服务,比如天地图官网, . 随便点开一个服务,里面有相关的信息.那如何加载这样图层服务呢. 一.首先感谢这篇博 ...
- 建立DHCPserver
一.实验的目的: 实现以下的要求的DHCPserver,了解子网内的IP分配的情况. 二.实验目标 虚拟机 vm1:192.168.6.3/24属于子网VMnet8. 在其上建立DHCPser ...
- ubuntu下vi的使用
ubuntu下vi的使用 ssh之后对于server的文件,我习惯用gedit,可是不好改动,于是就用vi. 1.vi的基本概念 基本上vi能够分为三种状态,各自是命令模式(command mode) ...
- Jquery第四课 Javascript中this的使用方法
thiskeyword的使用 在JavaScript中使用thiskeyword表示调用方法的对象,这与大部分面向对象语言是一样的.可是因为call.apply.bind等函数的影响.我们能够改变th ...
- mysql正则表达式及应用
mysql where子句的模式匹配 今天在应用中遇到了这样的一个问题,有一个字段 t1,其中的值类似于:1,1,1,2,3,3,4,4,5,5,2,4,3,2,1,2 需要从里面搜索出比如说:第一个 ...