本文概要javascript
本文将经过如下几个方面对AST进行学习:css
1. 为何要了解AST,简要说明AST在开发中的重要性;html
AST(抽象语法树)在开发过程当中扮演一个很是重要的角色,可是咱们却不多去直接接触它。vue
不管是代码编译(babel),打包(webpack),代码压缩,css预处理,代码校验(eslint),代码美化(pretiier),Vue中对template的编译,这些的实现都离不开AST。java
了解学习AST,可以帮助咱们更好的对上面说的这些工具原理进行理解,同时,咱们能够利用它去开发一些工具,来优化咱们的开发流程,提升开发效率。webpack
AST是对源代码的抽象语法结构的树状表现形式。es6
在不一样的场景下,会有不一样的解析器将源码解析成抽象语法树。web
下面直观的看一下AST是什么样的。正则表达式
代码:express
let answer = 2 * 3;
对应的AST语法树:
{ "type": "Program", "body": [ { "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "answer" }, "init": { "type": "BinaryExpression", "operator": "*", "left": { "type": "Literal", "value": 2, "raw": "2" }, "right": { "type": "Literal", "value": 3, "raw": "3" } } } ], "kind": "let" } ], "sourceType": "script" }
那么AST是如何生成的呢?
AST是经过JS Parser (解析器),将js源码转化为抽象语法树,主要分为两步:
将整个的代码字符串,分割成 语法单元数组(token)。
JS中的语法单元(token)指标识符(function,return),运算符,括号,数字,字符串等能解析的最小单元。主要有如下几种:
没有被引号括起来的连续字符,能够包含字母、数字、_、$,其中数字不能做为开头。
标识符多是var,return,function等关键字,也多是true,false这样的内置常量,或是一个变量。具体是哪一种语义,分词阶段不区分,只要正确拆分便可。
十六进制,十进制,八进制以及科学表达式等都是最小单元。
+、-、 *、/ 等。
对计算机而言,字符串只会参与计算和展现,具体里面细分不必分析。
不论是行注释仍是块注释,对于计算机来讲并不关心其内容,因此能够做为不可再拆分的最小单元。
连续的空格,换行,缩进等,只要不在字符串中都没有实际的逻辑意义,因此连续的空格能够做为一个语法单元。
大括号,中括号,小括号,冒号 等等。
依然拿上面的代码做为例子,分词后生成的语法单元数组以下:
[ { "type": "Keyword", "value": "var", "range": [ 0, 3 ] }, { "type": "Identifier", "value": "answer", "range": [ 4, 10 ] }, { "type": "Punctuator", "value": "=", "range": [ 11, 12 ] }, { "type": "Numeric", "value": "2", "range": [ 13, 14 ] }, { "type": "Punctuator", "value": "*", "range": [ 15, 16 ] }, { "type": "Numeric", "value": "3", "range": [ 17, 18 ] }, { "type": "Punctuator", "value": ";", "range": [ 18, 19 ] } ]
语义分析的目的是将分词获得的语法单元进行一个总体的组合,分析肯定语法单元之间的关系。
简单来讲,语义分析能够理解成对语句(statement)和表达式(expression)的识别。
一个具有边界的代码区域。相邻的两个语句之间从语法上讲互不影响。好比:var a = 1;if(xxx){xxx}
指最终会有一个结果的一小段代码,它能够嵌入到另外一个表达式中,且包含在表达式中。好比:a++,i > 0 && i< 6
语义分析是一个递归的过程,它会将分词分析出来的数组转化成树形的表达形式。同时,会验证语法,语法若是存在错误的话,会抛出语法错误。
文章一开始就说到了,babel,webpack,css预处理,eslint等都应用到了AST树,那么AST到底作了一个什么样的角色呢!?下面咱们就来看一下。
首先看一下babel工做原理的实现。
babel是一个javascript编译器,用来将es6语法编译成es5。
babel的工做能够分为3个阶段:
经过解析器babylon将代码解析成抽象语法树。
经过 babel-traverse
plugin 对抽象语法树进行深度优先遍历,遇到须要转换的,就直接在AST对象上对节点进行添加、更新及移除操做,好比遇到箭头函数,就转换成普通函数,最后获得新的AST树。
经过 babel-generator
将AST树生成es5代码。
Vue 提供了 2 个版本,一个是 Runtime + Compiler ,另外一个是 Runtime only 的,前者是包含编译代码的,会把编译的过程放在运行时作,后者是不包含编译代码的,须要借助 webpack 的vue-loader把模板编译render函数。无论使用哪一个版本,都有一个环节,就是将模板编译成render函数。
下面咱们分析下vue模板的编译过程,这也是vue源码实现中很是重要的一个模块。
vue模板的编译过程分为3个阶段:
const ast = parse(template.trim(), options)
将模板字符串解析生成 AST,这里的解析器是vue本身实现的,解析过程当中会使用正则表达式对模板顺序解析,当解析到开始标签、闭合标签、文本的时候都会有相对应的回调函数执行,来达到构造 AST 树的目的。
生成的AST 元素节点总共有 3 种类型,1 为普通元素, 2 为表达式,3为纯文本。
下面看一个例子:
<ul :class="bindCls" class="list" v-if="isShow"> <li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li> </ul>
上面模板解析生成的AST树以下:
ast = { 'type': 1, 'tag': 'ul', 'attrsList': [], 'attrsMap': { ':class': 'bindCls', 'class': 'list', 'v-if': 'isShow' }, 'if': 'isShow', 'ifConditions': [{ 'exp': 'isShow', 'block': // ul ast element }], 'parent': undefined, 'plain': false, 'staticClass': 'list', 'classBinding': 'bindCls', 'children': [{ 'type': 1, 'tag': 'li', 'attrsList': [{ 'name': '@click', 'value': 'clickItem(index)' }], 'attrsMap': { '@click': 'clickItem(index)', 'v-for': '(item,index) in data' }, 'parent': // ul ast element 'plain': false, 'events': { 'click': { 'value': 'clickItem(index)' } }, 'hasBindings': true, 'for': 'data', 'alias': 'item', 'iterator1': 'index', 'children': [ 'type': 2, 'expression': '_s(item)+":"+_s(index)' 'text': '{{item}}:{{index}}', 'tokens': [ {'@binding':'item'}, ':', {'@binding':'index'} ] ] }] }
optimize(ast, options)
vue模板中并非全部数据都是响应式的,有不少数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,咱们能够在patch的过程跳过对他们的比对。
此阶段会深度遍历生成的 AST树,检测它的每一颗子树是否是静态节点,若是是静态节点则它们生成 DOM 永远不须要改变,这对运行时对模板的更新起到极大的优化做用。
遍历过程当中,会对整个 AST 树中的每个 AST 元素节点标记static和staticRoot(递归该节点的全部children,一旦子节点有不是static的状况,则为false,不然为true)。
通过该阶段,上面例子中的ast会变成:
ast = { 'type': 1, 'tag': 'ul', 'attrsList': [], 'attrsMap': { ':class': 'bindCls', 'class': 'list', 'v-if': 'isShow' }, 'if': 'isShow', 'ifConditions': [{ 'exp': 'isShow', 'block': // ul ast element }], 'parent': undefined, 'plain': false, 'staticClass': 'list', 'classBinding': 'bindCls', 'static': false, 'staticRoot': false, 'children': [{ 'type': 1, 'tag': 'li', 'attrsList': [{ 'name': '@click', 'value': 'clickItem(index)' }], 'attrsMap': { '@click': 'clickItem(index)', 'v-for': '(item,index) in data' }, 'parent': // ul ast element 'plain': false, 'events': { 'click': { 'value': 'clickItem(index)' } }, 'hasBindings': true, 'for': 'data', 'alias': 'item', 'iterator1': 'index', 'static': false, 'staticRoot': false, 'children': [ 'type': 2, 'expression': '_s(item)+":"+_s(index)' 'text': '{{item}}:{{index}}', 'tokens': [ {'@binding':'item'}, ':', {'@binding':'index'} ], 'static': false ] }] }
const code = generate(ast, options)
经过generate方法,将ast生成render函数:
with(this){ return (isShow) ? _c('ul', { staticClass: "list", class: bindCls }, _l((data), function(item, index) { return _c('li', { on: { "click": function($event) { clickItem(index) } } }, [_v(_s(item) + ":" + _s(index))]) }) ) : _e() }
经过上面对babel实现原理和vue模板的编译原理能够看出,他们的实现有不少相同之处,都是先将源码解析成AST树,而后对AST树就行处理,最后生成想要的东西。
Prettier的实现一样是这样,首先依然是将代码解析生成AST树,而后是对AST遍历,调整长句,整理空格,括号等,最后输出代码,这里就不赘述了。
咱们分析了Babel原理、vue模板编译过程、Prettier原理,这里咱们简单总结一下。
若是把源码比做一个机器,那么分词过程就是将这台机器拆分红一个个零件,语义分析过程就是分析每一个零件的位置以及做用,而后根据须要对零件进行加工处理,最后再组装成一个新的机器。
那么工做中咱们能使用AST作些什么呢?!
这里就要发挥想象了,看看咱们平常工做中有什么需求是能够经过AST开发个工具来解决。
好比,能够经过AST能够将代码自动转成流程图;
或者根据自定义的注释规范,经过工具自动生成文档;
或是经过工具自动生成骨架屏文件。
你还有什么好想法呢?