一个表达式parse处理模块 [angular.js]

如何实现一个具有表达式解析功能的功能模块?

解析一个表达式的目的:最基本的是能获取表达式的值;对后端获取数据的简单处理;具备一定的逻辑处理能力;

最简单的实现方法:

function parse (expr) {
  return function(scope) {
     with (scope) {
        return eval(expr);  
        }           
    }  
}

parse函数返回一个函数,执行返回的函数并传入作用域即可在传入的作用域中,计算出表达式的值。

当然,这种做法简单粗暴。with和eval在严格模式下往往不可用。

一个具有解析表达式能力的模块,其输入当然是字符串,其输出则是一个可执行函数,执行该函数即可获取到表达式的值。

表达式的常见种类如下:

主要包括:字面量;函数调用;运算符;声明;成员获取;

 

下面是一种在angular 1.x中被用作表达式处理的方法:

新写一个词法解析语言。

主要包括三个部分:词法分析Lexer;AST(Abstract Syntax Tree)构建;AST编译;

如:输入"a + b"; 
1.先经过Lexer处理,输出tokens:
   [
        {text: 'a', identifier: true},
        {text: '+'},
        {text: 'b', identifier: true}
    ]    
2.tokens输入到AST build中,输出AST
   {
      type: AST.BinaryExpression,
      operator: '+',
      left: {
           type: AST.Identifier,
           name: 'a'
        },
      right: {
           type: AST.Identifier,
           name: 'b'
        }
   } 
3.AST输入到AST compiler中,返回一个可执行函数
   function(scope) {
      return scope.a + scope.b
    }           
  •  Lexer

Lexer直接面对字符串text,输出tokens数组。具体地,通过Lexer.prototype.lex原型函数来实现

由于需要对字符串的每个字符进行处理,因此需要进行循环操作。

第一个循环:根据输入内容,确定采用何种方法进行处理;

针对常见的表达式类型:依次确定如下几种类型的输入,并给予相应的处理。

1)数字  readNumber();

2)字符串 readString();

3)标识符 readIdent();

4)空白字符 index++;

5)属性获取'[]{}:()' just push到tokens中;

6)以上都不是 throw err;

具体代码如下:

while (this.index < this.text.length) {
    this.ch = this.text.charAt(this.index);
    if (this.isNumber(this.ch) ||
         (this.is('.') && this.isNumber(this.peek()))) {
      this.readNumber();
    } else if (this.is('\'"')) {
      this.readString(this.ch);
    } else if (this.is('[],{}:.()?;')) {
      this.tokens.push({
        text: this.ch
      });
      this.index++;
    } else if (this.isIdent(this.ch)) {
      this.readIdent();
    } else if (this.isWhitespace(this.ch)) {
      this.index++;
     } else {
        throw 'Unexpected next character: ' + this.ch;
      }
    }
  }

 

 第二个循环:进入处理函数内部,依次遍历每个字符。

readNumer: 返回tokens由text和value组成。

