【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前后台异 ...
随机推荐
- [bzoj2179]FFT快速傅立叶_FFT
FFT快速傅立叶 bzoj-2179 题目大意:给出两个n位10进制整数x和y,你需要计算x*y. 注释:$1\le n\le 6\times 10^4$. 想法: $FFT$入门题. $FFT$实现 ...
- 条款九: 避免隐藏标准形式的new
因为内部范围声明的名称会隐藏掉外部范围的相同的名称,所以对于分别在类的内部和全局声明的两个相同名字的函数f来说,类的成员函数会隐藏掉全局函数 class x { public: void f(); / ...
- Centos7 samba 匿名共享 简单config
安装Samba yum install samba samba-client samba-common -y 备份原始的Samba配置文件: mv /etc/samba/smb.conf /etc/s ...
- 多路转接模型之poll
poll系统调用和select类似.也是在指定时间内轮询一定数量的文件描写叙述符,以測试当中是否有就绪者.poll和select效率差点儿相同,仅仅是其使用接口相对简单些,poll不在局限于1024个 ...
- Ubuntu如何开启root账户登录
1 首先设置root密码,利用现有管理员帐户登陆Ubuntu,在终端执行命令:sudo passwd root,接着输入密码和root密码,重复密码.这样就有了可用的root用户. 2 打开一个终 ...
- mongodb的备忘录
https://www.cnblogs.com/best/p/6212807.html
- bug统计分析续(一)基于SQL的Bug统计方法
本文由 @lonelyrains 出品.转载请注明出处. 文章链接: http://blog.csdn.net/lonelyrains/article/details/44225533 上一篇为 bu ...
- kafka 生产者消费者 api接口
生产者 import java.util.Properties; import kafka.javaapi.producer.Producer; import kafka.producer.Keyed ...
- Swift 1.1语言第7章 函数和闭包
Swift 1.1语言第7章 函数和闭包 在编程中,随着处理问题的越来越复杂.代码量飞速添加. 当中,大量的代码往往相互反复或者近似反复.假设不採有效方式加以解决.代码将非常难维护. 为了解决问题, ...
- npm won't install packages “npm ERR! network tunneling socket could not be established, cause=Parse Error”
昨天在使用npm安装react-native的时候一直报网络不能connection,可是在浏览器中直接访问时是成功,搜索百度无果,最后在google中找到了这个解决方案:http://stackov ...