Angularjs 1.3 中的$parse实例代码


Posted in Javascript onSeptember 14, 2017

这次我们来看一下angular的Sandboxing Angular Expressions。关于内置方法的,核心有两块:Lexer和Parser。其中大家对$parse可能更了解一点。好了不多废话,先看Lexer的内部结构:

1.Lexer

//构造函数
var Lexer = function(options) {
 this.options = options;
};
//原型 
Lexer.prototype = {
 constructor: Lexer,
 lex: function(){},
 is: function(){},
 peek: function(){ /* 返回表达式的下一个位置的数据,如果没有则返回false */ },
 isNumber: function(){ /* 判断当前表达式是否是一个数字 */ },
 isWhitespace: function(){/* 判断当前表达式是否是空格符 */},
 isIdent: function(){/* 判断当前表达式是否是英文字符(包含_和$) */},
 isExpOperator: function(){/* 判断当时表达式是否是-,+还是数字 */},
 throwError: function(){ /* 抛出异常 */},
 readNumber: function(){ /* 读取数字 */},
 readIdent: function(){ /* 读取字符 */},
 readString: function(){ /*读取携带''或""的字符串*/ }
};

 这里指出一点,因为是表达式。所以类似"123"这类的东西,在Lexer看来应该算是数字而非字符串。表达式中的字符串必须使用单引号或者双引号来标识。Lexer的核心逻辑在lex方法中:

lex: function(text) {
 this.text = text;
 this.index = 0;
 this.tokens = [];

 while (this.index < this.text.length) {
  var ch = this.text.charAt(this.index);
  if (ch === '"' || ch === "'") {
  /* 尝试判断是否是字符串 */
  this.readString(ch);
  } else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) {
  /* 尝试判断是否是数字 */
  this.readNumber();
  } else if (this.isIdent(ch)) {
  /* 尝试判断是否是字母 */
  this.readIdent();
  } else if (this.is(ch, '(){}[].,;:?')) {
  /* 判断是否是(){}[].,;:? */
  this.tokens.push({index: this.index, text: ch});
  this.index++;
  } else if (this.isWhitespace(ch)) {
  /* 判断是否是空白符 */
  this.index++;
  } else {
  /* 尝试匹配操作运算 */
  var ch2 = ch + this.peek();
  var ch3 = ch2 + this.peek(2);
  var op1 = OPERATORS[ch];
  var op2 = OPERATORS[ch2];
  var op3 = OPERATORS[ch3];
  if (op1 || op2 || op3) {
   var token = op3 ? ch3 : (op2 ? ch2 : ch);
   this.tokens.push({index: this.index, text: token, operator: true});
   this.index += token.length;
  } else {
   this.throwError('Unexpected next character ', this.index, this.index + 1);
  }
  }
 }
 return this.tokens;
 }

主要看一下匹配操作运算。这里源码中会调用OPERATORS。看一下OPERATORS:

var OPERATORS = extend(createMap(), {
 '+':function(self, locals, a, b) {
  a=a(self, locals); b=b(self, locals);
  if (isDefined(a)) {
  if (isDefined(b)) {
   return a + b;
  }
  return a;
  }
  return isDefined(b) ? b : undefined;},
 '-':function(self, locals, a, b) {
   a=a(self, locals); b=b(self, locals);
   return (isDefined(a) ? a : 0) - (isDefined(b) ? b : 0);
  },
 '*':function(self, locals, a, b) {return a(self, locals) * b(self, locals);},
 '/':function(self, locals, a, b) {return a(self, locals) / b(self, locals);},
 '%':function(self, locals, a, b) {return a(self, locals) % b(self, locals);},
 '===':function(self, locals, a, b) {return a(self, locals) === b(self, locals);},
 '!==':function(self, locals, a, b) {return a(self, locals) !== b(self, locals);},
 '==':function(self, locals, a, b) {return a(self, locals) == b(self, locals);},
 '!=':function(self, locals, a, b) {return a(self, locals) != b(self, locals);},
 '<':function(self, locals, a, b) {return a(self, locals) < b(self, locals);},
 '>':function(self, locals, a, b) {return a(self, locals) > b(self, locals);},
 '<=':function(self, locals, a, b) {return a(self, locals) <= b(self, locals);},
 '>=':function(self, locals, a, b) {return a(self, locals) >= b(self, locals);},
 '&&':function(self, locals, a, b) {return a(self, locals) && b(self, locals);},
 '||':function(self, locals, a, b) {return a(self, locals) || b(self, locals);},
 '!':function(self, locals, a) {return !a(self, locals);},

 //Tokenized as operators but parsed as assignment/filters
 '=':true,
 '|':true
});

