关于ast:AST真香

豆皮粉儿们,又见面了,明天这一期,由字节跳动数据平台的太郎酱,带大家走进AST的世界。 作者:太郎酱什么是AST形象语法树(Abstract Syntax Tree, AST),是源代码的形象语法结构的树状示意,与之对应的是具体语法树;之所以是形象的,是因为形象语法树并不会示意出实在语法中呈现的每一个细节,而且是文法无关、不依赖于语言的细节;能够把AST设想成一套标准化的编程语言接口定义,只不过这一套标准,是针对编程语言自身的,小到变量申明,大到简单模块,都能够用这一套标准形容,有趣味的同学能够深刻理解AST的概念和原理,本文的重点聚焦在JavaScript AST的利用。 为什么要谈AST对于前端同学来说,日常开发中,和AST无关的场景无处不在;比方:webpack、babel、各种lint、prettier、codemod 等,都是基于AST解决的;把握了AST,相当于把握了控制代码的代码能力,能够帮忙咱们拓宽思路和视线,不论是写框架,还是写工具和逻辑,AST都会成为你的得力助手。 AST解析流程先举荐一个AST在线转换网站: astexplorer.net , 珍藏它,很重要;除了js,还有很多其余语言的AST库;不必做任何配置,就能够作为一个playground; 在解说case之前,先理解下解析流程,分为三步: source code --> ast (源代码解析为ast)traverse ast (遍历ast,拜访树中的各个节点,对节点做各种操作)ast --> code (把ast转换为源码,打完出工)源码解析成为AST的引擎有很多,转换进去的AST大同小异; Use Cases从一个变量申明说起,如下: const dpf = 'DouPiFan';把代码复制到astexplorer中,失去如下后果(后果已简化),这张图解释了从源码到AST的过程; 抉择不同的第三方库来生成AST,后果会有所差别,这里以babel/parse为例;前端同学对babel再相熟不过了,通过它的解决,能够在浏览器中反对ES2015+的代码,这仅仅是babel的其中一个利用场景,官网对本人的定位是:Babel is a javascript compiler。 回到 babel-parser,它应用 Babylon 作为解析引擎,它是AST 到 AST 的操作,babel在Babylon的根底上,封装了解析(babel-parser)和生成(babel-generator)这两步,因为每次操作都会做这两步;对于利用而言,操作的重点就是AST节点的遍历和更新了; 第一个babel插件咱们以一个最简略的babel插件为例,来理解它的处理过程; 当咱们开发babel-plugin的时候,咱们只须要在 visitor 中形容如何进行AST的转换即可。把它退出你的babel插件列表,就能够工作了,咱们的第一个babel插件开发实现; babel-plugin-import是如何实现的?应用过antd的同学,都晓得 babel-plugin-import插件,它是用来做antd组件的按需加载,配置之后的成果如下: import { Button } from 'antd' ↓ ↓ ↓ ↓ ↓ ↓import Button from 'antd/lib/button'本文旨在抛砖引玉,对于插件的实现细节以及各种边界条件,可参考插件源码;以AST的思维来思考,实现步骤如下: 查找代码中的 import 语句,且必须是 import { xxx } from 'antd'把步骤一找到的节点,转换为 import Button from 'antd/lib/button'实现步骤 ...

February 28, 2022 · 2 min · jiezi

关于ast:AST初探

前端开发中,应用了很多工具,譬如webpack、eslint来晋升研发效率,但咱们并不知道这些工具的实现原理。基于这些工具的外围都是形象语法树,那咱们就从形象语法树开始了解底层原理的新世界吧。 一、形象语法树是什么顾名思义,首先能够确定的是,这是一颗跟语法相干的树。 先上一盘硬菜,维基百科定义如下: In computer science, an abstract syntax tree (AST), or just syntax tree, is a tree representation of the abstract syntactic structure of source code written in a programming language. 也就是说,形象语法树,是通过编程语言编写的代码的形象语法结构。 用艰深的话讲,咱们所应用的编程语言是一门对人类敌对的语言,然而对于程序剖析来讲,并不敌对。因而,须要将编程语言转译成对程序剖析敌对的语言。 太干了,来点配菜。 咱们联合编译过程,来阐明形象语法树的作用。 如下图所示,个别的编译过程分为六个过程。 那么在语法分析阶段,就是要将词法分析阶段失去的分词后果整合成一棵语法树。简略举个例子: 代码: function foo(a) { let b = a + 3; return b;}将这个函数转成形象语法树,其外围局部如下图所示: blockstatement 块级作用域。 VariableDecalaration 变量申明 VariableDecalarator 变量申明器 Returnstatement 返回语句 在转成语法树之后,就能够对语句进行语法查看或进行批改了。 二、 语法树的利用后面提到,能够通过对语法树剖析来进行语法查看,有没有很相熟? 有没有想到咱们日常写代码过程中用到的eslint 插件、括号高亮插件等。 没有错,他们就是通过对ast 形象语法树进行剖析,来达成语法检查和高亮目标。 话不多说,看图。 ...

February 9, 2022 · 2 min · jiezi

关于ast:AST语法树增删改查

