【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的基类pluginstokenizer index.js //定义了 Token类 继承自上级目录parser的LocationParser 即 ../parser/location.jsutil
大概流程是这样的:
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/
转载于:https://www.cnblogs.com/mfoonirlee/p/7054939.html
【js】JavaScript parser实现浅析相关推荐
- javascript date php date,JavaScript Date 知识浅析
Date函数 new Date() Date 对象会自动把当前日期和时间保存为其初始值. date.getDate() 从 Date 对象返回一个月中的某一天 (1 ~ 31). date.getDa ...
- JS/JavaScript中解析JSON --- JSON.parse()、JSON.stringify()以及$.parseJSON()使用详解
JS/JavaScript中解析JSON --- JSON.parse().JSON.stringify()以及$.parseJSON()使用详解 现在JSON格式在web开发中非常重要,特别是在使用 ...
- 阿提拉公司 java_Atitit 文件上传 架构设计 实现机制 解决方案 实践java php c#.net js javascript c++ python...
Atitit 文件上传 架构设计 实现机制 解决方案 实践 java php c#.net js javascript c++ python 1 . 上传的几点要求 2 1 .1. 本地预览 2 1 ...
- atitit.js javascript 调用c# java php后台语言api html5交互的原理与总结p97
atitit.js javascript 调用c# java php后台语言api html5交互的原理与总结p97 1. 实现html5化界面的要解决的策略1 1.1. Js交互1 1.2. 动态参 ...
- java swing调用H5_atitit.js javascript 调用c# java php后台语言api html5交互的原理与总结p97...
atitit.js javascript 调用c# java php后台语言api html5交互的原理与总结p97 1.实现html5化界面的要解决的策略 1.1.Js交互 Firefox与Chro ...
- [js]JavaScript Number.toPrecision() 函数详解
[js]JavaScript Number.toPrecision() 函数详解 JavaScript: numberObject.toPrecision( [ precision ] ) 如果没有提 ...
- Atitit. 数据约束 校验 原理理论与 架构设计 理念模式java php c#.net js javascript mysql oracle...
Atitit. 数据约束 校验 原理理论与 架构设计 理念模式java php c#.net js javascript mysql oracle 1. 主键1 2. uniq index2 3. ...
- java 返回js,Javascript返回上一页
1. Javascript 返回上一页 history.go(-1), 返回两个页面: history.go(-2); 2. history.back(). 3. window.history.for ...
- Eclipse去除js(JavaScript)验证错误
这篇文章主要是对Eclipse去除js(JavaScript)验证错误进行了介绍.在Eclipse中,js文件常常会报错.可以通过如下几个步骤解决 第一步: 去除eclipse的JS验证: 将wind ...
最新文章
- java+js上传图片_java+ jsp+js 实现富文本编辑和上传图片功能
- CSS3模拟IOS滑动开关
- PHP PDO 预处理语句与存储过程
- .net中自定义过滤器对Response内容进行处理
- CRM的使用是讲究技巧的
- 热电偶校验仪_热电偶校验方法_热电偶的使用方法及维修经验
- VFP参考资料word版
- android广播第三方库,Android Support 库:LocalBroadcastManager
- OpenGL版本与OpenGL扩展机制
- nginx+lua+redis deny ip
- 牛客小白月赛9: D. 树上求和(dfs序+线段树)
- MyBatis中Mapper接口是怎么和XML文件关联起来的
- 计算机技术服务的增值税税率,咨询系统集成技术服务税率
- Jenkins项目配置-Nodejs项目(Vue框架)-全面
- sdcc和C51中断程序
- MATLAB的使用(二) help命令全解
- 定西稳定高速的服务器,中国移动宽带甘肃定西的dns服务器地址
- 半导体器件物理【4】晶胞晶格晶面晶向
- WEB请求处理二:Nginx请求反向代理
- 认识字符集、ASCII、GBK、Unicode、UTF-8
热门文章
- 安卓的java无法访问网络_Android网络访问的基本方法
- 软件测试行业用mac好还是win好,业余RMAA测试 同样的硬件下OS X果然比Windows声音好...
- java中asl_带你认识绕不开的ASLR
- 为什么苹果内购总是失败_IOS用户支付失败 购买无法完成解决教程
- ruby mysql 占位符_ruby操作常用数据库
- 云计算呼叫中心_干货|云呼叫中心系统和传统呼叫中心系统的区别在哪?
- facebook对话链接_如何应用防错原则,看看 Facebook 和 Gmail 是怎么做的
- hal 双串口同时接收丢失数据_【STM32Cube_06】使用USART发送和接收数据(查询模式)...
- linux send 失败_Epoll学习服务器的实现-Linux内核原始Epoll结构
- s2sh框架搭建(辅助工具:MyEclipse)及解决一些遇到的问题