可以看到OPERATORS实际上存储的是操作符和操作符函数的键值对。根据操作符返回对应的操作符函数。我们看一下调用例子:

var _l = new Lexer({});
var a = _l.lex("a = a + 1");
console.log(a);

 结合之前的lex方法,我们来回顾下代码执行过程:

1.index指向'a'是一个字母。匹配isIdent成功。将生成的token存入tokens中

2.index指向空格符,匹配isWhitespace成功,同上

3.index指向=,匹配操作运算符成功,同上

4.index指向空格符,匹配isWhitespace成功,同上

5.index指向'a'是一个字母。匹配isIdent成功。同上

7.index指向+,匹配操作运算符成功,同上

8.index指向空格符,匹配isWhitespace成功,同上

9.index指向1,匹配数字成功,同上

以上则是"a = a + 1"的代码执行过程。9步执行结束之后,跳出while循环。刚才我们看到了,每次匹配成功,源码会生成一个token。因为匹配类型的不同,生成出来的token的键值对略有不同:

number:{
  index: start,
  text: number,
  constant: true,
  value: Number(number)
 },
string: {
   index: start,
   text: rawString,
   constant: true,
   value: string
  },
ident: {
  index: start,
  text: this.text.slice(start, this.index),
  identifier: true /* 字符表示 */ 
 },
'(){}[].,;:?': {
 index: this.index,
 text: ch
},
"操作符": {
  index: this.index, 
  text: token, 
  operator: true
}
//text是表达式,而value才是实际的值

number和string其实都有相对应的真实值,意味着如果我们表达式是2e2,那number生成的token的值value就应该是200。到此我们通过lexer类获得了一个具有token值得数组。从外部看,实际上Lexer是将我们输入的表达式解析成了token json。可以理解为生成了表达式的语法树(AST)。但是目前来看,我们依旧还没有能获得我们定义表达式的结果。那就需要用到parser了。

2.Parser

先看一下Parser的内部结构:

//构造函数
var Parser = function(lexer, $filter, options) {
 this.lexer = lexer;
 this.$filter = $filter;
 this.options = options;
};
//原型
Parser.prototype = {
 constructor: Parser,
 parse: function(){},
 primary: function(){},
 throwError: function(){ /* 语法抛错 */},
 peekToken: function(){},
 peek: function(){/*返回tokens中的第一个成员对象 */},
 peekAhead: function(){ /* 返回tokens中指定成员对象,否则返回false */},
 expect: function(){ /* 取出tokens中第一个对象,否则返回false */ },
 consume: function(){ /* 取出第一个,底层调用expect */ },
 unaryFn: function(){ /* 一元操作 */},
 binaryFn: function(){ /* 二元操作 */},
 identifier: function(){},
 constant: function(){},
 statements: function(){},
 filterChain: function(){},
 filter: function(){},
 expression: function(){},
 assignment: function(){},
 ternary: function(){},
 logicalOR: function(){ /* 逻辑或 */},
 logicalAND: function(){ /* 逻辑与 */ },
 equality: function(){ /* 等于 */ },
 relational: function(){ /* 比较关系 */ },
 additive: function(){ /* 加法,减法 */ },
 multiplicative: function(){ /* 乘法,除法,求余 */ },
 unary: function(){ /* 一元 */ },
 fieldAccess: function(){},
 objectIndex: function(){},
 functionCall: function(){},
 arrayDeclaration: function(){},
 object: function(){}
}

Parser的入口方法是parse,内部执行了statements方法。来看下statements:

statements: function() {
 var statements = [];
 while (true) {
  if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']'))
  statements.push(this.filterChain());
  if (!this.expect(';')) {
  // optimize for the common case where there is only one statement.
  // TODO(size): maybe we should not support multiple statements?
  return (statements.length === 1)
   ? statements[0]
   : function $parseStatements(self, locals) {
    var value;
    for (var i = 0, ii = statements.length; i < ii; i++) {
     value = statements[i](self, locals);
    }
    return value;
    };
  }
 }
 }