Lexer.prototype.readNumber = function() {
  var number = '';
  while (this.index < this.text.length) {
  //  
  number = 1 * number;
  this.tokens.push({
    text: number,
    value: Number(number)
  });
};

 

 readString:返回tokens由text和value组成

Lexer.prototype.readString = function(quote) {
  this.index++;
  var string = '';
  while (this.index < this.text.length) {
    //
    this.index++;
    this.tokens.push({
      text: rawString,
      value: string
     });
     //
  }
};

 

 readIdent: 返回tokens由text和identifier标识组成

Lexer.prototype.readIdent = function() {
  var text = '';
  while (this.index < this.text.length) {
  //
  var token = {
    text: text,
    identifier: true
  };
  this.tokens.push(token);
};

 

  •  AST Builder

输入的是构造函数Lexer的实例,执行实例的原型方法lex,获取返回值tokens。加工tokens获取AST

AST为抽象语法树。其作用在于牢牢抓住表达式的构造,从而方便编译过程的后续环节对表达式进行解读。例如,React中的虚拟DOM的实现,实际上也有AST的构造过程。准确的AST构建,是正确编译(如生成实际dom树)的前提。

根据表达式的分类。生成的AST树具有以下几种节点类型。

AST.Program = 'Program';  //根节点
AST.Literal = 'Literal';  //字面量节点
AST.ArrayExpression = 'ArrayExpression';  //数组
AST.ObjectExpression = 'ObjectExpression';  //对象
AST.Property = 'Property';  //属性
AST.Identifier = 'Identifier';  //标识符
AST.ThisExpression = 'ThisExpression'; // this
AST.MemberExpression = 'MemberExpression'; //成员表达式
AST.CallExpression = 'CallExpression'; //函数调用
AST.AssignmentExpression = 'AssignmentExpression';  //赋值表达式
AST.UnaryExpression = 'UnaryExpression';  //一元表达式
AST.BinaryExpression = 'BinaryExpression';  //二元表达式
AST.LogicalExpression = 'LogicalExpression;  //逻辑表达式

 

 根据输入的tokens数组,需要进行类似pipe的处理。这里的pipe处理实际上处理了表达式中的操作符优先级问题

1.  AST.prototype.program 

构建AST的根节点;返回

{
  type: AST.Program,
  body: body
}

2.  AST.prototype.assignment

处理赋值表达式:返回

{
    type: AST.AssignmentExpression, 
    left: left, 
    right: right
 }

 

 3.  AST.prototype.logicalOR

处理逻辑或:返回

{
      type: AST.LogicalExpression, 
      operator: token.text,
      left: left, 
      right: this.logicalAND()
    }

 

4.  AST.prototype.logicalAND

处理逻辑与:返回

{
      type: AST.LogicalExpression, 
      operator: token.text,
      left: left, 
      right: this.equality()
    }

 

5.  AST.prototype.equality

处理比较表达式('==','!=','===','!=='):返回

{
      type: AST.BinaryExpression, 
      operator: token.text,
      left: left, 
      right: this.relational()
    };

 

6.  AST.prototype.relational

处理关系表达式('<','>','<=','>='):返回

{
      type: AST.BinaryExpression, 
      operator: token.text,
      left: left, 
      right: this.additive()
    };

 

7.  AST.prototype.additive

处理加减操作:返回

{
      type: AST.BinaryExpression,
      operator: token.text,
      left: left,
      right: this.multiplicative()
    }

 

8.  AST.prototype.multiplicative

处理乘除操作: 返回

{
      type: AST.BinaryExpression,
      operator: token.text,
      left: left,
      right: this.unary()
    }

 

9. AST.prototype.unary

处理一元操作符:返回

{
      type: AST.UnaryExpression,
      operator: token.text,
      argument: this.unary()
    }

 

此时存在递归调用unary,当边界条件满足时,其最终返回this.primary()

10.   AST.prototype.primary

核心处理函数:

  if (this.expect('[')) {
    primary = this.arrayDeclaration();
  } else if (this.expect('{')) {
    primary = this.object();
  } else if (this.constants.hasOwnProperty(this.tokens[0].text)) {
    primary = this.constants[this.consume().text];
  } else if (this.peek().identifier){
    primary = this.identifier();
  } else{
    primary = this.constant();
  }

 其中首先需说明的是从第2步执行到第10步,最核心的工具方法,expect函数。其用来判断当前tokens的数组项是否是期望的内容,并且shift移除这个tokens项。以实现在pipe中依次过滤处理的效果。

AST.prototype.expect = function(e1, e2, e3, e4) {
  var token = this.peek(e1, e2, e3, e4);
  if (token) {
    //注意shift的使用
    return this.tokens.shift();
  }
};
AST.prototype.peek = function(e1, e2, e3, e4) {
  if (this.tokens.length > 0) {
    var text = this.tokens[0].text;
    if (text === e1 ||text === e2 ||text === e3 ||text === e4 || (!e1 && !e2 && !e3 && !e4)) {
      return this.tokens[0];
    }
  }
};

 接着primary函数的处理逻辑,首先判断

1)AST.ArrayExpression 数组类型

-->this.arrayDeclaration()  返回

AST.prototype.arrayDeclaration = function() {
  var elements = [];
  if (!this.peek(']')) {
    do {
      if (this.peek(']')) {
        break;
      }
      //对每个数组元素都需递归进行this.primary()处理。
      elements.push(this.assignment());
    } while (this.expect(','));
  }
  this.consume(']');
  return {type: AST.ArrayExpression, elements: elements};
};

  

2) AST.ObjectExpression 对象类型

--> this.object() 返回

AST.prototype.object = function() {
  var properties = [];
  if (!this.peek('}')) {
    do {
      var property = {type: AST.Property};
      //key处理
      if (this.peek().identifier) {
        property.key = this.identifier();
      } else {
        property.key = this.constant();
      }
      this.consume(':');
      //value的primary处理
      property.value = this.assignment();
      properties.push(property);
    } while (this.expect(','));
  }
  this.consume('}');
  return {type: AST.ObjectExpression, properties: properties};
};

 

 3)AST.Identifier和AST.Literal

标识符和字面量的处理

{type: AST.Identifier, name: this.consume().text}
{type: AST.Literal, value: this.consume().value}

 

primary中,除了上述三种以外,仍有key.value;key['value'];key();三种类型的表达式需要处理。

具体地构建两种类型的节点:AST.MemberExpression;AST.CallExpression。构建过程需递归处理key和value

while((next = this.expect('.','[','('))){
    if(next.text === '['){
      primary = {
        type: AST.MemberExpression,
        object: primary,
        //注意[]内的量需要重新primary确定
        property: this.primary(),
        computed: true
      };
      this.consume(']');
    } else if(next.text === '.'){
      primary = {
        type: AST.MemberExpression,
        object: primary,
        //.的时候只能采用identifier,无需再primary
        property: this.identifier(),
        computed: false
      };
    } else if(next.text === '('){
      primary = {
        type: AST.CallExpression,
        callee: primary,
        arguments: this.parseArguments() //函数形参
      }
      this.consume(')');
    }
  }

 

  •  AST Compiler