AST 是 Abstract Syntax Tree 的缩写,即 “形象语法树”.它是以树状的模式体现编程语言的语法结构. webpack 打包 JS 代码的时候,webpack 会在咱们的原有代码根底上新增一些代码, 例如咱们能够在打包JS 代码的时候将高级代码转为低级代码,就是通过 AST 语法树来实现的AST在线生成地址babel插件查看应用地址 AST生成过程由源码->词法剖析->语法分析->形象语法树例如let a = 1 + 2词法剖析 从左至右一个字符一个字符地读入源程序, 从中辨认出一个一个 “单词”“符号”等 读出来的就是一般的字符,没有任何编程语言的函数将剖析之后后果保留在一个词法单元数组中单词单词符号数字符号数字leta=1+2[ {"type": "word", value: "let"}, {"type": "word", value: "a"}, {"type": "Punctuator", value: "="}, {"type": "Numberic", value: "1"}, {"type": "Punctuator", value: "+"}, {"type": "Numberic", value: "2"},]之后进入词法剖析语法分析将单词序列组合成各类的语法短语 关键字标识符赋值运算符字面量二元运算符字面量leta=1+2[{ "type": "VariableDecLaration", "content": { {"type": "kind", "value": "let"}, // kind 示意是什么类型的申明 {"type": "Identifier", "value": "a"}, // Identifier 示意是标识符 {"type": "init", "value": "="}, // 示意初始值的表达式 {"type": "Literal", "value": "1"}, // Literal 示意是一个字面量 {"type": "operator", "value": "+"}, // operator 示意是一个二元运算符 {"type": "Literal", "value": "2"}, } }]形象语法树 ...

February 4, 2022 · 3 min · jiezi

关于ast:从AST原理到ESlint实践

AST(形象语法树)为什么要谈AST?如果你查看目前任何支流的我的项目中的devDependencies,会发现前些年的成千上万的插件诞生。咱们演绎一下有:ES6转译、代码压缩、css预处理器、eslint、prettier等。这些模块很多都不会用到生产环境,然而它们在开发环境中起到很重要的作用,这些工具的诞生都是建设在了AST这个伟人的肩膀上。 什么是AST?It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code.形象语法树(abstract syntax code,AST)是源代码的形象语法结构的树状示意,树上的每个节点都示意源代码中的一种构造,这所以说是形象的,是因为形象语法树并不会示意出实在语法呈现的每一个细节,比如说,嵌套括号被隐含在树的构造中,并没有以节点的模式出现。形象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采纳的上下文无文文法,因为在写文法时,常常会对文法进行等价的转换(打消左递归,回溯,二义性等),这样会给文法剖析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得凌乱。因些,很多编译器常常要独立地结构语法分析树,为前端,后端建设一个清晰的接口。 从纯文本转换成树形构造的数据,也就是AST,每个条目和树中的节点一一对应。 AST的流程此局部将让你理解到从源代码到词法剖析生成tokens再到语法分析生成AST的整个流程。 从源代码中怎么失去AST呢?当下的编译器帮着做了这件事,那编译器是怎么做的呢? 一款编译器的编译流程(将高级语言转译成二进制位)是很简单的,但咱们只须要关注词法剖析和语法分析,这两步是从代码生成AST的关键所在。 第一步,词法分析器,也称为扫描器,它会先对整个代码进行扫描,当它遇到空格、操作符或特殊符号时,它决定一个单词实现,将辨认出的一个个单词、操作符、符号等以对象的模式({type, value, range, loc })记录在tokens数组中,正文会另外寄存在一个comments数组中。 比方var a = 1;,@typescript-eslint/parser解析器生成的tokens如下: tokens: [ { "type": "Keyword", "value": "var", "range": [112, 115], "loc": { "start": { "line": 11, "column": 0 }, "end": { "line": 11, "column": 3 } } }, { "type": "Identifier", "value": "a", "range": [116, 117], "loc": { "start": { "line": 11, "column": 4 }, "end": { "line": 11, "column": 5 } } }, { "type": "Punctuator", "value": "=", "range": [118, 119], "loc": { "start": { "line": 11, "column": 6 }, "end": { "line": 11, "column": 7 } } }, { "type": "Numeric", "value": "1", "range": [120, 121], "loc": { "start": { "line": 11, "column": 8 }, "end": { "line": 11, "column": 9 } } }, { "type": "Punctuator", "value": ";", "range": [121, 122], "loc": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 10 } } }]第二步,语法分析器,也称为解析器,将词法剖析失去的tokens数组转换为树形构造示意,验证语言语法并抛出语法错误(如果产生这种状况) ...

August 11, 2021 · 10 min · jiezi

关于ast:手把手教你写一个-AST-抽象语法树

AST 解析器工作中常常用到,Vue.js 中的 VNode 就是如此! 其实如果有须要将 非结构化数据转 换成 结构化对象用 来剖析、解决、渲染的场景,咱们都能够用此思维做转换。 logo 如何解析成 AST ?咱们晓得 HTML 源码只是一个文本数据,只管它外面蕴含简单的含意和嵌套节点逻辑,然而对于浏览器,Babel 或者 Vue 来说,输出的就是一个长字符串,显然,纯正的一个字符串是示意不进去啥含意,那么就须要转换成结构化的数据,可能清晰的表白每一节点是干嘛的。字符串的解决,自然而然就是弱小的正则表达式了。 本文论述 AST 解析器的实现办法和次要细节,简略易懂~~~~,总共解析器代码不过百行! 指标本次指标,一步一步将如下 HTML 构造文档转换成 AST 形象语法树 <div class="classAttr" data-type="dataType" data-id="dataId" style="color:red">我是外层div <span>我是内层span</span></div>构造比较简单,外层一个 div,内层嵌套一个 span,外层有 class,data,stye 等属性。 麻雀虽小,五脏俱全,根本蕴含咱们常常用到的了。其中转换后的 AST 构造 有哪些属性,须要怎么的模式显示,都能够依据须要本人定义即可。 本次转换后的构造: { "node": "root", "child": [{ "node": "element", "tag": "div", "class": "classAttr", "dataset": { "type": "dataType", "id": "dataId" }, "attrs": [{ "name": "style", "value": "color:red" }], "child": [{ "node": "text", "text": "我是外层div" }, { "node": "element", "tag": "span", "dataset": {}, "attrs": [], "child": [{ "node": "text", "text": "我是内层span" }] }] }]}不难发现,外层是根节点,而后内层用 child 一层一层标记子节点,有 attr 标记节点的属性,classStr 来标记 class 属性,data 来标记 data- 属性,type 来标记节点类型,比方自定义的 data-type="title" 等。 ...

June 5, 2021 · 3 min · jiezi

关于ast:走进AST

前言:AST曾经深刻的存在咱们我的项目脚手架中,然而咱们缺不理解他,本文率领大家一起体验AST,感受一下解决问题另一种办法什么是AST在讲之前先简略介绍一下什么AST,形象语法树(Abstract Syntax Tree)简称 AST,是源代码的形象语法结构的树状表现形式。平时很多库都有他的影子:例如 babel, es-lint, node-sass, webpack 等等。 OK 让咱们看下代码转换成 AST 是什么样子。 const ast = 'tree'这是一行简略的申明代码,咱们看下他转换成AST的样子 咱们发现整个树的根节点是 Program,他有一个子节点 body,body 是一个数组,数组中还有一个子节点 VariableDeclaration,VariableDeclaration中示意const ast = 'tree'这行代码的申明,具体的解析如下: type: 形容语句的类型,此处是一个变量申明类型kind: 形容申明类型,相似的值有'var' 'let'declarations: 申明内容的数组,其中每一项都是一个对象------------type: 形容语句的类型,此处是一个变量申明类型------------id: 被申明字段的形容----------------type: 形容语句的类型,这里是一个标识符----------------name: 变量的名字------------init: 变量初始化值的形容----------------type: 形容语句的类型,这里是一个标识符----------------name: 变量的值大体上的构造是这样,body下的每个节点还有一些字段没有给大家阐明,例如:地位信息,以及一些没有值的key都做了暗藏,举荐大家能够去 asteplorer这个网站去试试看。 总结一下, AST就是把代码通过编译器变成树形的表达形式。 如何生成AST如何生成把纯文本的代码变成AST呢?编辑器生成语法树个别分为三个步骤 词法剖析语法分析生成语法树词法剖析:也叫做扫描。它读取咱们的代码,而后把它们依照预约的规定合并成一个个的标识tokens。同时,它会移除空白符,正文,等。最初,整个代码将被宰割进一个tokens列表(或者说一维数组)。比方说下面的例子 const ast = 'tree',会被剖析为const、ast、=、'tree' const ast = 'tree';[ { type: 'keyword', value: 'const' }, { type: 'identifier', value: 'a' }, { type: 'punctuator', value: '=' }, { type: 'numeric', value: '2' }, ]当词法剖析源代码的时候,它会一个一个字母地读取代码,所以很形象地称之为扫描-scans;当它遇到空格,操作符,或者特殊符号的时候,它会认为一个话曾经实现了。 ...

July 25, 2020 · 2 min · jiezi

关于ast:走进AST

前言:AST曾经深刻的存在咱们我的项目脚手架中,然而咱们缺不理解他,本文率领大家一起体验AST,感受一下解决问题另一种办法什么是AST在讲之前先简略介绍一下什么AST,形象语法树(Abstract Syntax Tree)简称 AST,是源代码的形象语法结构的树状表现形式。平时很多库都有他的影子:例如 babel, es-lint, node-sass, webpack 等等。 OK 让咱们看下代码转换成 AST 是什么样子。 const ast = 'tree'这是一行简略的申明代码,咱们看下他转换成AST的样子 咱们发现整个树的根节点是 Program,他有一个子节点 body,body 是一个数组,数组中还有一个子节点 VariableDeclaration,VariableDeclaration中示意const ast = 'tree'这行代码的申明,具体的解析如下: type: 形容语句的类型,此处是一个变量申明类型kind: 形容申明类型,相似的值有'var' 'let'declarations: 申明内容的数组,其中每一项都是一个对象------------type: 形容语句的类型,此处是一个变量申明类型------------id: 被申明字段的形容----------------type: 形容语句的类型,这里是一个标识符----------------name: 变量的名字------------init: 变量初始化值的形容----------------type: 形容语句的类型,这里是一个标识符----------------name: 变量的值大体上的构造是这样,body下的每个节点还有一些字段没有给大家阐明,例如:地位信息,以及一些没有值的key都做了暗藏,举荐大家能够去 asteplorer这个网站去试试看。 总结一下, AST就是把代码通过编译器变成树形的表达形式。 如何生成AST如何生成把纯文本的代码变成AST呢?编辑器生成语法树个别分为三个步骤 词法剖析语法分析生成语法树词法剖析:也叫做扫描。它读取咱们的代码,而后把它们依照预约的规定合并成一个个的标识tokens。同时,它会移除空白符,正文,等。最初,整个代码将被宰割进一个tokens列表(或者说一维数组)。比方说下面的例子 const ast = 'tree',会被剖析为const、ast、=、'tree' const ast = 'tree';[ { type: 'keyword', value: 'const' }, { type: 'identifier', value: 'a' }, { type: 'punctuator', value: '=' }, { type: 'numeric', value: '2' }, ]当词法剖析源代码的时候,它会一个一个字母地读取代码,所以很形象地称之为扫描-scans;当它遇到空格,操作符,或者特殊符号的时候,它会认为一个话曾经实现了。 ...

July 25, 2020 · 2 min · jiezi

前端面试每日-31-第148天

今天的知识点 (2019.09.11) —— 第148天[html] input的onblur和onchange事件区别是什么?[css] 什么是脱离文档流?有什么办法可以让元素脱离标准的文档流?[js] 请使用原生的js实现斐波那契数列[软技能] 你知道什么是AST吗?说说你对AST的理解,它的运用场景有哪些?《论语》,曾子曰:“吾日三省吾身”(我每天多次反省自己)。 前端面试每日3+1题,以面试题来驱动学习,每天进步一点! 让努力成为一种习惯,让奋斗成为一种享受!相信 坚持 的力量!!!欢迎在 Issues 和朋友们一同讨论学习! 项目地址:前端面试每日3+1 【推荐】欢迎跟 jsliang 一起折腾前端,系统整理前端知识,目前正在折腾 LeetCode,打算打通算法与数据结构的任督二脉。GitHub 地址 微信公众号欢迎大家前来讨论,如果觉得对你的学习有一定的帮助,欢迎点个Star, 同时欢迎微信扫码关注 前端剑解 公众号,并加入 “前端学习每日3+1” 微信群相互交流(点击公众号的菜单:进群交流)。 学习不打烊,充电加油只为遇到更好的自己,365天无节假日,每天早上5点纯手工发布面试题(死磕自己,愉悦大家)。希望大家在这浮夸的前端圈里,保持冷静,坚持每天花20分钟来学习与思考。在这千变万化,类库层出不穷的前端,建议大家不要等到找工作时,才狂刷题,提倡每日学习!(不忘初心,html、css、javascript才是基石!)欢迎大家到Issues交流,鼓励PR,感谢Star,大家有啥好的建议可以加我微信一起交流讨论!希望大家每日去学习与思考,这才达到来这里的目的!!!(不要为了谁而来,要为自己而来!)交流讨论欢迎大家前来讨论,如果觉得对你的学习有一定的帮助,欢迎点个[Star] https://github.com/haizlin/fe...

September 11, 2019 · 1 min · jiezi

PHP-实现字符串表达式计算

什么是字符串表达式?即,将我们常见的表达式文本写到了字符串中,如:"$age >= 20",$age 的值是动态的整型变量。什么是字符串表达式计算?即,我们需要一段程序来执行动态的表达式,如给定一个含表达式的字符串变量并计算其结果,而表达式字符串是动态的,比如为客户A执行的表达式是 $orderCount >= 10,而为客户B执行的表达式是 $orderTotal >= 1000。 场景在哪儿?同一份程序具有完全通用性,但差异就其中一个表达式而已,那么我们需要将其抽象出来,让表达式变成动态的、可配置的、或可生成的。 方案一:eval 函数eval 函数可能是我们第一个想到的方案,也是最简单直接的方案。我们来试验下: $a = 10;var_dump(eval('return $a > 5;'));// 输出:// bool(true)嗯~完全能满足我们的需求,因为 eval 函数执行的 PHP 表达式,只要字符串内表达式符合 PHP 语法就行。 但需注意的是,eval 函数可执行任意 PHP 代码,也就意味着权限大、风险高、不安全。如果你的字符串表达式来自于外部输入,那务必注意了请自行做好安全检查和过滤,并考虑风险。当然,执行的是外部输入表达式,非常不建议使用此函数。 方案二:include 临时文件如何实现?将字符串表达式写入一个临时文件,然后 include 这个临时文件,执行完成后再删除这个临时文件。 方案依然很简单。需要考虑的有: 临时文件会很多,一个请求就有很多个,文件的过期和删除务必考虑在内文件的读写,也就牵扯到了磁盘 IO,那性能必定受到严重影响那这个方案我们还采用吗? 方案三:assert 断言其实 assert 做不到字符串表达式的计算,但提出来也算个猜想,因为能实现 PHP 表达式是否合法的校验。 下例演示了如何验证某个字符串表达式是否为合法的 PHP 表达式: try { assert('a +== 1');} catch (Throwable $e) { echo $e->getMessage(), "\n";}运行结果: Failure evaluating code: a +== 1可依然面临一个问题,那就是安全性,因为与 eval 一样能执行任意代码。所以,从 PHP 7.2 开始就不可以再执行字符串类型的表达式了。关于 PHP assert 断言,可参考 你所不知的 PHP 断言(assert) ...

September 8, 2019 · 2 min · jiezi

简单玩一下ASTJavaScript

直奔主题对于js,AST能干什么? babel将es6转es5mpvue、taro等将js转为小程序定制插件删除注释、console等ps: 本文只探讨AST的概念以及使用,编译原理的其他知识不做太多描述 工具库@babel/core 用来解析AST以及将AST生成代码@babel/types 构建新的AST节点前置知识 - 编译原理概述毫无疑问js是一个解释型语言,有疑问可以参考这篇文章所以这里只简单描述一下js的编译过程(大雾),有兴趣了解编译型语言详细编译过程的可以看这本 《编译原理》 词法分析:实际就是从左到右一个字一个字地扫描分解,识别出单词、符号等。例如:var num = 1会被分解成var,num,=,1语法分析将词法分析的结果,根据语法规则转化为一个嵌套的对象,也就是AST(Abstract Syntax Tree 即 抽象语法树)目标代码产生将AST转换为可执行代码生成ASTdemo.js是我随便copy来的一段代码 isLeapYear()function isLeapYear(year) { const cond1 = year % 4 == 0; //条件1:年份必须要能被4整除 const cond2 = year % 100 != 0; //条件2:年份不能是整百数 const cond3 = year % 400 ==0; //条件3:年份是400的倍数 const cond = cond1 && cond2 || cond3; console.log(cond) if(cond) { alert(year + "是闰年"); return true; } else { alert(year + "不是闰年"); return false; }}现在我要把它转成AST,这里使用@babel/core来解析,它提供了一个parse方法来将代码转化为AST。 ...

July 9, 2019 · 2 min · jiezi

初学-Babel-工作原理

原文链接:初学 Babel 工作原理 前言 Babel 对于前端开发者来说应该是很熟悉了,日常开发中基本上是离不开它的。 已经9102了,我们已经能够熟练地使用 es2015+ 的语法。但是对于浏览器来说,可能和它们还不够熟悉,我们得让浏览器理解它们,这就需要 Babel。 当然,仅仅是 Babel 是不够的,还需要 polyfill 等等等等,这里就先不说了。 What:什么是 BabelBabel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments.简单地说,Babel 能够转译 ECMAScript 2015+ 的代码,使它在旧的浏览器或者环境中也能够运行。 我们可以在 https://babel.docschina.org/repl 尝试一下。 一个小????: // es2015 的 const 和 arrow functionconst add = (a, b) => a + b;// Babel 转译后var add = function add(a, b) { return a + b;}; ...

June 25, 2019 · 5 min · jiezi

译什么是抽象语法树

原文地址:What is an Abstract Syntax Tree原文作者:Chidume Nnamdi AST 是抽象语法树的缩写词,表示编程语言的语句和表达式中生成的 token。有了 AST,解释器或编译器就可以生成机器码或者对一条指令求值。 小贴士: 通过使用 Bit,你可以将任意的 JS 代码转换为一个可在项目和应用中共享、使用和同步的 API,从而更快地构建并重用更多代码。试一下吧。 假设我们有下面这条简单的表达式: 1 + 2用 AST 来表示的话,它是这样的: + BinaryExpression - type: + - left_value: LiteralExpr: value: 1 - right_vaue: LiteralExpr: value: 2诸如 if 的语句则可以像下面这样表示: if(2 > 6) { var d = 90 console.log(d)}IfStatement - condition + BinaryExpression - type: > - left_value: 2 - right_value: 6 - body [ - Assign - left: 'd'; - right: LiteralExpr: - value: 90 - MethodCall: - instanceName: console - methodName: log - args: [ ] ]这告诉解释器如何解释语句,同时告诉编译器如何生成语句对应的代码。 ...

June 16, 2019 · 5 min · jiezi

高级前端基础-JavaScript抽象语法树AST

前言Babel为当前最流行的代码JavaScript编译器了,其使用的JavaScript解析器为babel-parser,最初是从Acorn 项目fork出来的。Acorn 非常快,易于使用,并且针对非标准特性(以及那些未来的标准特性) 设计了一个基于插件的架构。本文主要介绍esprima解析生成的抽象语法树节点,esprima的实现也是基于Acorn的。原文地址解析器 ParserJavaScript Parser 是把js源码转化为抽象语法树(AST)的解析器。这个步骤分为两个阶段:词法分析(Lexical Analysis) 和 语法分析(Syntactic Analysis)。常用的JavaScript Parser:esprimauglifyJS2traceuracornespree@babel/parser词法分析词法分析阶段把字符串形式的代码转换为 令牌(tokens)流。你可以把令牌看作是一个扁平的语法片段数组。n * n;例如上面nn的词法分析得到结果如下:[ { type: { … }, value: “n”, start: 0, end: 1, loc: { … } }, { type: { … }, value: “”, start: 2, end: 3, loc: { … } }, { type: { … }, value: “n”, start: 4, end: 5, loc: { … } },]每一个 type 有一组属性来描述该令牌:{ type: { label: ’name’, keyword: undefined, beforeExpr: false, startsExpr: true, rightAssociative: false, isLoop: false, isAssign: false, prefix: false, postfix: false, binop: null, updateContext: null }, …}和 AST 节点一样它们也有 start,end,loc 属性。语法分析语法分析就是根据词法分析的结果,也就是令牌tokens,将其转换成AST。function square(n) { return n * n;}如上面代码,生成的AST结构如下:{ type: “FunctionDeclaration”, id: { type: “Identifier”, name: “square” }, params: [{ type: “Identifier”, name: “n” }], body: { type: “BlockStatement”, body: [{ type: “ReturnStatement”, argument: { type: “BinaryExpression”, operator: “”, left: { type: “Identifier”, name: “n” }, right: { type: “Identifier”, name: “n” } } }] }}下文将对AST各个类型节点做解释。更多AST生成,入口如下:eslintAST Exploreresprima结合可视化工具,举个例子如下代码:var a = 42;var b = 5;function addA(d) { return a + d;}var c = addA(2) + b;第一步词法分析之后长成如下图所示:语法分析,生产抽象语法树,生成的抽象语法树如下图所示BaseNode所有节点类型都实现以下接口:interface Node { type: string; range?: [number, number]; loc?: SourceLocation;}该type字段是表示AST变体类型的字符串。该loc字段表示节点的源位置信息。如果解析器没有生成有关节点源位置的信息,则该字段为null;否则它是一个对象,包括一个起始位置(被解析的源区域的第一个字符的位置)和一个结束位置.interface SourceLocation { start: Position; end: Position; source?: string | null;}每个Position对象由一个line数字(1索引)和一个column数字(0索引)组成:interface Position { line: uint32 >= 1; column: uint32 >= 0;}Programsinterface Program <: Node { type: “Program”; sourceType: ‘script’ | ‘module’; body: StatementListItem[] | ModuleItem[];}表示一个完整的源代码树。Scripts and Modules源代码数的来源包括两种,一种是script脚本,一种是modules模块当为script时,body为StatementListItem。当为modules时,body为ModuleItem。类型StatementListItem和ModuleItem类型如下。type StatementListItem = Declaration | Statement;type ModuleItem = ImportDeclaration | ExportDeclaration | StatementListItem;ImportDeclarationimport语法,导入模块type ImportDeclaration { type: ‘ImportDeclaration’; specifiers: ImportSpecifier[]; source: Literal;}ImportSpecifier类型如下:interface ImportSpecifier { type: ‘ImportSpecifier’ | ‘ImportDefaultSpecifier’ | ‘ImportNamespaceSpecifier’; local: Identifier; imported?: Identifier;}ImportSpecifier语法如下:import { foo } from ‘./foo’;ImportDefaultSpecifier语法如下:import foo from ‘./foo’;ImportNamespaceSpecifier语法如下import * as foo from ‘./foo’;ExportDeclarationexport类型如下type ExportDeclaration = ExportAllDeclaration | ExportDefaultDeclaration | ExportNamedDeclaration;ExportAllDeclaration从指定模块中导出interface ExportAllDeclaration { type: ‘ExportAllDeclaration’; source: Literal;}语法如下:export * from ‘./foo’;ExportDefaultDeclaration导出默认模块interface ExportDefaultDeclaration { type: ‘ExportDefaultDeclaration’; declaration: Identifier | BindingPattern | ClassDeclaration | Expression | FunctionDeclaration;}语法如下:export default ‘foo’;ExportNamedDeclaration导出部分模块interface ExportNamedDeclaration { type: ‘ExportNamedDeclaration’; declaration: ClassDeclaration | FunctionDeclaration | VariableDeclaration; specifiers: ExportSpecifier[]; source: Literal;}语法如下:export const foo = ‘foo’;Declarations and Statementsdeclaration,即声明,类型如下:type Declaration = VariableDeclaration | FunctionDeclaration | ClassDeclaration;statements,即语句,类型如下:type Statement = BlockStatement | BreakStatement | ContinueStatement | DebuggerStatement | DoWhileStatement | EmptyStatement | ExpressionStatement | ForStatement | ForInStatement | ForOfStatement | FunctionDeclaration | IfStatement | LabeledStatement | ReturnStatement | SwitchStatement | ThrowStatement | TryStatement | VariableDeclaration | WhileStatement | WithStatement;VariableDeclarator变量声明,kind 属性表示是什么类型的声明,因为 ES6 引入了 const/let。interface VariableDeclaration <: Declaration { type: “VariableDeclaration”; declarations: [ VariableDeclarator ]; kind: “var” | “let” | “const”;}FunctionDeclaration函数声明(非函数表达式)interface FunctionDeclaration { type: ‘FunctionDeclaration’; id: Identifier | null; params: FunctionParameter[]; body: BlockStatement; generator: boolean; async: boolean; expression: false;}例如:function foo() {}ClassDeclaration类声明(非类表达式)interface ClassDeclaration { type: ‘ClassDeclaration’; id: Identifier | null; superClass: Identifier | null; body: ClassBody;}ClassBody声明如下:interface ClassBody { type: ‘ClassBody’; body: MethodDefinition[];}MethodDefinition表示方法声明;interface MethodDefinition { type: ‘MethodDefinition’; key: Expression | null; computed: boolean; value: FunctionExpression | null; kind: ‘method’ | ‘constructor’; static: boolean;}class foo { constructor() {} method() {}};ContinueStatementcontinue语句interface ContinueStatement { type: ‘ContinueStatement’; label: Identifier | null;}例如:for (var i = 0; i < 10; i++) { if (i === 0) { continue; }}DebuggerStatementdebugger语句interface DebuggerStatement { type: ‘DebuggerStatement’;}例如while(true) { debugger;}DoWhileStatementdo-while语句interface DoWhileStatement { type: ‘DoWhileStatement’; body: Statement; test: Expression;}test表示while条件例如:var i = 0;do { i++;} while(i = 2)EmptyStatement空语句interface EmptyStatement { type: ‘EmptyStatement’;}例如:if(true);var a = [];for(i = 0; i < a.length; a[i++] = 0);ExpressionStatement表达式语句,即,由单个表达式组成的语句。interface ExpressionStatement { type: ‘ExpressionStatement’; expression: Expression; directive?: string;}当表达式语句表示一个指令(例如“use strict”)时,directive属性将包含该指令字符串。例如:(function(){});ForStatementfor语句interface ForStatement { type: ‘ForStatement’; init: Expression | VariableDeclaration | null; test: Expression | null; update: Expression | null; body: Statement;}ForInStatementfor…in语句interface ForInStatement { type: ‘ForInStatement’; left: Expression; right: Expression; body: Statement; each: false;}ForOfStatementfor…of语句interface ForOfStatement { type: ‘ForOfStatement’; left: Expression; right: Expression; body: Statement;}IfStatementif 语句interface IfStatement { type: ‘IfStatement’; test: Expression; consequent: Statement; alternate?: Statement;}consequent表示if命中后内容,alternate表示else或者else if的内容。LabeledStatementlabel语句,多用于精确的使用嵌套循环中的continue和break。interface LabeledStatement { type: ‘LabeledStatement’; label: Identifier; body: Statement;}如:var num = 0;outPoint:for (var i = 0 ; i < 10 ; i++){ for (var j = 0 ; j < 10 ; j++){ if( i == 5 && j == 5 ){ break outPoint; } num++; }}ReturnStatementreturn 语句interface ReturnStatement { type: ‘ReturnStatement’; argument: Expression | null;}SwitchStatementSwitch语句interface SwitchStatement { type: ‘SwitchStatement’; discriminant: Expression; cases: SwitchCase[];}discriminant表示switch的变量。SwitchCase类型如下interface SwitchCase { type: ‘SwitchCase’; test: Expression | null; consequent: Statement[];}ThrowStatementthrow语句interface ThrowStatement { type: ‘ThrowStatement’; argument: Expression;}TryStatementtry…catch语句interface TryStatement { type: ‘TryStatement’; block: BlockStatement; handler: CatchClause | null; finalizer: BlockStatement | null;}handler为catch处理声明内容,finalizer为finally内容。CatchClaus 类型如下interface CatchClause { type: ‘CatchClause’; param: Identifier | BindingPattern; body: BlockStatement;}例如:try { foo();} catch (e) { console.erroe(e);} finally { bar();}WhileStatementwhile语句interface WhileStatement { type: ‘WhileStatement’; test: Expression; body: Statement;}test为判定表达式WithStatementwith语句(指定块语句的作用域的作用域)interface WithStatement { type: ‘WithStatement’; object: Expression; body: Statement;}如:var a = {};with(a) { name = ‘xiao.ming’;}console.log(a); // {name: ‘xiao.ming’}Expressions and PatternsExpressions可用类型如下:type Expression = ThisExpression | Identifier | Literal | ArrayExpression | ObjectExpression | FunctionExpression | ArrowFunctionExpression | ClassExpression | TaggedTemplateExpression | MemberExpression | Super | MetaProperty | NewExpression | CallExpression | UpdateExpression | AwaitExpression | UnaryExpression | BinaryExpression | LogicalExpression | ConditionalExpression | YieldExpression | AssignmentExpression | SequenceExpression;Patterns可用有两种类型,函数模式和对象模式如下:type BindingPattern = ArrayPattern | ObjectPattern;ThisExpressionthis 表达式interface ThisExpression { type: ‘ThisExpression’;}Identifier标识符,就是我们写 JS 时自定义的名称,如变量名,函数名,属性名,都归为标识符。相应的接口是这样的:interface Identifier { type: ‘Identifier’; name: string;}Literal字面量,这里不是指 [] 或者 {} 这些,而是本身语义就代表了一个值的字面量,如 1,“hello”, true 这些,还有正则表达式(有一个扩展的 Node 来表示正则表达式),如 /d?/。interface Literal { type: ‘Literal’; value: boolean | number | string | RegExp | null; raw: string; regex?: { pattern: string, flags: string };}例如:var a = 1;var b = ‘b’;var c = false;var d = /\d/;ArrayExpression数组表达式interface ArrayExpression { type: ‘ArrayExpression’; elements: ArrayExpressionElement[];}例:[1, 2, 3, 4];ArrayExpressionElement数组表达式的节点,类型如下type ArrayExpressionElement = Expression | SpreadElement;Expression包含所有表达式,SpreadElement为扩展运算符语法。SpreadElement扩展运算符interface SpreadElement { type: ‘SpreadElement’; argument: Expression;}如:var a = [3, 4];var b = [1, 2, …a];var c = {foo: 1};var b = {bar: 2, …c};ObjectExpression对象表达式interface ObjectExpression { type: ‘ObjectExpression’; properties: Property[];}Property代表为对象的属性描述类型如下interface Property { type: ‘Property’; key: Expression; computed: boolean; value: Expression | null; kind: ‘get’ | ‘set’ | ‘init’; method: false; shorthand: boolean;}kind用来表示是普通的初始化,或者是 get/set。例如:var obj = { foo: ‘foo’, bar: function() {}, noop() {}, // method 为 true [‘computed’]: ‘computed’ // computed 为 true}FunctionExpression函数表达式interface FunctionExpression { type: ‘FunctionExpression’; id: Identifier | null; params: FunctionParameter[]; body: BlockStatement; generator: boolean; async: boolean; expression: boolean;}例如:function foo() {}function bar() { yield “44”; }async function noop() { await new Promise(function(resolve, reject) { resolve(‘55’); }) }ArrowFunctionExpression箭头函数表达式interface ArrowFunctionExpression { type: ‘ArrowFunctionExpression’; id: Identifier | null; params: FunctionParameter[]; body: BlockStatement | Expression; generator: boolean; async: boolean; expression: false;}generator表示是否为generator函数,async表示是否为async/await函数,params为参数定义。FunctionParameter类型如下type FunctionParameter = AssignmentPattern | Identifier | BindingPattern;例:var foo = () => {};ClassExpression类表达式interface ClassExpression { type: ‘ClassExpression’; id: Identifier | null; superClass: Identifier | null; body: ClassBody;}例如:var foo = class { constructor() {} method() {}};TaggedTemplateExpression标记模板文字函数interface TaggedTemplateExpression { type: ‘TaggedTemplateExpression’; readonly tag: Expression; readonly quasi: TemplateLiteral;}TemplateLiteral类型如下interface TemplateLiteral { type: ‘TemplateLiteral’; quasis: TemplateElement[]; expressions: Expression[];}TemplateElement类型如下interface TemplateElement { type: ‘TemplateElement’; value: { cooked: string; raw: string }; tail: boolean;}例如var foo = function(a){ console.log(a); }footest;MemberExpression属性成员表达式interface MemberExpression { type: ‘MemberExpression’; computed: boolean; object: Expression; property: Expression;}例如:const foo = {bar: ‘bar’};foo.bar;foo[‘bar’]; // computed 为 trueSuper父类关键字interface Super { type: ‘Super’;}例如:class foo {};class bar extends foo { constructor() { super(); }}MetaProperty(这个不知道干嘛用的)interface MetaProperty { type: ‘MetaProperty’; meta: Identifier; property: Identifier;}例如:new.target // 通过new 声明的对象,new.target会存在import.metaCallExpression函数执行表达式interface CallExpression { type: ‘CallExpression’; callee: Expression | Import; arguments: ArgumentListElement[];}Import类型,没搞懂。interface Import { type: ‘Import’}ArgumentListElement类型type ArgumentListElement = Expression | SpreadElement;如:var foo = function (){};foo();NewExpressionnew 表达式interface NewExpression { type: ‘NewExpression’; callee: Expression; arguments: ArgumentListElement[];}UpdateExpression更新操作符表达式,如++、–;interface UpdateExpression { type: “UpdateExpression”; operator: ‘++’ | ‘–’; argument: Expression; prefix: boolean;}如:var i = 0;i++;++i; // prefix为trueAwaitExpressionawait表达式,会与async连用。interface AwaitExpression { type: ‘AwaitExpression’; argument: Expression;}如async function foo() { var bar = function() { new Primise(function(resolve, reject) { setTimeout(function() { resove(‘foo’) }, 1000); }); } return await bar();}foo() // fooUnaryExpression一元操作符表达式interface UnaryExpression { type: “UnaryExpression”; operator: UnaryOperator; prefix: boolean; argument: Expression;}枚举UnaryOperatorenum UnaryOperator { “-” | “+” | “!” | “~” | “typeof” | “void” | “delete” | “throw”}BinaryExpression二元操作符表达式interface BinaryExpression { type: ‘BinaryExpression’; operator: BinaryOperator; left: Expression; right: Expression;}枚举BinaryOperatorenum BinaryOperator { “==” | “!=” | “===” | “!==” | “<” | “<=” | “>” | “>=” | “<<” | “>>” | “>>>” | “+” | “-” | “” | “/” | “%” | “**” | “|” | “^” | “&” | “in” | “instanceof” | “|>"}LogicalExpression逻辑运算符表达式interface LogicalExpression { type: ‘LogicalExpression’; operator: ‘||’ | ‘&&’; left: Expression; right: Expression;}如:var a = ‘-’;var b = a || ‘-’;if (a && b) {}ConditionalExpression条件运算符interface ConditionalExpression { type: ‘ConditionalExpression’; test: Expression; consequent: Expression; alternate: Expression;}例如:var a = true;var b = a ? ‘consequent’ : ‘alternate’;YieldExpressionyield表达式interface YieldExpression { type: ‘YieldExpression’; argument: Expression | null; delegate: boolean;}例如:function gen(x) { var y = yield x + 2; return y;}AssignmentExpression赋值表达式。interface AssignmentExpression { type: ‘AssignmentExpression’; operator: ‘=’ | ‘*=’ | ‘**=’ | ‘/=’ | ‘%=’ | ‘+=’ | ‘-=’ | ‘<<=’ | ‘>>=’ | ‘>>>=’ | ‘&=’ | ‘^=’ | ‘|=’; left: Expression; right: Expression;}operator属性表示一个赋值运算符,left和right是赋值运算符左右的表达式。SequenceExpression序列表达式(使用逗号)。interface SequenceExpression { type: ‘SequenceExpression’; expressions: Expression[];}var a, b;a = 1, b = 2ArrayPattern数组解析模式interface ArrayPattern { type: ‘ArrayPattern’; elements: ArrayPatternElement[];}例:const [a, b] = [1,3];elements代表数组节点ArrayPatternElement如下type ArrayPatternElement = AssignmentPattern | Identifier | BindingPattern | RestElement | null;AssignmentPattern默认赋值模式,数组解析、对象解析、函数参数默认值使用。interface AssignmentPattern { type: ‘AssignmentPattern’; left: Identifier | BindingPattern; right: Expression;}例:const [a, b = 4] = [1,3];RestElement剩余参数模式,语法与扩展运算符相近。interface RestElement { type: ‘RestElement’; argument: Identifier | BindingPattern;}例:const [a, b, …c] = [1, 2, 3, 4];ObjectPatterns对象解析模式interface ObjectPattern { type: ‘ObjectPattern’; properties: Property[];}例:const object = {a: 1, b: 2};const { a, b } = object;结束AST的作用大致分为几类IDE使用,如代码风格检测(eslint等)、代码的格式化,代码高亮,代码错误等等代码的混淆压缩转换代码的工具。如webpack,rollup,各种代码规范之间的转换,ts,jsx等转换为原生js了解AST,最终还是为了让我们了解我们使用的工具,当然也让我们更了解JavaScript,更靠近JavaScript。参考文献前端进阶之 Javascript 抽象语法树抽象语法树(Abstract Syntax Tree) ...

March 17, 2019 · 7 min · jiezi

规则引擎RulerZ用法及实现原理解读

规则引擎RulerZ用法及实现原理解读废话不多说,rulerz的官方地址是:https://github.com/K-Phoen/ru…注意,本例中只拿普通数组做例子进行分析1. 简介RulerZ是一个用php实现的composer依赖包,目的是实现一个数据过滤规则引擎。RulerZ不仅支持数组过滤,也支持一些市面上常见的ORM,如Eloquent、Doctrine等,也支持Solr搜索引擎。这是一个缺少中文官方文档的开源包,当然由于star数比较少,可能作者也觉得没必要。2.安装在你的项目composer.json所在目录下运行:composer require ‘kphoen/rulerz'3.使用 - 过滤现有数组如下:$players = [ [‘pseudo’ => ‘Joe’, ‘fullname’ => ‘Joe la frite’, ‘gender’ => ‘M’, ‘points’ => 2500], [‘pseudo’ => ‘Moe’, ‘fullname’ => ‘Moe, from the bar!’, ‘gender’ => ‘M’, ‘points’ => 1230], [‘pseudo’ => ‘Alice’, ‘fullname’ => ‘Alice, from… you know.’, ‘gender’ => ‘F’, ‘points’ => 9001],];初始化引擎:use RulerZ\Compiler\Compiler;use RulerZ\Target;use RulerZ\RulerZ;// compiler$compiler = Compiler::create();// RulerZ engine$rulerz = new RulerZ( $compiler, [ new Target\Native\Native([ // 请注意,这里是添加目标编译器,处理数组类型的数据源时对应的是Native ’length’ => ‘strlen’ ]), ]);创建一条规则:$rule = “gender = :gender and points > :min_points’将参数和规则交给引擎分析。$parameters = [ ‘min_points’ => 30, ‘gender’ => ‘F’,];$result = iterator_to_array( $rulerz->filter($players, $rule, $parameters) // the parameters can be omitted if empty );// result 是一个过滤后的数组array:1 [▼ 0 => array:4 [▼ “pseudo” => “Alice” “fullname” => “Alice, from… you know.” “gender” => “F” “points” => 9001 ]]4.使用 - 判断是否满足规则$rulerz->satisfies($player, $rule, $parameters);// 返回布尔值,true表示满足5.底层代码解读下面,让我们看看从创建编译器开始,到最后出结果的过程中发生了什么。1.Compiler::create();这一步是实例化一个FileEvaluator类,这个类默认会将本地的系统临时目录当做下一步临时类文件读写所在目录,文件类里包含一个has()方法和一个write()方法。文件类如下:<?phpdeclare(strict_types=1);namespace RulerZ\Compiler;class NativeFilesystem implements Filesystem{ public function has(string $filePath): bool { return file_exists($filePath); } public function write(string $filePath, string $content): void { file_put_contents($filePath, $content, LOCK_EX); }}2.初始化RulerZ引擎,new RulerZ()先看一下RulerZ的构建方法: public function construct(Compiler $compiler, array $compilationTargets = []) { $this->compiler = $compiler; foreach ($compilationTargets as $targetCompiler) { $this->registerCompilationTarget($targetCompiler); } }这里的第一个参数,就是刚刚的编译器类,第二个是目标编译器类(实际处理数据源的),因为我们选择的是数组,所以这里的目标编译器是Native,引擎会将这个目标编译类放到自己的属性$compilationTargets。 public function registerCompilationTarget(CompilationTarget $compilationTarget): void { $this->compilationTargets[] = $compilationTarget; }3.运用filter或satisfies方法这一点便是核心了。以filter为例: public function filter($target, string $rule, array $parameters = [], array $executionContext = []) { $targetCompiler = $this->findTargetCompiler($target, CompilationTarget::MODE_FILTER); $compilationContext = $targetCompiler->createCompilationContext($target); $executor = $this->compiler->compile($rule, $targetCompiler, $compilationContext); return $executor->filter($target, $parameters, $targetCompiler->getOperators()->getOperators(), new ExecutionContext($executionContext)); }第一步会检查目标编译器是否支持筛选模式。第二步创建编译上下文,这个一般统一是Context类实例 public function createCompilationContext($target): Context { return new Context(); }第三步,执行compiler的compile()方法 public function compile(string $rule, CompilationTarget $target, Context $context): Executor { $context[‘rule_identifier’] = $this->getRuleIdentifier($target, $context, $rule); $context[’executor_classname’] = ‘Executor’.$context[‘rule_identifier’]; $context[’executor_fqcn’] = ‘\RulerZ\Compiled\Executor\Executor’.$context[‘rule_identifier’]; if (!class_exists($context[’executor_fqcn’], false)) { $compiler = function () use ($rule, $target, $context) { return $this->compileToSource($rule, $target, $context); }; $this->evaluator->evaluate($context[‘rule_identifier’], $compiler); } return new $context’executor_fqcn’; } protected function getRuleIdentifier(CompilationTarget $compilationTarget, Context $context, string $rule): string { return hash(‘crc32b’, get_class($compilationTarget).$rule.$compilationTarget->getRuleIdentifierHint($rule, $context)); } protected function compileToSource(string $rule, CompilationTarget $compilationTarget, Context $context): string { $ast = $this->parser->parse($rule); $executorModel = $compilationTarget->compile($ast, $context); $flattenedTraits = implode(PHP_EOL, array_map(function ($trait) { return “\t”.‘use \’.ltrim($trait, ‘\’).’;’; }, $executorModel->getTraits())); $extraCode = ‘’; foreach ($executorModel->getCompiledData() as $key => $value) { $extraCode .= sprintf(‘private $%s = %s;’.PHP_EOL, $key, var_export($value, true)); } $commentedRule = str_replace(PHP_EOL, PHP_EOL.’ // ‘, $rule); return <<<EXECUTORnamespace RulerZ\Compiled\Executor;use RulerZ\Executor\Executor;class {$context[’executor_classname’]} implements Executor{ $flattenedTraits $extraCode // $commentedRule protected function execute($target, array $operators, array $parameters) { return {$executorModel->getCompiledRule()}; }}EXECUTOR; }这段代码会依照crc13算法生成一个哈希串和Executor拼接作为执行器临时类的名称,并将执行器相关代码写进上文提到的临时目录中去。生成的代码如下:// /private/var/folders/w_/sh4r42wn4_b650l3pc__fh7h0000gp/T/rulerz_executor_ff2800e8<?phpnamespace RulerZ\Compiled\Executor;use RulerZ\Executor\Executor;class Executor_ff2800e8 implements Executor{ use \RulerZ\Executor\ArrayTarget\FilterTrait; use \RulerZ\Executor\ArrayTarget\SatisfiesTrait; use \RulerZ\Executor\ArrayTarget\ArgumentUnwrappingTrait; // gender = :gender and points > :min_points and points > :min_points protected function execute($target, array $operators, array $parameters) { return ($this->unwrapArgument($target[“gender”]) == $parameters[“gender”] && ($this->unwrapArgument($target[“points”]) > $parameters[“min_points”] && $this->unwrapArgument($target[“points”]) > $parameters[“min_points”])); }}这个临时类文件就是最后要执行过滤动作的类。FilterTrait中的filter方法是首先被执行的,里面会根据execute返回的布尔值来判断,是否通过迭代器返回符合条件的行。execute方法就是根据具体的参数和操作符挨个判断每行中对应的cell是否符合判断来返回true/false。 public function filter($target, array $parameters, array $operators, ExecutionContext $context) { return IteratorTools::fromGenerator(function () use ($target, $parameters, $operators) { foreach ($target as $row) { $targetRow = is_array($row) ? $row : new ObjectContext($row); if ($this->execute($targetRow, $operators, $parameters)) { yield $row; } } }); }satisfies和filter基本逻辑类似,只是最后satisfies是执行单条判断。有一个问题,我们的编译器是如何知道我们设立的操作规则$rule的具体含义的,如何parse的?这就涉及另一个问题了,抽象语法树(AST)。Go further - 抽象语法树我们都知道php zend引擎在解读代码的过程中有一个过程是语法和词法分析,这个过程叫做parser,中间会将代码转化为抽象语法树,这是引擎能够读懂代码的关键步骤。同样,我们在写一条规则字符串的时候,代码如何能够明白我们写的是什么呢?那就是抽象语法树。以上面的规则为例:gender = :gender and points > :min_points这里, =、and、>都是操作符,但是机器并不知道他们是操作符,也不知道其他字段是什么含义。于是rulerz使用自己的语法模板。首先是默认定义了几个操作符。<?phpdeclare(strict_types=1);namespace RulerZ\Target\Native;use RulerZ\Target\Operators\Definitions;class NativeOperators{ public static function create(Definitions $customOperators): Definitions { $defaultInlineOperators = [ ‘and’ => function ($a, $b) { return sprintf(’(%s && %s)’, $a, $b); }, ‘or’ => function ($a, $b) { return sprintf(’(%s || %s)’, $a, $b); }, ’not’ => function ($a) { return sprintf(’!(%s)’, $a); }, ‘=’ => function ($a, $b) { return sprintf(’%s == %s’, $a, $b); }, ‘is’ => function ($a, $b) { return sprintf(’%s === %s’, $a, $b); }, ‘!=’ => function ($a, $b) { return sprintf(’%s != %s’, $a, $b); }, ‘>’ => function ($a, $b) { return sprintf(’%s > %s’, $a, $b); }, ‘>=’ => function ($a, $b) { return sprintf(’%s >= %s’, $a, $b); }, ‘<’ => function ($a, $b) { return sprintf(’%s < %s’, $a, $b); }, ‘<=’ => function ($a, $b) { return sprintf(’%s <= %s’, $a, $b); }, ‘in’ => function ($a, $b) { return sprintf(‘in_array(%s, %s)’, $a, $b); }, ]; $defaultOperators = [ ‘sum’ => function () { return array_sum(func_get_args()); }, ]; $definitions = new Definitions($defaultOperators, $defaultInlineOperators); return $definitions->mergeWith($customOperators); }}在RulerZParserParser中,有如下方法:public function parse($rule){ if ($this->parser === null) { $this->parser = Compiler\Llk::load( new File\Read(DIR.’/../Grammar.pp’) ); } $this->nextParameterIndex = 0; return $this->visit($this->parser->parse($rule));}这里要解读一个核心语法文件://// Hoa////// @license//// New BSD License//// Copyright © 2007-2015, Ivan Enderlin. All rights reserved.//// Redistribution and use in source and binary forms, with or without// modification, are permitted provided that the following conditions are met:// * Redistributions of source code must retain the above copyright// notice, this list of conditions and the following disclaimer.// * Redistributions in binary form must reproduce the above copyright// notice, this list of conditions and the following disclaimer in the// documentation and/or other materials provided with the distribution.// * Neither the name of the Hoa nor the names of its contributors may be// used to endorse or promote products derived from this software without// specific prior written permission.//// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS”// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE// POSSIBILITY OF SUCH DAMAGE.//// Inspired from \Hoa\Ruler\Grammar.//// @author Stéphane Py <stephane.py@hoa-project.net>// @author Ivan Enderlin <ivan.enderlin@hoa-project.net>// @author Kévin Gomez <contact@kevingomez.fr>// @copyright Copyright © 2007-2015 Stéphane Py, Ivan Enderlin, Kévin Gomez.// @license New BSD License%skip space \s// Scalars.%token true (?i)true%token false (?i)false%token null (?i)null// Logical operators%token not (?i)not\b%token and (?i)and\b%token or (?i)or\b%token xor (?i)xor\b// Value%token string ("|’)(.?)(?<!\)\1%token float -?\d+.\d+%token integer -?\d+%token parenthesis_ (%token parenthesis )%token bracket [%token bracket ]%token comma ,%token dot .%token positional_parameter ?%token named_parameter :[a-z-A-Z0-9]+%token identifier [^\s()[],.]+#expression: logical_operation()logical_operation: operation() ( ( ::and:: #and | ::or:: #or | ::xor:: #xor ) logical_operation() )?operation: operand() ( <identifier> logical_operation() #operation )?operand: ::parenthesis_:: logical_operation() ::parenthesis:: | value()parameter: <positional_parameter> | <named_parameter>value: ::not:: logical_operation() #not | <true> | <false> | <null> | <float> | <integer> | <string> | parameter() | variable() | array_declaration() | function_call()variable: <identifier> ( object_access() #variable_access )*object_access: ::dot:: <identifier> #attribute_access#array_declaration: ::bracket:: value() ( ::comma:: value() ) ::bracket::#function_call: <identifier> ::parenthesis:: ( logical_operation() ( ::comma:: logical_operation() )* )? ::parenthesis::上面Llk::load方法会加载这个基础语法内容并解析出片段tokens,tokens解析的逻辑就是正则匹配出我们需要的一些操作符和基础标识符,并将对应的正则表达式提取出来:array:1 [▼ “default” => array:20 [▼ “skip” => “\s” “true” => “(?i)true” “false” => “(?i)false” “null” => “(?i)null” “not” => “(?i)not\b” “and” => “(?i)and\b” “or” => “(?i)or\b” “xor” => “(?i)xor\b” “string” => “("|’)(.*?)(?<!\)\1” “float” => “-?\d+.\d+” “integer” => “-?\d+” “parenthesis” => “(” “parenthesis” => “)” “bracket” => “[” “bracket” => “]” “comma” => “,” “dot” => “.” “positional_parameter” => “?” “named_parameter” => “:[a-z-A-Z0-9]+” “identifier” => “[^\s()[],.]+” ]]这一步也会生成一个rawRulesarray:10 [▼ “#expression” => " logical_operation()” “logical_operation” => " operation() ( ( ::and:: #and | ::or:: #or | ::xor:: #xor ) logical_operation() )?" “operation” => " operand() ( <identifier> logical_operation() #operation )?" “operand” => " ::parenthesis_:: logical_operation() ::parenthesis:: | value()" “parameter” => " <positional_parameter> | <named_parameter>" “value” => " ::not:: logical_operation() #not | <true> | <false> | <null> | <float> | <integer> | <string> | parameter() | variable() | array_declaration() | function_call( ▶" “variable” => " <identifier> ( object_access() #variable_access )*" “object_access” => " ::dot:: <identifier> #attribute_access" “#array_declaration” => " ::bracket:: value() ( ::comma:: value() )* ::bracket::" “#function_call” => " <identifier> ::parenthesis:: ( logical_operation() ( ::comma:: logical_operation() )* )? ::_parenthesis::"]这个rawRules会通过analyzer类的analyzeRules方法解析替换里面的::表示的空位,根据$_ppLexemes属性的值,Compiler\Llk\Lexer()词法解析器会将rawRules数组每一个元素解析放入双向链表栈(SplStack)中,然后再通过对该栈插入和删除操作,形成一个包含所有操作符和token实例的数组$rules。array:54 [▼ 0 => Concatenation {#64 ▶} “expression” => Concatenation {#65 ▼ #_name: “expression” #_children: array:1 [▼ 0 => 0 ] #_nodeId: “#expression” #_nodeOptions: [] #_defaultId: “#expression” #_defaultOptions: [] #_pp: " logical_operation()" #_transitional: false } 2 => Token {#62 ▶} 3 => Concatenation {#63 ▼ #_name: 3 #_children: array:1 [▼ 0 => 2 ] #_nodeId: “#and” #_nodeOptions: [] #_defaultId: null #_defaultOptions: [] #_pp: null #_transitional: true } 4 => Token {#68 ▶} 5 => Concatenation {#69 ▶} 6 => Token {#70 ▶} 7 => Concatenation {#71 ▶} 8 => Choice {#72 ▶} 9 => Concatenation {#73 ▶} 10 => Repetition {#74 ▶} “logical_operation” => Concatenation {#75 ▶} 12 => Token {#66 ▶} 13 => Concatenation {#67 ▶} 14 => Repetition {#78 ▶} “operation” => Concatenation {#79 ▶} 16 => Token {#76 ▶} 17 => Token {#77 ▶} 18 => Concatenation {#82 ▶} “operand” => Choice {#83 ▶} 20 => Token {#80 ▶} 21 => Token {#81 ▼ #_tokenName: “named_parameter” #_namespace: null #_regex: null #_ast: null #_value: null #_kept: true #_unification: -1 #_name: 21 #_children: null #_nodeId: null #_nodeOptions: [] #_defaultId: null #_defaultOptions: [] #_pp: null #_transitional: true } “parameter” => Choice {#86 ▶} 23 => Token {#84 ▶} 24 => Concatenation {#85 ▶} 25 => Token {#89 ▶} 26 => Token {#90 ▶} 27 => Token {#91 ▶} 28 => Token {#92 ▶} 29 => Token {#93 ▶} 30 => Token {#94 ▶} “value” => Choice {#95 ▶} 32 => Token {#87 ▶} 33 => Concatenation {#88 ▶} 34 => Repetition {#98 ▶} “variable” => Concatenation {#99 ▶} 36 => Token {#96 ▶} 37 => Token {#97 ▶} “object_access” => Concatenation {#102 ▶} 39 => Token {#100 ▶} 40 => Token {#101 ▶} 41 => Concatenation {#105 ▶} 42 => Repetition {#106 ▶} 43 => Token {#107 ▶} “array_declaration” => Concatenation {#108 ▶} 45 => Token {#103 ▶} 46 => Token {#104 ▶} 47 => Token {#111 ▶} 48 => Concatenation {#112 ▶} 49 => Repetition {#113 ▶} 50 => Concatenation {#114 ▶} 51 => Repetition {#115 ▶} 52 => Token {#116 ▶} “function_call” => Concatenation {#117 ▶}]然后返回HoaCompilerLlkParser实例,这个实例有一个parse方法,正是此方法构成了一个语法树。public function parse($text, $rule = null, $tree = true) { $k = 1024; if (isset($this->_pragmas[‘parser.lookahead’])) { $k = max(0, intval($this->_pragmas[‘parser.lookahead’])); } $lexer = new Lexer($this->_pragmas); $this->_tokenSequence = new Iterator\Buffer( $lexer->lexMe($text, $this->_tokens), $k ); $this->_tokenSequence->rewind(); $this->_errorToken = null; $this->_trace = []; $this->_todo = []; if (false === array_key_exists($rule, $this->_rules)) { $rule = $this->getRootRule(); } $closeRule = new Rule\Ekzit($rule, 0); $openRule = new Rule\Entry($rule, 0, [$closeRule]); $this->_todo = [$closeRule, $openRule]; do { $out = $this->unfold(); if (null !== $out && ‘EOF’ === $this->_tokenSequence->current()[’token’]) { break; } if (false === $this->backtrack()) { $token = $this->_errorToken; if (null === $this->_errorToken) { $token = $this->_tokenSequence->current(); } $offset = $token[‘offset’]; $line = 1; $column = 1; if (!empty($text)) { if (0 === $offset) { $leftnl = 0; } else { $leftnl = strrpos($text, “\n”, -(strlen($text) - $offset) - 1) ?: 0; } $rightnl = strpos($text, “\n”, $offset); $line = substr_count($text, “\n”, 0, $leftnl + 1) + 1; $column = $offset - $leftnl + (0 === $leftnl); if (false !== $rightnl) { $text = trim(substr($text, $leftnl, $rightnl - $leftnl), “\n”); } } throw new Compiler\Exception\UnexpectedToken( ‘Unexpected token “%s” (%s) at line %d and column %d:’ . “\n” . ‘%s’ . “\n” . str_repeat(’ ‘, $column - 1) . ‘↑’, 0, [ $token[‘value’], $token[’token’], $line, $column, $text ], $line, $column ); } } while (true); if (false === $tree) { return true; } $tree = $this->_buildTree(); if (!($tree instanceof TreeNode)) { throw new Compiler\Exception( ‘Parsing error: cannot build AST, the trace is corrupted.’, 1 ); } return $this->_tree = $tree; }我们得到的一个完整的语法树是这样的:Rule {#120 ▼ #_root: Operator {#414 ▼ #_name: “and” #_arguments: array:2 [▼ 0 => Operator {#398 ▼ #_name: “=” #_arguments: array:2 [▼ 0 => Context {#396 ▼ #_id: “gender” #_dimensions: [] } 1 => Parameter {#397 ▼ -name: “gender” } ] #_function: false #_laziness: false #_id: null #_dimensions: [] } 1 => Operator {#413 ▼ #_name: “and” #_arguments: array:2 [▼ 0 => Operator {#401 ▼ #_name: “>” #_arguments: array:2 [▼ 0 => Context {#399 ▶} 1 => Parameter {#400 ▶} ] #_function: false #_laziness: false #_id: null #_dimensions: [] } 1 => Operator {#412 ▶} ] #_function: false #_laziness: true #_id: null #_dimensions: [] } ] #_function: false #_laziness: true #_id: null #_dimensions: [] }}这里有根节点、子节点、操作符参数以及HoaRulerModelOperator实例。这时$executorModel = $compilationTarget->compile($ast, $context);就可以通过NativeVisitor的visit方法对这个语法树进行访问和分析了。这一步走的是visitOperator() /** * {@inheritdoc} */ public function visitOperator(AST\Operator $element, &$handle = null, $eldnah = null) { $operatorName = $element->getName(); // the operator does not exist at all, throw an error before doing anything else. if (!$this->operators->hasInlineOperator($operatorName) && !$this->operators->hasOperator($operatorName)) { throw new OperatorNotFoundException($operatorName, sprintf(‘Operator “%s” does not exist.’, $operatorName)); } // expand the arguments $arguments = array_map(function ($argument) use (&$handle, $eldnah) { return $argument->accept($this, $handle, $eldnah); }, $element->getArguments()); // and either inline the operator call if ($this->operators->hasInlineOperator($operatorName)) { $callable = $this->operators->getInlineOperator($operatorName); return call_user_func_array($callable, $arguments); } $inlinedArguments = empty($arguments) ? ’’ : ‘, ‘.implode(’, ‘, $arguments); // or defer it. return sprintf(‘call_user_func($operators["%s"]%s)’, $operatorName, $inlinedArguments); }返回的逻辑代码可以通过得到:$executorModel->getCompiledRule() ...

March 4, 2019 · 10 min · jiezi

如何编写简单的parser(实践篇)

上一篇(《如何编写简单的parser(基础篇)》)中介绍了编写一个parser所需具备的基础知识,接下来,我们要动手实践一个简单的parser,既然是“简单”的parser,那么,我们就要为这个parser划定范围,否则,完整的JavaScript语言parser的复杂度就不是那么简单的了。划定范围基于能够编写简单实用的JavaScript程序和具备基础语法的解释能力这两点考虑,我们将parser的规则范围划分如下:声明:变量声明 & 函数声明赋值:赋值操作 (& 左表达式)加减乘除:加减操作 & 乘除操作条件判断:if语句如果用一句话来划分的话,即一个能解析包括声明、赋值、加减乘除、条件判断的解析器。功能划分基于上一篇中介绍的JavaScript语言由词组(token)组成表达式(expression),由表达式组成语句(statement)的模式,我们将parser划分为——负责解析词法的TokenSteam模块,负责解析表达式和语句的Parser,另外,负责记录读取代码位置的InputSteam模块。这里,有两点需要进行说明:由于我们这里包含的expression解析类型和statement的解析类型都不多,所以,我们使用一个parser模块来统一解析,但是在如babel-parser这类完整的parser中,是将expression和statement拆开进行解析的,这里的逻辑仅供参考;另外,这里对词法的解析是逐字进行解析,并没有使用正则表达式进行匹配解析,因为在完整度高的parser中,使用正则匹配词法会提高整体的复杂度。InputSteamInputSteam负责读取和记录当前代码的位置,并把读取到的代码交给TokenSteam处理,其意义在于,当传递给TokenSteam的代码需要进行判读猜测时,能够记录当前读取的位置,并在接下来的操作汇总回滚到之前的读取位置,也能在发生语法错误时,准确指出错误发生在代码段的第几行第几个字符。该模块是功能最简洁的模块,我们只需创建一个类似“流”的对象即可,其中主要包含以下几个方法:peek() —— 阅读下一个代码,但是不会将当前读取位置迁移,主要用于存在不确定性情况下的判读;next() —— 阅读下一个代码,并移动读取位置到下一个代码,主要用于确定性的语法读取;eof() —— 判断是否到当前代码的结束部分;croak(msg) —— 抛出读取代码的错误。接下来,我们看一下这几个方法的实现:function InputStream(input) { var pos = 0, line = 1, col = 0; return { next : next, peek : peek, eof : eof, croak : croak, }; function next() { var ch = input.charAt(pos++); if (ch == “\n”) line++, col = 0; else col++; return ch; } function peek() { return input.charAt(pos); } function eof() { return peek() == “”; } function croak(msg) { throw new Error(msg + " (" + line + “:” + col + “)”); }}TokenSteam我们依据一开始划定的规则范围 —— 一个能解析包括声明、赋值、加减乘除、条件判断的解析器,来给TokenSteam划定词法解析的范围:变量声明 & 函数声明:包含了变量、“var”关键字、“function”关键字、“{}”符号、“()”符号、“,”符号的识别;赋值操作:包含了“=”操作符的识别;加减操作 & 乘除操作:包含了“+”、“-”、“”、“/”操作符的识别;if语句:包含了“if”关键字的识别;字面量(毕竟没有字面量也没办法赋值):包括了数字字面量和字符串字面量。接下来,TokenSteam主要使用InputSteam读取并判读代码,将代码段解析为符合ECMAScript标准的词组流,返回的词组流大致如下:{ type: “punc”, value: “(” } // 符号,包含了()、{}、,{ type: “num”, value: 5 } // 数字字面量{ type: “str”, value: “Hello World!” } // 字符串字面量{ type: “kw”, value: “function” } // 关键字,包含了function、var、if{ type: “var”, value: “a” } // 标识符/变量{ type: “op”, value: “!=” } // 操作符,包含+、-、、/、=其中,不包含空白符和注释,空白符用于分隔词组,对于已经解析了的词组流来说并无意义,至于注释,在我们简单的parser中,就不需要解析注释来提高复杂度了。有了需要判读的词组,我们只需根据ECMAScript标准的定义,进行适当的简化,便能抽取出对应词组需要的判读规则,大致逻辑如下:首先,跳过空白符;如果input.eof()返回true,则结束判读;如果input.peek()返回是一个“"”,接下来,读取一个字符串字面量;如果input.peek()返回是一个数字,接下来,读取一个数字字面量;如果input.peek()返回是一个字母,接下来,读取的可能是一个标识符,也可能是一个关键字;如果input.peek()返回是标点符号中的一个,接下来,读取一个标点符号;如果input.peek()返回是操作符中的一个,接下来,读取一个操作符;如果没有匹配以上的条件,则使用input.croak()抛出一个语法错误。以上的,即是TokenSteam工作的主要逻辑了,我们只需不断重复以上的判断,即能成功将一段代码,解析成为词组流了,将该逻辑整理为代码如下:function read_next() { read_while(is_whitespace); if (input.eof()) return null; var ch = input.peek(); if (ch == ‘"’) return read_string(); if (is_digit(ch)) return read_number(); if (is_id_start(ch)) return read_ident(); if (is_punc(ch)) return { type : “punc”, value : input.next() }; if (is_op_char(ch)) return { type : “op”, value : read_while(is_op_char) }; input.croak(“Can’t handle character: " + ch);}主逻辑类似于一个分发器(dispatcher),识别了接下来可能的工作之后,便将工作分发给对应的处理函数如read_string、read_number等,处理完成后,便将返回结果吐出。需要注意的是,我们并不需要一次将所有代码全部解析完成,每次我们只需将一个词组吐给parser模块进行处理即可,以避免还没有解析完词组,就出现了parser的错误。为了使大家更清晰的明确词法解析器的工作,我们列出数字字面量的解析逻辑如下:// 使用正则来判读数字function is_digit(ch) { return /[0-9]/i.test(ch);}// 读取数字字面量function read_number() { var has_dot = false; var number = read_while(function(ch){ if (ch == “.”) { if (has_dot) return false; has_dot = true; return true; } return is_digit(ch); }); return { type: “num”, value: parseFloat(number) };}其中read_while函数在主逻辑和数字字面量中都出现了,该函数主要负责读取符合格则的一系列代码,该函数的代码如下:function read_while(predicate) { var str = “”; while (!input.eof() && predicate(input.peek())) str += input.next(); return str;}最后,TokenSteam需要将解析的词组吐给Parser模块进行处理,我们通过next()方法,将读取下一个词组的功能暴露给parser模块,另外,类似TokenSteam需要判读下一个代码的功能,parser模块在解析表达式和语句的时候,也需要通过下一个词组的类型来判读解析表达式和语句的类型,我们将该方法也命名为peek()。function TokenStream(input) { var current = null; function peek() { return current || (current = read_next()); } function next() { var tok = current; current = null; return tok || read_next(); } function eof() { return peek() == null; } // 主代码逻辑 function read_next() { //…. } // … return { next : next, peek : peek, eof : eof, croak : input.croak }; }在next()函数中,需要注意的是,因为有可能在之前的peek()判读中,已经调用read_next()来进行判读了,所以,需要用一个current变量来保存当前正在读的词组,以便在调用next()的时候,将其吐出。Parser最后,在Parser模块中,我们对TokenSteam模块读取的词组进行解析,这里,我们先讲一下最后Parser模块输出的内容,也就是上一篇当中讲到的抽象语法树(AST),这里,我们依然参考babel-parser的AST语法标准,在该标准中,代码段都是被包裹在Program节点中的(其实也是大部分AST标准的模式),这也为我们Parser模块的工作指明了方向,即自顶向下的解析模式:function parse_toplevel() { var prog = []; while (!input.eof()) { prog.push(parse_statement()); } return { type: “prog”, prog: prog };}该parse_toplevel函数,即是Parser模块的主逻辑了,逻辑也很简单,代码段既然是有语句(statements)组成的,那么我们就不停地将词组流解析为语句即可。parse_statement和TokenSteam类似的是,parse_statement也是一个类似于分发器(dispatcher)的函数,我们根据一个词组来判读接下来的工作:function parse_statement() { if(is_punc(”;")) skip_punc(";"); else if (is_punc("{")) return parse_block(); else if (is_kw(“var”)) return parse_var_statement(); else if (is_kw(“if”)) return parse_if_statement(); else if (is_kw(“function”)) return parse_func_statement(); else if (is_kw(“return”)) return parse_ret_statement(); else return parse_expression();}当然,这样的分发模式,也是只限定于我们在最开始划定的规则范围,得益于规则范围小的优势,parse_statement函数的逻辑得以简化,另外,虽然语句(statements)是由表达式(expressions)组成的,但是,表达式(expression)依然能单独存在于代码块中,所以,在parse_statement的最后,不符合所有语句条件的情况,我们还是以表达式进行解析。parse_function在语句的解析中,我们拿函数的的解析来作一个例子,依据AST标准的定义以及ECMAScript标准的定义,函数的解析规则变得很简单:function parse_function(isExpression) { skip_kw(“function”); return { type: isExpression?“FunctionExpression”:“FunctionDeclaration”, id: is_punc("(")?null:parse_identifier(), params: delimited("(", “)”, “,”, parse_identifier), body: parse_block() };}对于函数的定义:首先一定是以关键字“function”开头;其后,若是匿名函数,则没有函数名标识符,否则,则解析一个标识符;接下来,则是函数的参数,包含在一对“()”中,以“,”间隔;最后,即是函数的函数体。在代码中,解析参数的函数delimited是依据传入规则,在起始符与结束符之间,以间隔符隔断的代码段来进行解析的函数,其代码如下:function delimited(start, stop, separator, parser) { var res = [], first = true; skip_punc(start); while (!input.eof()) { if (is_punc(stop)) break; if (first) first = false; else skip_punc(separator); if (is_punc(stop)) break; res.push(parser()); } skip_punc(stop); return res;}至于函数体的解析,就比较简单了,因为函数体即是多段语句,和程序体的解析是一致的,ECMAScript标准的定义也很清晰:function parse_block() { var body = []; skip_punc("{"); while (!is_punc("}")) { var sts = parse_statement() sts && body.push(sts); } skip_punc("}"); return { type: “BlockStatement”, body: body }}parse_atom & parse_expression接下来,语句的解析能力具备了,该轮到解析表达式了,这部分,也是整个Parser比较难理解的一部分,这也是为什么将这部分放到最后的原因。因为在解析表达式的时候,会遇到一些不确定的过程,比如以下的代码:(function(a){return a;})(a)当我们解析完成第一对“()”中的函数表达式后,如果此时直接返回一个函数表达式,那么后面的一对括号,则会被解析为单独的标识符。显然这样的解析模式是不符合JavaScript语言的解析模式的,这时,往往我们需要在解析完一个表达式后,继续往后进行尝试性的解析。这一点,在parse_atom和parse_expression中都有所体现。回到正题,parse_atom也是一个分发器(dispatcher),主要负责表达式层面上的解析分发,主要逻辑如下:function parse_atom() { return maybe_call(function(){ if (is_punc("(")) { input.next(); var exp = parse_expression(); skip_punc(")"); return exp; } if (is_kw(“function”)) return parse_function(true) var tok = input.next(); if (tok.type == “var” || tok.type == “num” || tok.type == “str”) return tok; unexpected(); });}该函数一开头便是以一个猜测性的maybe_call函数开头,正如上我们解释的原因,maybe_call主要是对于调用表达式的一个猜测,一会我们在来看这个maybe_call的实现。parse_atom识别了位于“()”符号中的表达式、函数表达式、标识符、数字和字符串字面量,若都不符合以上要求,则会抛出一个语法错误。parse_expression的实现,主要处理了我们在最开始规则中定义的加减乘除操作的规则,具体实现如下:function parse_expression() { return maybe_call(function(){ return maybe_binary(parse_atom(), 0); });}这里又出现了一个maybe_binary的函数,该函数主要处理了加减乘除的操作,这里看到maybe开头,便能知道,这里也有不确定的判断因素,所以,接下来,我们统一讲一下这些maybe开头的函数。maybe_这些以maybe开头的函数,如我们以上讲的,为了处理表达式的不确定性,需要向表达式后续的语法进行试探性的解析。maybe_call函数的处理非常简单,它接收一个用于解析当前表达式的函数,并对该表达式后续词组进行判读,如果后续词组是一个“(”符号词组,那么该表达式一定是一个调用表达式(CallExpression),那么,我们就将其交给parse_call函数来进行处理,这里,我们又用到之前分隔解析的函数delimited。// 推测表达式是否为调用表达式function maybe_call(expr) { expr = expr(); return is_punc("(") ? parse_call(expr) : expr;}// 解析调用表达式function parse_call(func) { return { type: “call”, func: func, args: delimited("(", “)”, “,”, parse_expression), };}由于解析加、减、乘、除操作时,涉及到不同操作符的优先级,不能使用正常的从左至右进行解析,使用了一种二元表达式的模式进行解析,一个二元表达式包含了一个左值,一个右值,一个操作符,其中,左右值可以为其他的表达式,在后续的解析中,我们就能根据操作符的优先级,来决定二元的树状结构,而二元的树状结构,就决定了操作的优先级,具体的优先级和maybe_binary的代码如下:// 操作符的优先级,值越大,优先级越高var PRECEDENCE = { “=”: 1, “||”: 2, “&&”: 3, “<”: 7, “>”: 7, “<=”: 7, “>=”: 7, “==”: 7, “!=”: 7, “+”: 10, “-”: 10, “”: 20, “/”: 20, “%”: 20,};// 推测是否是二元表达式,即看该左值接下来是否是操作符function maybe_binary(left, my_prec) { var tok = is_op(); if (tok) { var his_prec = PRECEDENCE[tok.value]; if (his_prec > my_prec) { input.next(); return maybe_binary({ type : tok.value == “=” ? “assign” : “binary”, operator : tok.value, left : left, right : maybe_binary(parse_atom(), his_prec) }, my_prec); } } return left;}需要注意的是,maybe_binary是一个递归处理的函数,在返回之前,需要将当前的表达式以当前操作符的优先级进行二元表达式的解析,以便包含在另一个优先级较高的二元表达式中。为了让大家更方便理解二元的树状结构如何决定优先级,这里举两个例子:// 表达式一1+23// 表达式二12+3这两段加法乘法表达式使用上面的方法解析后,分别得到如下的AST:// 表达式一{ type : “binary”, operator : “+”, left : 1, right : { type: “binary”, operator: “”, left: 2, // 这里简化了左右值的结构 right: 3 }}// 表达式二{ type : “binary”, operator : “+”, left : { type : “binary”, operator : “”, left : 1, right : 2 }, right : 3}可以看到,经过优先级的处理后,优先级较为低的操作都被处理到了外层,而优先级高的部分,则被处理到了内部,如果你还感到迷惑的话,可以试着自己拿几个表达式进行处理,然后一步一步的追踪代码的执行过程,便能明白了。总结其实,说到底,简单的parser复杂度远比完整版的parser低很多,如果想要更进一步的话,可以尝试去阅读babel-parser的源码,相信,有了这两篇文章的铺垫,babel的源码阅读起来也会轻松不少。另外,在文章的最后,附上该篇文章的demo。参考几篇可以参考的原文,推荐大伙看看:《How to implement a programming language in JavaScript》(http://lisperator.net/pltut/)《Parsing in JavaScript: Tools and Libraries》(https://tomassetti.me/parsing…)标准以及文献:《ECMAScript® 2016 Language Specification》(http://www.ecma-international…)the core @babel/parser (babylon) AST node types(https://github.com/babel/babe…) ...

January 30, 2019 · 4 min · jiezi

如何编写简单的parser(基础篇)

什么是parser?简单的说,parser的工作即是将代码片段转换成计算机可读的数据结构的过程。这个“计算机可读的数据结构”更专业的说法是“抽象语法树(abstract syntax tree)”,简称AST。AST是代码片段具体语义的抽象表达,它不包含该段代码的所有细节,比如缩进、换行这些细节,所以,我们可以使用parser转换出AST,却不能使用AST还原出“原”代码,当然,可以还原出语义一致的代码,就如同将ES6语法的js代码转换成ES5的代码。parser的结构一般来说,一个parser会由两部分组成:词法解析器(lexer/scanner/tokenizer)对应语法的解释器(parser)在解释某段代码的时候,先由词法解释器将代码段转化成一个一个的词组流(token),再交由解释器对词组流进行语法解释,转化为对应语法的抽象解释,即是AST了。为了让大家更清楚的理解parser两部分的工作顺序,我们通过一个例子来进行说明:437 + 734在parser解析如上的计算表达式时,词法解析器首先依次扫描到“4”、“3”、“7”直到一个空白符,这时,词法解析器便将之前扫描到的数字组成一个类型为“NUM”的词组(token);接下来,词法解析器继续向下扫描,扫描到了一个“+”,对应输出一个类型为“PLUS”的词组(token);最后,扫描“7”、“3”、“4”输出另一个类型为“NUM”的词组(token)。语法解释器在拿到词法解析器输出的词组流后,根据词组流的“NUM”,“PLUS”,“NUM”的排列顺序,解析成为加法表达式。由上的例子我们可以看出,词法解析器根据一定的规则对字符串进行解析并输出为词组(token),具体表现为连续不断的数字组合(“4”、“3”、“7”和“7”、“3”、“4”)即代表了数字类型的词组;语法解释器同样根据一定的规则对词组的组合进行解析,并输出对应的表达式或语句。在这里,词法解析器应用的规则即为词汇语法(Lexical Grammar)的定义,语法解释器应用的规则即为表达式(Expressions)、语句(Statements)、声明(Declarations)和函数(Functions)等的定义。ECMAScript标准看到这里大家可能会感觉到奇怪,为什么讲parser讲的好好的,又跑到ECMAScript标准上来了呢?因为以上提到的词汇语法(Lexical Grammar)、表达式(Expressions)、语句(Statements)、声明(Declarations)和函数(Functions)等都是ECMAScript标准中的所定义的,这其实也是ECMAScript标准的作用之一,即定义JavaScript的标准语法。词汇词法(Lexical Grammar)ECMAScript的词汇词法规定了JavaScript中的基础语法,比如哪些字符代表了空白(White Space),哪些字符代表了一行终止(Line Terminators),哪些字符的组合代表了注释(Comments)等。具体的规定说明,可以在ECMAScript标准11章中找到。这里我们不仔细研读每个语法的定义,只需知道词法解析器(lexer)判读词组(token)的依据来源于此即可,为了让大家有一定的了解,这里,我们拿上面例子中的数字字面量(Numeric Literals)来进行说明:ECMAScript标准中,对数字字面量的定义如下:该定义需要自上向下解读:首先,规则定义了数字字面量(Numeric Literal)可以是十进制字面量(Decimal Literal)、二进制整数字面量(Binary Integer Literal)、八进制整数字面量(Octal Integer Literal)、十六进制整数字面量(Hex Integer Literal);在我们的例子中,我们只关心十进制的字面量,所以,接下来,规则定义十进制字面量(Decimal Literal)可以是包含小数点与不包含小数点的组合,这里我们只需关注不包含小数点的定义,即十进制整数字面量(Decimal Integer Literal) + 可选的指数部分(Exponent Part);最后,规则定义十进制整数字母量由非零数字(Non Zero Digit)+ 十进制数字(Decimal Digit)或十进制数字组(Decimal Digits)组成,非零数字是由19的数字组成,十进制数字是由09组成。将上面的定义重新整合后,就能得到我们需要的数字字面量的定义规则:非零数字(19)+十进制数字组(09)需要注意的是,这是简化版的数字字面量定义,完整版的需要加上以上规则中的所有分支条件。表达式(Expressions)、语句(Statements)ECMAScript标准12~13章包含了表达式和语句的相关定义,之前由词法解析器(lexer)处理后生成的词组流(token)交由语法解释器(parser)处理的主要内容,即是处理词组流构成的表达式与语句。在这里,我们需要稍加明确一下表达式与语句之间的不同与关系:首先,语句包含表达式,大部分语句是由关键字+表达式或语句组成,而表达式则是由字面量(Literal)、标识符(Identifier)、符号(Punctuators)等低一级的词组组成;其次,表达式一般来讲会产生一个值,而语句不总有值。理解第一点对于我们写语法解释器很重要,由于语句是由表达式组成的,而表达式是有词组组成的,词组是有词法解析器进行解析生成的,所以,在语法解释器中,将以表达式为切入点,由表达式解析再深入到语句解析中。抽象语法树(AST)了解一个parser的结构,以及parser解析语法所依赖的规则后,接下来,我们需要了解一下一个parser所生产出来的结果——抽象语法树。在文章的开头,我有简单的解释抽象语法树即是具体代码片段的抽象表达,那它具体是长什么样的呢?function sum (a , b) { return a+b;}以上的代码片段,AST树的描述如下(使用babylon7-7.0.0-beta.44,结果进行了简化):{ “type”: “Program”, “body”: [ { “type”: “FunctionDeclaration”, “id”: { “type”: “Identifier”, “name”: “sum” }, “params”: [ { “type”: “Identifier”, “name”: “a” }, { “type”: “Identifier”, “name”: “b” } ], “body”: { “type”: “BlockStatement”, “body”: [ { “type”: “ReturnStatement”, “argument”: { “type”: “BinaryExpression”, “left”: { “type”: “Identifier”, “name”: “a” }, “operator”: “+”, “right”: { “type”: “Identifier”, “name”: “b” } } } ] } } ]}对该AST仔细观察一番,便会明白,AST其实即是我们在已经ECMAScript标准对代码进行解析后,将标识符(identifier)、声明(declaration)、表达式(expression)、语句(statement)等按代码表述的逻辑整理成为树状结构。就拿上面的例子来说,当语法解析器识别了一个二元表达式(Binary Expression),便将这个二元表达式所携带的信息——左值,右值,操作符按照固定的计算机可读的数据格式保存下来,即是我们看到的AST树了。当然,AST也需要具备固定的格式,这样计算机才能依照该格式阅读AST并进行接下来的编译工作,当然,有一些AST也被用来转义(如babel)。关于AST定义的规则,我们可以参考babel的定义,这也是后面我们实现parser时,所参考的标准。接下来理解完以上相关的知识,我们便具备编写一个parser的先决条件了,那在下一章,我们将实际操作一番,编写一个简易版本的JavaScript语言parser。 ...

January 22, 2019 · 1 min · jiezi

JavaScript的工作原理:解析、抽象语法树(AST)+ 提升编译速度5个技巧

这是专门探索 JavaScript 及其所构建的组件的系列文章的第 14 篇。如果你错过了前面的章节,可以在这里找到它们:JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述!JavaScript 是如何工作的:深入V8引擎&编写优化代码的5个技巧!JavaScript 是如何工作的:内存管理+如何处理4个常见的内存泄漏 !JavaScript 是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式!JavaScript 是如何工作的:深入探索 websocket 和HTTP/2与SSE +如何选择正确的路径!JavaScript 是如何工作的:与 WebAssembly比较 及其使用场景 !JavaScript 是如何工作的:Web Workers的构建块+ 5个使用他们的场景!JavaScript 是如何工作的:Service Worker 的生命周期及使用场景!JavaScript 是如何工作的:Web 推送通知的机制!JavaScript是如何工作的:使用 MutationObserver 跟踪 DOM 的变化!JavaScript是如何工作的:渲染引擎和优化其性能的技巧!JavaScript是如何工作的:深入网络层 + 如何优化性能和安全!JavaScript是如何工作的:CSS 和 JS 动画底层原理及如何优化它们的性能!概述我们都知道运行一大段 JavaScript 代码性能会变得很糟糕。这段代码不仅需要通过网络传输,而且还需要解析、编译成字节码,最后执行。在之前的文章中,我们讨论了 JS 引擎、运行时和调用堆栈等,以及主要由谷歌 Chrome 和 NodeJS 使用的V8引擎。它们在整个 JavaScript 执行过程中都发挥着至关重要的作用。这篇说的抽象语法树同样重要:在这我们将了解大多数 JavaScript 引擎如何将文本解析为对机器有意义的内容,转换之后发生的事情以及做为 Web 开发者如何利用这一知识。编程语言原理那么,首先让我们回顾一下编程语言原理。不管你使用什么编程语言,你需要一些软件来处理源代码以便让计算机能够理解。该软件可以是解释器,也可以是编译器。无论你使用的是解释型语言(JavaScript、Python、Ruby)还是编译型语言(c#、Java、Rust),都有一个共同的部分:将源代码作为纯文本解析为 抽象语法树(abstract syntax tree, AST) 的数据结构。AST 不仅以结构化的方式显示源代码,而且在语义分析中扮演着重要角色。在语义分析中,编译器验证程序和语言元素的语法使用是否正确。之后,使用 AST 来生成实际的字节码或者机器码。抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。和抽象语法树相对的是具体语法树(concrete syntaxtree),通常称作分析树(parse tree)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树。一旦 AST 被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。AST 程序AST 不仅仅是用于语言解释器和编译器,在计算机世界中,它们还有多种应用。使用它们最常见的方法之一是进行静态代码分析。静态分析器不执行输入的代码,但是,他们仍然需要理解代码的结构。例如,你可能想要实现一个工具,该工具可以找到公共代码结构,以便你可以重构它们以减少重复。你可能会通过使用字符串比较来实现这一点,但这个会相当简单且有局限性。当然,如果你对实现这样的工具感兴趣,你不需要编写自己的解析器。有许多与 Ecmascript规范完全兼容的开源项目。Esprima 和 Acorn 即是黄金搭档,还有许多工具可以帮助解析器生成输出,即 ASTs ,ASTs 被广泛应用于代码转换。例如,你可能希望实现一个将 Python 代码转换为J avaScript 的转换器。基本思想是使用Python 转换器生成 AST,然后使用 AST 生成JavaScript代码。你可能会觉得难以置信,事实是 ASTs 只是部分语言的不同表示法。在解析之前,它被表示为遵循一些规则的文本,这些规则构成了一种语言。在解析之后,它被表示为一个树结构,其中包含与输入文本完全相同的信息。因此,也可以进行反向解析然后回到文本。JavaScript 解析让我们看看 AST 是如何构建的。我们用一个简单的 JavaScript 函数作为例子:function foo(x) { if (x > 10) { var a = 2; return a * x; } return x + 10;}解析器会产生如下的 AST: 注意,为了观看方便,这里是解析器将生成的结果的简化版本。实际的 AST 要复杂得多。然而,这里的目的是为了运行源码之前的第一个步骤前。如果人想查看实际的 AST 是什么样子,可以访问 AST Explorer。它是一个在线工具,你以在其中输入一些 JavaScript 并输出对应的 AST。你可能会问,为什么需要知道 JavaScript解析器工作原理,毕竟这是浏览器工作,你想法是部分正确。下图展示了 JavaScript 执行过程中不同阶段的耗时。仔细瞅瞅,你或许会发现一些有趣的东西。发现没? 通常情况下,浏览器解析 JavaScript 大约需占总执行时间的 15% 到 20%。我没有具体统计过这些数值。这些是来自真实应用程序和以某种方式使用 JavaScript 的网站的统计数据。也许 15% 看起来不是很多,但相信我,这是很多。一个典型的单页程序加载 0.4 mb 左右的 JavaScript,浏览器需要大约 370ms 来解析它。也许你会又说,这也不是很多嘛,本身花费的时间并不多。但请记住,这只是将 JavaScript 代码解析为 AST 所需要的时间。这并不包括运行本身的时间,也不包括在页面加载 ,如 CSS 和 HTML 渲染过程的耗时。这些还只涉及桌面,移动浏览器的情况会更加复杂,在手机上花在解析上的时间通常是桌面浏览器的 2 到 5 倍。上图显示了 1MB JavaScript 包在不同类的移动和桌面浏览器解析时间。更重要的是,为了获得更多类原生的用户体验而把越来越多的业务逻辑堆积在前端,Web 应用程序正变得越来越复杂。你可以轻易地想到网络应用受到的性能影响。只需打开浏览器开发工具,然后使用该工具来解析、编译和浏览器中发生的所有其他事情上所消耗的时间。不幸的是,移动浏览器上没有开发者工具。不过不用担心,这并不意味着你对此无能为力。因为有 DeviceTiming 工具,它可以用来帮助检测受控环境中脚本的解析和运行时间。它通过插入代码来封装本地代码,这样每次从不同的设备访问页面时,就可以在本地测量解析和运行时间。好事就是 JavaScript 引擎做了很多工作来避免冗余的工作,并得到了更好的优化,以下为主流浏览器使用的技术。例如,V8 实现脚本流(script streaming)和代码缓存技术。脚本流即脚本一旦开始下载,async 和 deferred的 脚本就会在单独的线程上解析。这意味着在下载脚本完成后几乎立即完成解析,这会提升 10% 的页面加载速度。每次访问页面时,JavaScript 代码通常编译为字节码。 然而,一旦用户访问另一页面,该字节码就被丢弃。 发生这种情况是因为编译后的代码很大程度上依赖于编译时机器的状态和上下文。 这是 Chrome 42 引入字节码缓存的原因。 该技术会本地缓存编译过的代码,这样当用户返回同一页面时,诸如下载,解析和编译等所有步骤都会被跳过。 这使得 Chrome 可以节省大约 40% 的解析和编译时间。 此外,这还可以节省移动设备的电量。在 Opera 中,Carakan 引擎可以重用另一个程序最近编译过的输出。没有要求代码必须来自相同的页面甚至同个域下。这种缓存技术实际上非常高效,还可以完全跳过编译步骤。它依赖于典型的用户行为和浏览场景:每当用户在应用程序/网站中遵循某个用户的特定浏览习惯,都会加载相同的 JavaScript 代码。不过,Carakan 引擎早已被谷歌的 V8 所取代。Opera 新的 JavaScript 引擎 “Carakan”,目前速度是其他已存在 JavaScript 引擎(基于 SunSpider)的2.5倍。其在转化为本地机器代码时专门针对正则表达式做了优化。Firefox 使用的 SpiderMonkey 引擎不会缓存所有内容。它可以过渡到监视阶段,在这个阶段中,它计算执行给定脚本的次数。基于此计算,它推导出频繁使用而可以被优化的代码部分。SpiderMonkey 是 Mozilla 项目的一部分,是一个用 C 语言实现的 JavaScript 脚本引擎,另外还有一个叫做Rhino 的 Java 版本。显然,有些人决定什么都不做。Safari 的首席开发人员 Maciej Stachowiak 表示,Safari 不会对编译后的字节码进行任何缓存。缓存技术他们是有考虑过的问题,但是他们还没有实现,因为生成代码的耗时小于总运行时间的 2%。这些优化不会直接影响 JavaScript 源代码的解析,但是会尽可能完全避免。毕竟做总比没做好点?我们可以做很多事情来改善应用程序的初始加载时间。最小化加载的 JavaScript 数量:代码越小、解析所需要时间就越少,运行时间也就越小。要做到这一点,我们只能在当前的路由上加载所需的代码,而不是加载一大陀的代码。例如,PRPL模式即表示该种代码传输类型。或者,可以检查代码的依赖关系,看看是否有什么冗余的依赖导致代码库膨胀,然而,这些东西需要很大的篇幅来进行讨论。本文的主要的目的讨论作为 Web 开发人员可以做些什么来帮助 JavaScript 解析器更快地完成它的工作。还有,现代JavaScript 解析器使用 启发法(heuristics) 来决定是否立即运行指定的代码片段或者推迟在未来的某个时候运行。基于这些启发法,解析器将进行即时或懒解析。启发法是针对模型求解方法而言的,是一种逐次逼近最优解的方法。这种方法对所求得的解进行反复判断实践修正直至满意为止。启发法的特点是模型简单,需要进行方案组合的个数少,因此便于找出最终答案。此方法虽不能保证得到最优解,但只要处理得当,可获得决策者满意的近似最优解。一般步骤包括:定义一个计算总费用的方法;报定判别准则;规定方案改选的途径;建立相应的模型;送代求解。立即解析会运行需要立即编译的函数。它主要做三件事:构建 AST,构建作用域层级和查找所有语法错误。另一方面, 懒解析只运行未编译的函数。它不构建AST,也不查找所有语法错误,它只构建作用域层级,与立即解析相比节省了大约一半的时间。显然,这不是一个新概念。即使像 IE 9 这样的浏览器也支持这种类型的优化,尽管与现在的解析器的工作方式相比,这种优化方式还很初级。来看一个例子,假设有以下代码片段:function foo() { function bar(x) { return x + 10; } function baz(x, y) { return x + y; } console.log(baz(100, 200));}foo()就像前面的例子一样,代码被输入到语法分析器中,语法分析器进行语法分析并输出AST,如下:声明函数 foo调用函数 foo在 foo 里声明函数 bar 接收参数 x, 并返回 x 和 10 相加的结果在 foo 里声明函数 baz 接收参数 x和 y, 并返回 x 和 y 相加的结果调用 baz 函数传入 100 和 2。调用 console.log 参数为之前函数调用的返回值。那么期间发生了什么? 解析器看到 bar 函数的声明、baz 函数的声明、bar函数的调用和 console.log 的调用。但是,解析器做了一些完全无关的额外工作即解析 bar 函数。为什么这无关紧要? 因为函数 bar 从来没有被调用过(或者至少在那个时候没有)。这是一个简单的示例,看起来可能有些不同寻常,但在许多实际应用程序中,许多声明的函数从未被调用。这里不解析bar函数,该函数声明了却没有调用它。只在需要的时候在函数运行前进行真正的解析。懒解析仍然需要找到函数的整个主体并为其声明,但仅此而已。它不需要语法树,因为它还没有被处理。另外,它不会从堆中分配内存,而堆通常会占用相当多的系统资源,简而言之,跳过这些步骤会带来很大的性能改进。所以之前的例子,解析器实际上会像如下这样解析:注意,这里只确认 bar 函数声明,没有进入 bar 函数体。在这种情况下,函数体只是一个返回语句。但是,与大多数实际应用程序一样,它可以更大,包含多个返回语句、条件语句、循环、变量声明,甚至嵌套函数声明。这完全是在浪费时间和系统资源,因为这个函数永远不会被调用。这是一个相当简单的概念,但实际上,它的实现是非常难的,不局限于以上示例。整个方法还可以适用于函数、循环、条件、对象等。基本上,所有需要解析的东西。例如,下面是一个非常常见的 JavaScript 模式。var myModule = (function() { // 整个模块的逻辑 // 返回模块对象})();大多数现代 JavaScript 解析器都能识别这种模式,此模式表示代码需要立即解析。那么为什么解析器不都使用懒解析呢? 如果懒解析某些代码,这些代码需要立即执行,这实际上会使代码运行速度变慢。需要运行一次懒解析之后进行另一个立即解析,这和立即解析相比,运行速度会慢 50%。现在对解析器底层原理有了大致的了解,是时候考虑如何提高解析器的解析速度。可以用这种方式编写代码,以便在正确的时间解析函数。大多数解析器都能识别一种模式:使用括号封装函数。对于解析器来说,这几乎总是一个积极的信号,即函数需要立即执行。如果解析器看到一个左括号,紧接着是一个函数声明,它将立即解析这个函数。可以通过显式地声明立即执行的函数来帮助解析器加快解析速度。假设有一个名为 foo 的函数。function foo(x) { return x * 10;}因为没有明显地标识表明需要立即运行该函数所以浏览器会进行懒解析。然而,我们确定这是不对的,那么可以运行两个步骤。首先,将函数存储在一个变量中:var foo = function foo(x) { return x * 10;};注意,这里有使用函数的名称 foo,这不是必需的,但是建议这样做,因为在抛出异常的情况下,stacktrace 会保留实际函数名称,而不仅仅是 <anonymous>。以上事例解析器执行懒解析,可以用括号封装起来,让解析器进行立即解析:var foo = (function foo(x) { return x * 10;});现在,解析器看见 function 关键字前的左括号便会立即进行解析。因为需要知道解析器在哪些情况下执行懒解析或者立即解析,所以很难手动管理。此外,还需要花时间考虑是否立即调用某个函数,肯定没人想这么做的。最后,这种地让代码更难阅读和理解。可以使用 Optimize.js 可以帮我们做这类事情,该工具只是用来优化 JavaScript 源代码的初始加载时间,它们对代码进行静态分析,然后通过使用括号封装需要立即运行的函数以便浏览器立即解析并准备运行它们。像往常一样编码,然后有一段代码看起来像这样的:(function() { console.log(‘Hello, World!’);})();一切看起来都很好,如预期的那样工作,而且速度很快,因为在函数声明之前添加左括号。当然,在进入生产环境之前需要进行代码压缩,以下为压缩工具的输出:!function(){console.log(‘Hello, World!’)}();好像没问题,代码像以前一样工作。但是好像少了什么,压缩工具删除包裹函数的括号,而是在函数前放置了一个感叹号,这意味着解析器将跳过此并将执行惰解析。 最重要的是,为了能够执行该函数,它将在懒解析之后立即进行立即解析。 这会使代码运行得更慢,幸运的是,可以利用 Optimize.js 来解决此类问题,传给 Optimize.js 压缩过的代码会输出如下代码:!(function(){console.log(‘Hello, World!’)})();这还差不多,现在拥有两全其美方案:压缩代码且解析器正确地识别懒解析和立即解析的函数。预编译但为什么不能在服务器端完成所有这些工作呢? 毕竟,最好这样做一次并将结果提供给客户端,而不强制各个客户端重复做该项事情。那么,目前正在讨论引擎是否应该提供一种执行预编译脚本的方法,这样就可以节省浏览器运行时间。从本质上讲,该思路是拥有可以生成字节码的务器端工具,这样只需要传输字节码并在客户端运行,之后会看到启动时间的一些主要差异。 这可能听起来很诱人,但事情并非那么简单,还可能会产生相反的效果,因为它会更大,并且很可能需要签署代码并出于安全原因对其进行处理。 例如,V8 团队正在努力解决重复解析问题,这样预编译有可能实际并没有多大的用处。提升编译速度一些建议检查依赖,减少不必要的依赖分割代码为更小的块而不是一整陀的尽可能推迟加载 JavaScript,按需要加载或者动态加载。使用开发者工具和 DeviceTiming 来检测性能瓶颈用像 Optimize.js 的工具来帮助解析器选择立即解析或者懒解析以加快解析速度原文:https://blog.sessionstack.com…代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》! ...

January 21, 2019 · 2 min · jiezi

babel的初步了解

前段时间开始研究ast,然后慢慢的顺便把babel都研究了,至于ast稍后的时间会写一篇介绍性博客专门介绍ast,本博客先介绍一下babel的基本知识点。背景:由于现在前端出现了很多非es5的语法,如jsx,.vue,ts等等的格式和写法,如果要在浏览器的设备上识别并执行,需要额外将这些非传统格式的语法转成传统的es5格式,而babel插件,就是用来将非es5格式的语法转成es5语法。babel其实是一个解释器,它主要讲进行中的代码分为三个阶段执行:解释,转换,生成。其中babel插件或者其他插件都是在转换阶段起作用。babel核心包:babel既然是个解释器,那么就会拥有解释,遍历,以及生成的一系列工具和api:1)babylon:babel里面用来将js代码词法分析,生成ast,他的结构有些像acron,它的返回的结构里面包含着ast和tokens。require(“babylon”).parse(“code”, { // parse in strict mode and allow module declarations sourceType: “module”, plugins: [ // enable jsx and flow syntax “jsx”, “flow” ]});sourceType: module表示的是在严格模式下解析并且允许模块定义(即能识别import和expor语法);script识别不了。2)babel-traverse:功能就像estraverse一样,主要是给plugin提供遍历ast节点的功能;var babylon = require(‘babylon’);var result = babylon.parse(code, { sourceType: “module”,});console.log(‘result:’, result);import traverse from “babel-traverse”;traverse(result, { enter(node) { console.log(node); }});3)babel-generator:将ast生成js代码;var babylon = require(‘babylon’);var result = babylon.parse(code, { sourceType: “module”,});console.log(‘result:’, result);import traverse from “babel-traverse”;import generate from ‘babel-generator’;traverse(result, { enter(node) { console.log(node); }});var conde1 = generate(result);console.log(‘generate:’, conde1);babel工具包:要完成复杂的转换工作,单靠核心包是不能完成的,所以必要还要依赖于其他工具包辅助。1)babel-types:包含着ast中的所有类型,可以生成一个ast的节点,然后替换真是ast的节点,从而改变ast的内容(ast工具库,类似于lodash,具有校验,创建和转换ast的方法)。import * as t from “babel-types”;console.log(t.stringLiteral(“my-module”));语法:t.anyTypeAnnotation(内容) // 最终返回一个类型的对象2)babel-template:可以通过字符串的形式生成一个ast;import template from “babel-template”;const buildRequire = template( var IMPORT_NAME = require(SOURCE););const ast2 = buildRequire({ IMPORT_NAME: t.identifier(“myModule”), SOURCE: t.stringLiteral(“my-module”)});console.log(‘ast2’, ast2);3)babel-helps: 主要是用来协助babel转换;4)babel-core-frame: 主要是用来将错误信息打印出来;5)babel-cli:babel的命令行工具,通过命令行对js代码进行转译;6)babel-register: 因为babel工具文件,插件里面使用了很多require,而 该文件可以将node中的require于babel中的require绑定,从而可以使用require引入文件;7)babel-plugin-xxx: 在转换过程中使用的插件;8)babel-plugin-transform-xxx: 在transerform过程中使用到的插件;(.babelrc文件:该文件会在babel编译过程中,自动配置babel的参数,babel的运行环境–env,babel的设置—preset,babel的所需要用到的插件—plugins等)9)babel-core:该核心包包含着babel的核心(babel-lon,babel-traverse,babel-generate),提供了更多更友善的api给开发者使用。babel编译原理:编译器就是讲高级的语言或者语法,编译成更进阶机器识别的语言和语法;babel其实更像一个转译器,因为它主要是将高级的js语法转成低级的语法;他们两者虽然有区别,但有很多相似之处(都是经历三个过程:解析,处理,生成);以es6转成es5为例:ES6代码输入 ==》 babylon进行解析 ==》 得到AST ==》 plugin用babel-traverse对AST树进行遍历转译 ==》 得到新的AST树 ==》 用babel-generator通过AST树生成ES5代码babel-pollfill,babel-runtime,transfer-runtime的区别:babel-pollfill是针对于应用和页面范围内,对新的对象和新的语法进行兼容,主要是通过一些辅助函数进行兼容新的语法,但如果针对外部的库使用,就会产生污染全局环境的影响,一般对项目代码使用;babel-runtime是对于外部插件和库的语法兼容,能将新的对象和语法,通过在运行时,把对应的可识别的语法和对象匹配出来并进行转换,从而显示在运行时进行语法降级兼容,且不会产生全局污染,一般对外部的插件使用;transfer-babel是对babel-runtime进行封装,新的语法,对象能通过该插件,换种形式引用runtime的东西;(其实runtime,pollfill都是建立在core-js之上的)。对于babel的插件,主要是因为生成的ast的底层中有一个accept方法,专门用来接收visitor(插件)访问者对象,然后在visitor中定义各种节点类型的操作-visite,每个visite都可以接受一个path参数(节点信息,节点和位置信息的对象,其包含很多有用的方法),在visit中处理path,从而实现转换的作用。const result = babel.transform(code, { plugins: [{ visitor }]})console.log(result.code);至于visitor后续会详细介绍。整个babel的结构图,我大概花了一张图表示出来:而babel和webpack的协同开发,我也大概花了一张图表示他们之间的关系,但里面的原理,我后续会再去研究,研究好再分享一下:以上是我对babel的初步理解,如果有不正确的地方,欢迎指出。 ...

December 24, 2018 · 1 min · jiezi