This time we take a look at angular's Sandboxing Angular Expressions. Regarding the built-in method, there are two core parts: Lexer and Parser. Among them, you may know a little more about $parse. OK, I won’t talk much nonsense, let’s first look at the internal structure of Lexer:
//Constructorvar Lexer = function(options) { = options; }; //prototype = { constructor: Lexer, lex: function(){}, is: function(){}, peek: function(){ /* Returns the data at the next position of the expression, if not, returns false */ }, isNumber: function(){ /* Determine whether the current expression is a number */ }, isWhitespace: function(){/* Determine whether the current expression is a space character */}, isIdent: function(){/* Determine whether the current expression is an English character (including _ and $) */}, isExpOperator: function(){/* Determine whether the expression is -, + or a number at that time */}, throwError: function(){ /* throw an exception */}, readNumber: function(){ /* Read numbers */}, readIdent: function(){ /* Read characters */}, readString: function(){ /*Read the string carrying '' or ""*/ } };
Let me point it out here because it is an expression. So things like "123" should be considered numbers rather than strings in Lexer's opinion. Strings in expressions must be identified with single or double quotes. The core logic of Lexer is in the lex method:
lex: function(text) { = text; = 0; = []; while ( < ) { var ch = (); if (ch === '"' || ch === "'") { /* Try to determine whether it is a string */ (ch); } else if ((ch) || ch === '.' && (())) { /* Try to determine whether it is a number */ (); } else if ((ch)) { /* Try to determine whether it is a letter */ (); } else if ((ch, '(){}[].,;:?')) { /* Determine whether it is (){}[].,;:? */ ({index: , text: ch}); ++; } else if ((ch)) { /* Determine whether it is a blank sign */ ++; } else { /* Try matching operation */ var ch2 = ch + (); var ch3 = ch2 + (2); var op1 = OPERATORS[ch]; var op2 = OPERATORS[ch2]; var op3 = OPERATORS[ch3]; if (op1 || op2 || op3) { var token = op3 ? ch3 : (op2 ? ch2 : ch); ({index: , text: token, operator: true}); += ; } else { ('Unexpected next character ', , + 1); } } } return ; }
Let’s take a look at the matching operation. OPERATORS will be called in the source code here. Take a look at 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 });
You can see that OPERATORS actually stores key-value pairs of operators and operator functions. Return the corresponding operator function according to the operator. Let's take a look at the call example:
var _l = new Lexer({}); var a = _l.lex("a = a + 1"); (a);
Combining the previous lex method, let’s review the code execution process:
Pointing to 'a' is a letter. Matching isIdent succeeds. Save the generated tokens into tokens
Point to the space character, match isWhitespace successfully, same as above
Point to =, match the operation operator successfully, same as above
Point to the space character, match isWhitespace successfully, same as above
Pointing to 'a' is a letter. Matching isIdent succeeds. Same as above
Point to +, match the operation operator successfully, same as above
Point to the space character, match isWhitespace successfully, same as above
Point to 1, match the number successfully, same as above
The above is the code execution process of "a = a + 1". After the 9-step execution is completed, the while loop breaks out. We just saw that every time the match is successful, the source code will generate a token. Because of the different matching types, the key-value pairs of generated tokens are slightly different:
number:{ index: start, text: number, constant: true, value: Number(number) }, string: { index: start, text: rawString, constant: true, value: string }, ident: { index: start, text: (start, ), identifier: true /* Character representation */ }, '(){}[].,;:?': { index: , text: ch }, "Operator": { index: , text: token, operator: true } //textIt's an expression,andvalueIt's the actual value
number and string actually have corresponding real values, meaning that if our expression is 2e2, the value of the token generated by number should be 200. At this point, we obtain an array with token value through the lexer class. From the outside, Lexer actually parses the expression we input into token json. It can be understood as a syntax tree (AST) that generates expressions. But at present, we still have no results in defining the expression. Then you need to use parser.
Let’s take a look at the internal structure of Parser:
//Constructorvar Parser = function(lexer, $filter, options) { = lexer; this.$filter = $filter; = options; }; //prototype = { constructor: Parser, parse: function(){}, primary: function(){}, throwError: function(){ /* Syntax error throwing */}, peekToken: function(){}, peek: function(){/*Return the first member object in tokens */}, peekAhead: function(){ /* Returns the specified member object in tokens, otherwise returns false */}, expect: function(){ /* Take out the first object in tokens, otherwise return false */ }, consume: function(){ /* Take out the first one, and call expect */ }, unaryFn: function(){ /* One-way operation */}, binaryFn: function(){ /* Binary operation */}, identifier: function(){}, constant: function(){}, statements: function(){}, filterChain: function(){}, filter: function(){}, expression: function(){}, assignment: function(){}, ternary: function(){}, logicalOR: function(){ /* Logical or */}, logicalAND: function(){ /* Logic and */ }, equality: function(){ /* equals */ }, relational: function(){ /* Comparative relationship */ }, additive: function(){ /* Addition, subtraction */ }, multiplicative: function(){ /* Multiplication, division, find the remainder */ }, unary: function(){ /* One dollar */ }, fieldAccess: function(){}, objectIndex: function(){}, functionCall: function(){}, arrayDeclaration: function(){}, object: function(){} }
The entry method of Parser is parse, and the statements method is executed internally. Let’s take a look at the statements:
statements: function() { var statements = []; while (true) { if ( > 0 && !('}', ')', ';', ']')) (()); if (!(';')) { // optimize for the common case where there is only one statement. // TODO(size): maybe we should not support multiple statements? return ( === 1) ? statements[0] : function $parseStatements(self, locals) { var value; for (var i = 0, ii = ; i < ii; i++) { value = statements[i](self, locals); } return value; }; } } }
Here we understand tokens as an expression, but in fact it is converted through lexer through expressions. statements. If the expression does not start with },),;,], the filterChain method will be executed. After the tokens search is completed, a $parseStatements method is finally returned. In fact, many methods in Parser return similar objects, which means that the returned content will need to be executed before the result can be obtained.
Take a look at filterChain:
filterChain: function() { /* filter for angular syntax */ var left = (); var token; while ((token = ('|'))) { left = (left); } return left; }
Among them, filterChain is designed for the "|" filter writing method unique to angular expressions. Let's go around this first and enter expression
expression: function() { return (); }
Let's look at assignment:
assignment: function() { var left = (); var right; var token; if ((token = ('='))) { if (!) { ('implies assignment but [' + (0, ) + '] can not be assigned to', token); } right = (); return extend(function $parseAssignment(scope, locals) { return (scope, right(scope, locals), locals); }, { inputs: [left, right] }); } return left; }
We see the ternary method. This is a method to analyze three-point operations. At the same time, assignment divides the expression into two pieces with =: left and right. And both pieces try to execute ternary.
ternary: function() { var left = (); var middle; var token; if ((token = ('?'))) { middle = (); if ((':')) { var right = (); return extend(function $parseTernary(self, locals) { return left(self, locals) ? middle(self, locals) : right(self, locals); }, { constant: && && }); } } return left; }
Before analyzing the three-item operation, the expression is divided into two pieces according to the ?. On the left side, try to execute logicalOR. In fact, this is a logical analysis. According to this execution process, we have an idea. This is a bit similar to when we usually write three-points. Code execution status, for example: 2 > 2 ? 1 : 0. If you treat this as an expression, then divide left and right according to?, left should be 2 > 2, and right should be 1:0. Then try to see if there is any logic or operation in left. That is, the deeper the nested series of methods called in Parser, the higher the priority of the method. OK, then let’s take a look at the highest priority in one go?
logicalOR -> logicalAND -> equality -> relational -> additive -> multiplicative -> unary
Well, there are a bit more nested series. Then let's take a look at unary.
unary: function() { var token; if (('+')) { return (); } else if ((token = ('-'))) { return (, , ()); } else if ((token = ('!'))) { return (, ()); } else { return (); } }
There are two main methods that need to be seen here, one is binaryFn and primay. If it is judged that it is -, the function must be added through binaryFn. Check out binaryFn
binaryFn: function(left, op, right, isBranching) { var fn = OPERATORS[op]; return extend(function $parseBinaryFn(self, locals) { return fn(self, locals, left, right); }, { constant: && , inputs: !isBranching && [left, right] }); }
Among them, OPERATORS is also used by Lexer before, which stores the corresponding operation functions according to the operator. Take a look at fn(self, locals, left, right). And we just take an example from OPERATORS:
'-':function(self, locals, a, b) { a=a(self, locals); b=b(self, locals); return (isDefined(a) ? a : 0) - (isDefined(b) ? b : 0); }
Among them, a and b are left and right. They actually return $parseStatements methods similar to those before. The value in the token is stored by default. The final answer is generated through four operations parsed in advance. In fact, this is the basic function of Parser. As for nesting, we can understand it as the priority of the operator of js. This makes it clear at a glance. As for the primay method. Tower brush selection { (object for further analysis.
Parser's code is not complicated, but the function method calls are closely related. Let's look at another example:
var _l = new Lexer({}); var _p = new Parser(_l); var a = _p.parse("1 + 1 + 2"); (a()); //4
Let's take a look at what the token generated by 1+1+2 looks like:
[ {"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 tries to parse based on tokens generated by lexer. Each member of tokens will generate a function, and its execution logic will be executed in the order of 1+1+2 input by the user. Note that tokens with true constants like 1 and 2, parser will generate the required function $parseConstant through constant, that is, the two 1 and one 2 in 1+1+2 return the $parseConstant function, and manage the addition logic through $parseBinaryFn.
constant: function() { var value = ().value; return extend(function $parseConstant() { return value; //After this function is executed, the value value is returned. }, { constant: true, literal: true }); }, binaryFn: function(left, op, right, isBranching) { var fn = OPERATORS[op];//Addition logic return extend(function $parseBinaryFn(self, locals) { return fn(self, locals, left, right);//left and right respectively represent the generated corresponding functions }, { constant: && , inputs: !isBranching && [left, right] }); }
So what function should a in our demo return? Of course it's $parseBinaryFn. The left and right are $parseBinaryFn of 1+1, respectively, and right is $parseConstant of 2.
Let’s take another example:
var _l = new Lexer({}); var _p = new Parser(_l); var a = _p.parse('{"name": "hello"}'); (a);
Here we pass in a json. Theoretically, after executing the a function, we should return an object of {name: "hello"}. It calls object in Parser
object: function() { var keys = [], valueFns = []; if (().text !== '}') { do { if (('}')) { // Support trailing commas per ES5.1. break; } var token = (); if () { //Take out the key (); } else if () { (); } else { ("invalid key", token); } (':'); //After the colon, it is the value, and the value is stored in valueFns (()); //Iterate over the next one according to the comma } while ((',')); } ('}'); return extend(function $parseObjectLiteral(self, locals) { var object = {}; for (var i = 0, ii = ; i < ii; i++) { object[keys[i]] = valueFns[i](self, locals); } return object; }, { literal: true, constant: (isConstant), inputs: valueFns }); }
For example, in our example {"name": "hello"}, object will store name in keys, hello will generate the $parseConstant function to store valueFns, and finally return the $parseObjectLiternal function.
Next example:
var a = _p.parse('{"name": "hello"}["name"]');
The difference between this and the previous example is that later I try to read the value of name, and here I call the objectIndex method in parser.
objectIndex: function(obj) { var expression = ; var indexFn = (); (']'); return extend(function $parseObjectIndex(self, locals) { var o = obj(self, locals), //parseObjectLiteral, it is actually obj i = indexFn(self, locals), //$parseConstant, here is the 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 which would break ensureSafeObject check var o = ensureSafeObject(obj(self, locals), expression); if (!o) (self, o = {}, locals); return o[key] = value; } }); }
It's very simple, obj[xx] and similar. Everyone read it yourself, let's look at a demo of function calls
var _l = new Lexer({}); var _p = new Parser(_l, '', {}); var demo = { "test": function(){ alert("welcome"); } }; var a = _p.parse('test()'); (a(demo));
We pass in a call to test. This is called the functionCall method and identifier method in parser
identifier: function() { var id = ().text; //Continue reading each `.identifier` unless it is a method invocation while (('.') && (1).identifier && !(2, '(')) { id += ().text + ().text; } return getterFn(id, , ); }
Take a look at the getterFn method
... 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&&("' + 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 */ = valueFn(code); ...
This is how to create an anonymous function through a string. Let's take a look at what anonymous function is generated by the demo test:
function('s', 'l', 'eso', 'fe'){ if(s == null) return undefined; s=((l&&("test"))?l:s).test; return s; }
The meaning of this anonymous function is to pass in a context. The anonymous function directly returns undefined by looking for whether there is a test attribute in the context. This is why we need to pass in demo objects when generating a good a function when executing it. Finally add a functionCall
functionCall: function(fnGetter, contextGetter) { var argsFn = []; if (().text !== ')') { /* Confirm that there are incoming parameters when calling */ do { //Storage formal parameters into argsFn (()); } while ((',')); } (')'); var expressionText = ; // we can safely reuse the array across invocations var args = ? [] : null; return function $parseFunctionCall(scope, locals) { var context = contextGetter ? contextGetter(scope, locals) : isDefined(contextGetter) ? undefined : scope; // Or create the generated anonymous function before var fn = fnGetter(scope, locals, context) || noop; if (args) { var i = ; 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 //The context needs to be passed to when executing anonymous functions var v = ? (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). = 0; } return ensureSafeObject(v, expressionText); }; }
Let's take a look at $ParseProvider, which is an angular built-in provider based on Lex and Parser functions. It provides basic support for scope's API.
... return function $parse(exp, interceptorFn, expensiveChecks) { var parsedExpression, oneTime, cacheKey; switch (typeof exp) { case 'string': cacheKey = exp = (); var cache = (expensiveChecks ? cacheExpensive : cacheDefault); parsedExpression = cache[cacheKey]; if (!parsedExpression) { if ((0) === ':' && (1) === ':') { oneTime = true; exp = (2); } var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions; //Call lexer and parser var lexer = new Lexer(parseOptions); var parser = new Parser(lexer, $filter, parseOptions); parsedExpression = (exp); //Add $$watchDelegate to provide support for scope part if () { 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 = ? oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; } else if () { parsedExpression.$$watchDelegate = inputsWatchDelegate; } //Make related cache cache[cacheKey] = parsedExpression; } return addInterceptor(parsedExpression, interceptorFn); case 'function': return addInterceptor(exp, interceptorFn); default: return addInterceptor(noop, interceptorFn); } };
Summary: The implementation of Lexer and Parser is really eye-opening. Through these two functions, angular's own syntax parser is implemented. The logic part is still relatively complex
The above is the $parse instance code in Angularjs 1.3 introduced to you by the editor. I hope it will be helpful to you. If you have any questions, please leave me a message and the editor will reply to you in time. Thank you very much for your support for my website!