最近笔者的团队迁移了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实现浅析的更多相关文章

  1. JavaScript parser

    JavaScript parser 和上面功能有点像,折叠JS代码,快速找到JS中类,方法的工具

  2. javascript订阅模式浅析和基础实例

    前言 最近在开发redux或者vux的时候,状态管理当中的createStore,以及我们在组件中调用的dispatch传递消息给状态管理中心,去处理一些操作的时候,有些类似我们常见到订阅模式 于是写 ...

  3. Atitit.js javascript的rpc框架选型

    Atitit.js javascript的rpc框架选型 1. Dwr1 2. 使用AJAXRPC1 2.2. 数据类型映射表1 3. json-rpc轻量级远程调用协议介绍及使用2 3.1. 2.3 ...

  4. Immutable.js – JavaScript 不可变数据集合

    不可变数据是指一旦创建就不能被修改的数据,使得应用开发更简单,允许使用函数式编程技术,比如惰性评估.Immutable JS 提供一个惰性 Sequence,允许高效的队列方法链,类似 map 和 f ...

  5. Js(javaScript)的闭包原理

    问题?什么是js(javaScript)的闭包原理,有什么作用? 一.定义 官方解释:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分.  小编 ...

  6. 【转】Eclipse去除js(JavaScript)验证错误

    这篇文章主要是对Eclipse去除js(JavaScript)验证错误进行了介绍.在Eclipse中,js文件常常会报错.可以通过如下几个步骤解决 第一步:去除eclipse的JS验证:将window ...

  7. thrift js javascript C# Csharp webservice

    http://www.cnblogs.com/xxxteam/archive/2013/04/15/3023159.html 利用thrift实现js与C#通讯的例子 关键字:thrift js ja ...

  8. Atitit.js javascript异常处理机制与java异常的转换 多重catc hDWR 环境 .js exception process Vob7

    Atitit.js javascript异常处理机制与java异常的转换 多重catc hDWR 环境 .js exception processVob7 1. 1. javascript异常处理机制 ...

  9. Atitit.js javascript异常处理机制与java异常的转换.js exception process Voae

    Atitit.js javascript异常处理机制与java异常的转换.js exception processVoae 1. 1. javascript异常处理机制 1 2. 2. Web前后台异 ...

随机推荐

  1. 【裸的并查集】POJ 1611 The Suspects

    http://poj.org/problem?id=1611 [Accepted] #include<iostream> #include<cstdio> #include&l ...

  2. Django:(7)auth用户认证组件 & 中间件

    用户认证组件 用户认证组件: 功能:用session记录登陆验证状态 前提:用户表:django自带的auth_user 创建超级用户的命令: python manage.py createsuper ...

  3. fastcgi与cgi的区别[转载]

    cgi 在2000年或更早的时候用得比较多, 以前web服务器一般只处理静态的请求,如果碰到一个动态请求怎么办呢?web服务器会根据这次请求的内容,然后会fork一个新进程来运行外部c程序 (或per ...

  4. RelativeLayout属性大全

  5. cocos2dx-3.0(21) 移植android平台 说多了都是泪

    ----我的生活,我的点点滴滴! ! 网上3.0的教程真心少.能够说没有吧,大多都是2.x 或者 3.0測试版之类的,因为我心大,没有照着2.x去搞,后来搞完后总结了一下,发觉事实上3.0的移植and ...

  6. docker+zabbix,使用docker搭建zabbix服务

    Zabbix 介绍 zabbix(音同 zæbix)是一个基于WEB界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案. zabbix能监视各种网络参数,保证服务器系统的安全运营:并提供灵 ...

  7. 【v2.x OGE教程 16】 Modifier使用相关

    OGE引擎的改动器功能非常强大,为我们提供了丰富的改动器的实现类,能够改动实体的属性.提供了移动.旋转.缩放.透明.自己定义序列.同步.循环等功能,并且改变的效果能够当场生效.也能够在某一个时间段内生 ...

  8. 利用栈Stack实现队列(Queue)

    实现说明: 入队时,将元素压入s1; 出队时,推断s2是否为空,如不为空,则直接弹出顶元素:如为空.则将s1的元素逐个"倒入"s2.把最后一个元素弹出并出队; 这个思路,避免了重复 ...

  9. 【Android】自己定义View

    翻译自:http://developer.android.com/training/custom-views/index.html 一)创建view类 一个设计良好的自己定义view与其它的类一样.它 ...

  10. 什么是sibling and tail recursive calls

    1 tail call 在函数f中调用函数b,如果这个调用是函数f中执行的最后一条指令,那么这个调用就称为tail call. 例子: int foo(float a, float b) { ... ...