这里我们将tokens理解为表达式,实际上它就是经过表达式通过lexer转换过来的。statements中。如果表达式不以},),;,]开头,将会执行filterChain方法。当tokens检索完成之后,最后返回了一个$parseStatements方法。其实Parser中很多方法都返回了类似的对象,意味着返回的内容将需要执行后才能得到结果。

看一下filterChain:

filterChain: function() {
 /* 针对angular语法的filter */
 var left = this.expression();
 var token;
 while ((token = this.expect('|'))) {
  left = this.filter(left);
 }
 return left;
 }

其中filterChain是针对angular表达式独有的"|"filter写法设计的。我们先绕过这块,进入expression

expression: function() {
 return this.assignment();
 }

再看assignment:

assignment: function() {
 var left = this.ternary();
 var right;
 var token;
 if ((token = this.expect('='))) {
  if (!left.assign) {
  this.throwError('implies assignment but [' +
   this.text.substring(0, token.index) + '] can not be assigned to', token);
  }
  right = this.ternary();
  return extend(function $parseAssignment(scope, locals) {
  return left.assign(scope, right(scope, locals), locals);
  }, {
  inputs: [left, right]
  });
 }
 return left;
 }

我们看到了ternary方法。这是一个解析三目操作的方法。与此同时,assignment将表达式以=划分成left和right两块。并且两块都尝试执行ternary。

ternary: function() {
 var left = this.logicalOR();
 var middle;
 var token;
 if ((token = this.expect('?'))) {
  middle = this.assignment();
  if (this.consume(':')) {
  var right = this.assignment();
  return extend(function $parseTernary(self, locals) {
   return left(self, locals) ? middle(self, locals) : right(self, locals);
  }, {
   constant: left.constant && middle.constant && right.constant
  });
  }
 }
 return left;
 }

在解析三目运算之前,又根据?将表达式划分成left和right两块。左侧再去尝试执行logicalOR,实际上这是一个逻辑与的解析,按照这个执行流程,我们一下有了思路。这有点类似我们一般写三目时。代码的执行情况,比如: 2 > 2 ? 1 : 0。如果把这个当成表达式,那根据?划分left和right,left就应该是2 > 2,right应该就是 1: 0。然后尝试在left看是否有逻辑或的操作。也就是,Parser里面的方法调用的嵌套级数越深,其方法的优先级则越高。好,那我们一口气看看这个最高的优先级在哪?

logicalOR -> logicalAND -> equality -> relational -> additive -> multiplicative -> unary

好吧,嵌套级数确实有点多。那么我们看下unary。

unary: function() {
 var token;
 if (this.expect('+')) {
  return this.primary();
 } else if ((token = this.expect('-'))) {
  return this.binaryFn(Parser.ZERO, token.text, this.unary());
 } else if ((token = this.expect('!'))) {
  return this.unaryFn(token.text, this.unary());
 } else {
  return this.primary();
 }
 }

这边需要看两个主要的方法,一个是binaryFn和primay。如果判断是-,则必须通过binaryFn去添加函数。看下binaryFn

binaryFn: function(left, op, right, isBranching) {
 var fn = OPERATORS[op];
 return extend(function $parseBinaryFn(self, locals) {
  return fn(self, locals, left, right);
 }, {
  constant: left.constant && right.constant,
  inputs: !isBranching && [left, right]
 });
 }

其中OPERATORS是之前聊Lexer也用到过,它根据操作符存储相应的操作函数。看一下fn(self, locals, left, right)。而我们随便取OPERATORS中的一个例子:

'-':function(self, locals, a, b) {
   a=a(self, locals); b=b(self, locals);
   return (isDefined(a) ? a : 0) - (isDefined(b) ? b : 0);
  }