AST Compiler编译函数,关键是对AST树进行编译,即进行递归处理。核心函数compile返回的是new Function() 

返回:

new Function(*,*,fnString)(*,*)

 

其中,fnString作为新返回函数的函数体,其构成实际上即为一组解析后的字符串。

函数体内部定义var fn;并且将其返回。

var fnString = 'var fn=function(s,l){' +
    (this.state.vars.length ?
      'var ' + this.state.vars.join(',') + ';' :
      ''
    ) + this.state.body.join('') + '}; return fn;';

 

关键是this.state.body.join(''),编译出的内容均存放在state.body中。

编译函数为:ASTCompiler.prototype.recurse

根据AST树种的type类型进行依次处理

ASTCompiler.prototype.recurse = function(ast, context, create) {
  var intoId;
  switch (ast.type) {
  case AST.Program:
  //
 case AST.Literal:
//
case AST.ArrayExpression: //case AST.ObjectExpression: //case AST.Identifier: //case AST.ThisExpression: //case AST.CallExpression: //case AST.AssignmentExpression: //case AST.UnaryExpression: //case AST.LogicalExpression: // };

1)AST.Program

根节点:作用是分派递归recurse处理state的body。

2)AST.Literal

字面量:如果是字符串,返回的是'\'' + value + '\'',否则返回的是其value值。

3)AST.ArrayExpression

数组:递归rescure处理数组的每一项,返回

'[' + elements.join(',') + ']';

 

4) AST.ObjectExpression

对象:递归rescure处理对象的每一个value,返回

var properties = _.map(ast.properties, _.bind(function(property) {
      var key = property.key.type === AST.Identifier ?
        property.key.name :
        this.escape(property.key.value);
      var value = this.recurse(property.value);
      return key + ':' + value;
    }, this));
return '{' + properties.join(',') + '}';

 

5) AST.Identifier

标识符:判断标识符是否在'l'或者‘s’作用域上,如果有返回获取标识符值得字符串标识。

this.if_(this.getHasOwnProperty('l', ast.name),
      this.assign(intoId, this.nonComputedMember('l', ast.name)));
ASTCompiler.prototype.getHasOwnProperty = function(object, property) {
  return object + '&&(' + this.escape(property) + ' in ' + object + ')';
};
ASTCompiler.prototype.nonComputedMember = function(left, right) {
  return '(' + left + ').' + right;  //返回的标识符值为类似 '(scope).key'
};

 6)AST.ThisExpression

this对象。由返回的var fn = function(s)中的s来实现,简单的

return 's';

 

即可。

7) AST.MemberExpression

成员表达式。

根据表达式的类型:key.value还是key[value],存在两种类型的处理方法。一种是上文提到的nonComputedMember。另一种是需要计算的。即:

return '(' + left + ')[' + right + ']';

 

其中,left直接进行rescure递归操作,而right则根据是否需要计算,来确定是否需要rescure操作。 

 8) AST.CallExpression

函数调用:重新构造一下arguments,返回:

return callee + '&&' + callee + '(' + args.join(',') + ')';

 

 9) AST.AssignmentExpression

赋值表达式

return this.assign(leftExpr,
      '' + this.recurse(ast.right));

 

 其中,assign函数为赋值表达式,本质上返回的即是一个字符串形式的赋值语句。

10) AST.UnaryExpression、AST.BinaryExpression、AST.LogicalExpression

操作符表达式;本质上即使将对应的操作符与相应的rescure后的内容进行组合:

  case AST.UnaryExpression:
    return ast.operator +
      '(' + this.recurse(ast.argument) + ')';
  case AST.BinaryExpression:
    if (ast.operator === '+' || ast.operator === '-') {
      return '(' + this.recurse(ast.left) + ')' +
        ast.operator +
        '(' + this.recurse(ast.right) + ')';
    } else {
      return '(' + this.recurse(ast.left) + ')' +
        ast.operator +
        '(' + this.recurse(ast.right) + ')';
    }
    break;
  case AST.LogicalExpression:
    intoId = this.nextId();
    this.state.body.push(this.assign(intoId, this.recurse(ast.left)));
    this.if_(ast.operator === '&&' ? intoId : this.not(intoId),
      this.assign(intoId, this.recurse(ast.right)));
    return intoId;

 

 Parser

上述三个函数,最终通过Parser构造函数融合在一起

function Parser(lexer) {
  this.lexer = lexer;
  this.ast = new AST(this.lexer);
  this.astCompiler = new ASTCompiler(this.ast);
}

Parser.prototype.parse = function(text) {
  return this.astCompiler.compile(text);
};


function parse(expr) {
  var lexer = new Lexer();
  var parser = new Parser(lexer);
  return parser.parse(expr);
}

 

 采用流程图的形式表示上述内容如下:

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2017-02-03 18:41  大球和二憨  阅读(640)  评论(0编辑  收藏  举报