【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/

转载于:https://www.cnblogs.com/mfoonirlee/p/7054939.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/541140.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mysql8安装目录linux7.5_Linux系统下 MySQL 5.7和8.0 版本安装指南

一. 准备工作1 删除本地CentOS7中的mariadb:查看系统中是否已安装 mariadb 服务:rpm -qa | grep mariadb或yum list installed | grep mariadb如果已安装则删除 mariadb及其依赖的包:yum -y remove mariadb-libs-5.5.44-2.el7.centos.x86_64关…

如何快速精确的和leader沟通

2019独角兽企业重金招聘Python工程师标准>>> 【缘起】 一个同学找我讨论个事情,沟通了一会还是不确定要表达什么,希望我配合什么。结合自己的经验,简单的聊聊“如何快速精准的和leader沟通一件事”。 【员工角度的潜在困惑&#x…

li怎么让文字在图片下面_div+css(ul li)实现图片上文字下列表布局

css样式表代码:html布局代码:效果图:html布局部分,可根据自己需要添加对应的div即可。1、CSS关键样式单词解释1)、ul.imglist{ margin:0 auto; width:536px; overflow:hidden}使用margin:0 auto,让ul结构布局居中&…

如何使用React Native样式表?

Without wasting much time, a style sheet as commonly known in a CSS is an object or block of code of many styling properties and values which is applied in a code when called. 在不浪费大量时间的情况下,CSS中通常已知的样式表是具有许多样式属性和值的…

【iCore1S 双核心板_ARM】例程三:EXTI中断输入实验——读取ARM按键状态

实验原理: 按键的一端与STM32的GPIO(PB9)相连,且PB9外接一个1k大小的限流上接电阻。 初始化时把PB9设置成输入模式,当按键弹起时,PB9由于上拉电阻的作用呈高电平(3.3V); 当按键按下时&#xff0…

MySQL小黑框怎么打开_打开你的小黑框命令行,来跟我一起嗨嗨嗨

文章更新于2020-03-16关于电脑位数:位数代表cpu可寻址的内存地址大小。32位的cpu最多可使用4GB内存,而64位cpu能处理的内存范围就高多了。操作系统也类似,只要看到操作系统里面能识别8GB内存就可以知道cpu和操作系统都是64位。一、常用的 cmd…

mysql重做日志与binlog日志区别_MySQL中的重做日志(redo log),回滚日志(undo log),以及二进制日志(binlog)的简单总结...

MySQL中有六种日志文件,分别是重做日志(redo log)回滚日志(undo log)二进制日志(binlog)错误日志(errorlog)慢查询日志(slow query log)一般查询日志(general log)中继日志(relay log)。其中重做日志和回滚日志与事务操作息息相关,二进制日志也与事务操作…

python 绘制三角函数_Python | 绘制三角函数

python 绘制三角函数Trigonometry is one of the most important parts in engineering and many times, therefore matplotlib.pyplot in combination with NumPy can help us to plot our desired trigonometric functions. In this article, we are going to introduce a fe…

《深入理解Elasticsearch(原书第2版)》一2.3.3 把查询模板保存到文件

本节书摘来华章计算机《深入理解Elasticsearch(原书第2版)》一书中的第2章 ,第2.3.3节,[美]拉斐尔酷奇(Rafal Ku) 马雷克罗戈任斯基(Marek Rogoziski)著 张世武 余洪淼 商旦 译 …

之江学院第0届 A qwb与支教 容斥与二分

题目链接: http://115.231.222.240:8081/JudgeOnline/problem.php?cid1005&pid0 题目描述: 给你三个数x, y, z 和 N 输出从1开始数第N个不是x, y, z 任意一个数的倍数的数字 解题思路: 一看到倍数我先想到素数唯一分解定理, …

mysql简单部署_安装部署Mysql实例(最简单快速噢)

题外话作为Mysql DBA,我们平时必须要熟练的一个最最基础的技能,即安装部署Mysql实例,所以本文分享一个快速安装部署Mysql实例的方法。一、环境介质准备Mysql安装包准备服务器准备我这里使用的是centos 7.x,此方法适用于任何其他li…

zabbix增加手机端4个url地址的返回值

由同事提供4个需要监控的url地址GET类型:http://10.15.24.61:809/UserCenterService.svc/getAccountInfo/563/9638POST类型:http://10.15.24.61:809/ProductService/userInvestVarietyYjsList/4/0/563/1/9638/1.0http://10.15.24.61:809/ProductService/…

微信红包促销系统开发

如今,互联网的普及,借助网络,营销更加方便。已经有商家开始与我们合作开发新推出的微信二维码红包促销活动了,不仅能达到活动气氛还能进行防伪,同时还可以给自己的公众号沉淀粉丝。微信红包促销系统开发—— 张小龙作为…

『科学计算』科学绘图库matplotlib练习

思想:万物皆对象 作业 第一题: import numpy as np import matplotlib.pyplot as pltx [1, 2, 3, 1] y [1, 3, 0, 1]def plot_picture(x, y):plt.plot(x, y, colorr, linewidth2, linestyle--, markerD, labelone)plt.xticks(list(range(-5,5,1)))plt.…

mysql下载64位 csdn_Linunx-CentOS7安装mysql-5.7.23-linux-glibc2.12-x86_64.tar

2. Linux下安装Mysql2.2 将下载好的mysql安装包通过xftp上传到虚拟机上。2.3 将安装包移动到/usr/local路径下#mv mysql-5.7.23-linux-glibc2.12-x86_64.tar.gz /usr/local2.4 解压安装包#tar -zxvf mysql-5.7.23-linux-glibc2.12-x86_64.tar.gz2.5 重命名解压文件或者是建立软…

rust拆掉墙_rust怎么拆自己的墙

rust游戏中可以建造自己的家,但是当建墙的时候总会觉得摆放不好,所以就需要拆除,但是要怎么拆自己的墙呢,下面小编就来为大家介绍一下吧!rust怎么拆自己的墙要先放上领地柜,然后在墙刚建好的几分钟内锤子右…

基于Kubernetes的分布式压力测试方案

压力测试是用来检测系统承载能力的有效手段。在系统规模较小的时候,在一台空闲的服务器上使用[ab],[wrk],[siege]等工具发起一定量的并发请求即可得到一个初步的测试结果。但在系统复杂度逐步提高,特别是引入了负载均衡&#xff0…

The output path is not specified for module XXX

新建项目启动时候:The output path is not specified for module XXX 没有为模块XXX指定输出路径。 解决方案 第一步 第二步 第三步

erwin模型导入mysql_使用erwin进行mysql建模

1,定义数据字典,把需要用到的数据类型创建好2,在物理模式下,设置数据字典,修改comment为%AttName这样会默认使用逻辑模式下,实体属性的名称作为字段的注释3,物理模式下选择database->pre & post scr…

Linux网络那点事

跨平台系列汇总:http://www.cnblogs.com/dunitian/p/4822808.html#linux 之前的之前说过网络自连接的配置(CentOS服务器网络配置:http://www.cnblogs.com/dunitian/p/4975830.html),这次和这个类似 这种方法适用于Cent…