其中a和b就是left和right,他们其实都是返回的跟之前类似的$parseStatements方法。默认存储着token中的value。经过事先解析好的四则运算来生成最终答案。其实这就是Parser的基本功能。至于嵌套,我们可以把它理解为js的操作符的优先级。这样就一目了然了。至于primay方法。塔刷选{ ( 对象做进一步的解析过程。

Parser的代码并不复杂,只是函数方法间调用密切,让我们再看一个例子:

var _l = new Lexer({});
var _p = new Parser(_l);
var a = _p.parse("1 + 1 + 2");
console.log(a()); //4

我们看下1+1+2生成的token是什么样的:

[
{"index":0,"text":"1","constant":true,"value":1},{"index":2,"text":"+","operator":true},{"index":4,"text":"1","constant":true,"value":1},{"index":6,"text":"+","operator":true},{"index":8,"text":"2","constant":true,"value":2}
]

Parser根据lexer生成的tokens尝试解析。tokens每一个成员都会生成一个函数,其先后执行逻辑按照用户输入的1+1+2的顺序执行。注意像1和2这类constants为true的token,parser会通过constant生成需要的函数$parseConstant,也就是说1+1+2中的两个1和一个2都是返回$parseConstant函数,通过$parseBinaryFn管理加法逻辑。

constant: function() {
 var value = this.consume().value;

 return extend(function $parseConstant() {
  return value; //这个函数执行之后,就是将value值返回。
 }, {
  constant: true,
  literal: true
 });
 },
binaryFn: function(left, op, right, isBranching) {
 var fn = OPERATORS[op];//加法逻辑
 return extend(function $parseBinaryFn(self, locals) {
  return fn(self, locals, left, right);//left和right分别表示生成的对应函数
 }, {
  constant: left.constant && right.constant,
  inputs: !isBranching && [left, right]
 });
 }

那我们demo中的a应该返回什么函数呢?当然是$parseBinaryFn。其中的left和right分别是1+1的$parseBinaryFn,right就是2的$parseConstant。

再来一个例子:

var _l = new Lexer({});
var _p = new Parser(_l);
var a = _p.parse('{"name": "hello"}');
console.log(a);

这边我们传入一个json,理论上我们执行完a函数,应该返回一个{name: "hello"}的对象。它调用了Parser中的object

object: function() {
 var keys = [], valueFns = [];
 if (this.peekToken().text !== '}') {
  do {
  if (this.peek('}')) {
   // Support trailing commas per ES5.1.
   break;
  }
  var token = this.consume();
  if (token.constant) {
   //把key取出来
   keys.push(token.value);
  } else if (token.identifier) {
   keys.push(token.text);
  } else {
   this.throwError("invalid key", token);
  }
  this.consume(':');
  //冒号之后,则是值,将值存在valueFns中
  valueFns.push(this.expression());
  //根据逗号去迭代下一个
  } while (this.expect(','));
 }
 this.consume('}');
 return extend(function $parseObjectLiteral(self, locals) {
  var object = {};
  for (var i = 0, ii = valueFns.length; i < ii; i++) {
  object[keys[i]] = valueFns[i](self, locals);
  }
  return object;
 }, {
  literal: true,
  constant: valueFns.every(isConstant),
  inputs: valueFns
 });
 }

比方我们的例子{"name": "hello"},object会将name存在keys中,hello则会生成$parseConstant函数存在valueFns中,最终返回$parseObjectLiternal函数。

下一个例子:

var a = _p.parse('{"name": "hello"}["name"]');

这个跟上一个例子的差别在于后面尝试去读取name的值,这边则调用parser中的objectIndex方法。

objectIndex: function(obj) {
 var expression = this.text;
 var indexFn = this.expression();
 this.consume(']');
 return extend(function $parseObjectIndex(self, locals) {
  var o = obj(self, locals), //parseObjectLiteral,实际就是obj
   i = indexFn(self, locals), //$parseConstant,这里就是name
   v;
  ensureSafeMemberName(i, expression);
  if (!o) return undefined;
  v = ensureSafeObject(o[i], expression);
  return v;
 }, {
  assign: function(self, value, locals) {
  var key = ensureSafeMemberName(indexFn(self, locals), expression);
  // prevent overwriting of Function.constructor which would break ensureSafeObject check
  var o = ensureSafeObject(obj(self, locals), expression);
  if (!o) obj.assign(self, o = {}, locals);
  return o[key] = value;
  }
 });
 }

很简单吧,obj[xx]和obj.x类似。大家自行阅读,我们再看一个函数调用的demo

var _l = new Lexer({});
var _p = new Parser(_l, '', {});
var demo = {
 "test": function(){
 alert("welcome");
 }
};
var a = _p.parse('test()');
console.log(a(demo));

我们传入一个test的调用。这边调用了parser中的functionCall方法和identifier方法

identifier: function() {
 var id = this.consume().text;
 //Continue reading each `.identifier` unless it is a method invocation
 while (this.peek('.') && this.peekAhead(1).identifier && !this.peekAhead(2, '(')) {
  id += this.consume().text + this.consume().text;
 }
 return getterFn(id, this.options, this.text);
 }

看一下getterFn方法

...
forEach(pathKeys, function(key, index) {
  ensureSafeMemberName(key, fullExp);
  var lookupJs = (index
      // we simply dereference 's' on any .dot notation
      ? 's'
      // but if we are first then we check locals first, and if so read it first
      : '((l&&l.hasOwnProperty("' + key + '"))?l:s)') + '.' + key;
  if (expensiveChecks || isPossiblyDangerousMemberName(key)) {
  lookupJs = 'eso(' + lookupJs + ', fe)';
  needsEnsureSafeObject = true;
  }
  code += 'if(s == null) return undefined;\n' +
    's=' + lookupJs + ';\n';
 });
 code += 'return s;';
 /* jshint -W054 */
 var evaledFnGetter = new Function('s', 'l', 'eso', 'fe', code); // s=scope, l=locals, eso=ensureSafeObject
 /* jshint +W054 */
 evaledFnGetter.toString = valueFn(code);
...

这是通过字符串创建一个匿名函数的方法。我们看下demo的test生成了一个什么匿名函数:

function('s', 'l', 'eso', 'fe'){
if(s == null) return undefined;
s=((l&&l.hasOwnProperty("test"))?l:s).test;
return s;
}

这个匿名函数的意思,需要传入一个上下文,匿名函数通过查找上下文中是否有test属性,如果没有传上下文则直接返回未定义。这也就是为什么我们在生成好的a函数在执行它时需要传入demo对象的原因。最后补一个functionCall

functionCall: function(fnGetter, contextGetter) {
 var argsFn = [];
 if (this.peekToken().text !== ')') {
  /* 确认调用时有入参 */
  do {
  //形参存入argsFn
  argsFn.push(this.expression());
  } while (this.expect(','));
 }
 this.consume(')');
 var expressionText = this.text;
 // we can safely reuse the array across invocations
 var args = argsFn.length ? [] : null;
 return function $parseFunctionCall(scope, locals) {
  var context = contextGetter ? contextGetter(scope, locals) : isDefined(contextGetter) ? undefined : scope;
  //或者之前创建生成的匿名函数
  var fn = fnGetter(scope, locals, context) || noop;
  if (args) {
  var i = argsFn.length;
  while (i--) {
   args[i] = ensureSafeObject(argsFn[i](scope, locals), expressionText);
  }
  }
  ensureSafeObject(context, expressionText);
  ensureSafeFunction(fn, expressionText);
  // IE doesn't have apply for some native functions
  //执行匿名函数的时候需要传入上下文
  var v = fn.apply
   ? fn.apply(context, args)
   : fn(args[0], args[1], args[2], args[3], args[4]);
  if (args) {
  // Free-up the memory (arguments of the last function call).
  args.length = 0;
  }
  return ensureSafeObject(v, expressionText);
  };
 }

下面我们看一下$ParseProvider,这是一个基于Lex和Parser函数的angular内置provider。它对scope的api提供了基础支持。

...
return function $parse(exp, interceptorFn, expensiveChecks) {
  var parsedExpression, oneTime, cacheKey;
  switch (typeof exp) {
  case 'string':
   cacheKey = exp = exp.trim();
   var cache = (expensiveChecks ? cacheExpensive : cacheDefault);
   parsedExpression = cache[cacheKey];
   if (!parsedExpression) {
   if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
    oneTime = true;
    exp = exp.substring(2);
   }
   var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions;
   //调用lexer和parser
   var lexer = new Lexer(parseOptions);
   var parser = new Parser(lexer, $filter, parseOptions);
   parsedExpression = parser.parse(exp);
   //添加$$watchDelegate,为scope部分提供支持
   if (parsedExpression.constant) {
    parsedExpression.$$watchDelegate = constantWatchDelegate;
   } else if (oneTime) {
    //oneTime is not part of the exp passed to the Parser so we may have to
    //wrap the parsedExpression before adding a $$watchDelegate
    parsedExpression = wrapSharedExpression(parsedExpression);
    parsedExpression.$$watchDelegate = parsedExpression.literal ?
    oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
   } else if (parsedExpression.inputs) {
    parsedExpression.$$watchDelegate = inputsWatchDelegate;
   }
   //做相关缓存
   cache[cacheKey] = parsedExpression;
   }
   return addInterceptor(parsedExpression, interceptorFn);
  case 'function':
   return addInterceptor(exp, interceptorFn);
  default:
   return addInterceptor(noop, interceptorFn);
  }
 };

总结:Lexer和Parser的实现确实让我大开眼界。通过这两个函数,实现了angular自己的语法解析器。逻辑部分还是相对复杂

以上所述是小编给大家介绍的Angularjs 1.3 中的$parse实例代码,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
用一段js程序来实现动画功能
Mar 06 Javascript
JS解密入门之凭直觉解
Jun 25 Javascript
js Event对象的5种坐标
Sep 12 Javascript
js异常捕获方法介绍
Apr 10 Javascript
javascript实现简单的省市区三级联动
May 14 Javascript
跟我学习javascript的var预解析与函数声明提升
Nov 16 Javascript
浅谈JS之tagNaem和nodeName
Sep 13 Javascript
javascript判断元素存在和判断元素存在于实时的dom中的方法
Jan 17 Javascript
angularjs+bootstrap菜单的使用示例代码
Mar 07 Javascript
Javascript前端经典的面试题及答案
Mar 14 Javascript
angularjs实现上拉加载和下拉刷新数据功能
Jun 12 Javascript
Vue作用域插槽slot-scope实例代码
Sep 05 Javascript
浅谈JavaScript中的属性:如何遍历属性
Sep 14 #Javascript
基于node.js express mvc轻量级框架实践
Sep 14 #Javascript
gulp教程_从入门到项目中快速上手使用方法
Sep 14 #Javascript
利用jsonp与代理服务器方案解决跨域问题
Sep 14 #Javascript
基于js中document.cookie全面解析
Sep 14 #Javascript
基于Vue过渡状态实例讲解
Sep 14 #Javascript
JavaScript使用atan2来绘制箭头和曲线的实例
Sep 14 #Javascript
You might like
收音机史话 - 1960年代前后的DIY
2021/03/02 无线电
php面向对象全攻略 (十) final static const关键字的使用
2009/09/30 PHP
php 传值赋值与引用赋值的区别
2010/12/29 PHP
PHP的Yii框架中Model模型的学习教程
2016/03/29 PHP
yii2.0框架场景的简单使用示例
2020/01/25 PHP
超清晰的document对象详解
2007/02/27 Javascript
推荐dojo学习笔记
2007/03/24 Javascript
JavaScript与DropDownList 区别分析
2010/01/01 Javascript
jQuery 网易相册鼠标移动显示隐藏效果实现代码
2013/03/31 Javascript
JS实现的3D拖拽翻页效果代码
2015/10/31 Javascript
javascript产生随机数方法汇总
2016/01/25 Javascript
jQuery处理XML文件的几种方法
2016/06/14 Javascript
js Canvas绘制圆形时钟教程
2017/02/06 Javascript
Vue中保存用户登录状态实例代码
2017/06/07 Javascript
React-native桥接Android原生开发详解
2018/01/17 Javascript
vue基础之事件简写、事件对象、冒泡、默认行为、键盘事件实例分析
2019/03/11 Javascript
Node.js实现用户评论社区功能(体验前后端开发的乐趣)
2019/05/09 Javascript
JS字符串与二进制的相互转化实例代码详解
2019/06/28 Javascript
javascript实现弹出层效果
2019/12/10 Javascript
Vue实现随机验证码功能
2020/12/29 Vue.js
[51:15]2014 DOTA2国际邀请赛中国区预选赛 Orenda VS LGD-GAMING
2014/05/22 DOTA
[54:27]TNC vs Serenity 2018国际邀请赛小组赛BO2 第一场 8.18
2018/08/19 DOTA
Python的字典和列表的使用中一些需要注意的地方
2015/04/24 Python
详解Python中open()函数指定文件打开方式的用法
2016/06/04 Python
python基于itchat实现微信群消息同步机器人
2017/02/27 Python
Python实现E-Mail收集插件实例教程
2019/02/06 Python
使用python实现多维数据降维操作
2020/02/24 Python
美国最大网上鞋店:Zappos
2016/07/25 全球购物
阳光体育活动方案
2014/02/16 职场文书
买卖协议书范本
2014/04/21 职场文书
2014党员自我评议表范文
2014/09/20 职场文书
政府四风问题整改措施
2014/10/04 职场文书
分布式锁为什么要选择Zookeeper而不是Redis?看完这篇你就明白了
2021/05/21 Redis
SpringBoot工程下使用OpenFeign的坑及解决
2021/07/02 Java/Android
解决MySQL添加新用户-ERROR 1045 (28000)的问题
2022/03/03 MySQL
mysql sql常用语句大全
2022/06/21 MySQL