diff --git a/benchmarks/parsed-expressions-bp/app.js b/benchmarks/parsed-expressions-bp/app.js new file mode 100755 index 000000000000..0ebea963af73 --- /dev/null +++ b/benchmarks/parsed-expressions-bp/app.js @@ -0,0 +1,81 @@ +var app = angular.module('parsedExpressionBenchmark', []); + +app.config(function($compileProvider) { + if ($compileProvider.debugInfoEnabled) { + $compileProvider.debugInfoEnabled(false); + } +}); + +app.filter('noop', function() { + return function(input) { + return input; + }; +}); + +//Executes the specified expression as a watcher +app.directive('bmPeWatch', function() { + return { + restrict: 'A', + compile: function($element, $attrs) { + $element.text( $attrs.bmPeWatch ); + return function($scope, $element, $attrs) { + $scope.$watch($attrs.bmPeWatch); + }; + } + }; +}); + +//Executes the specified expression as a watcher +//Adds a simple wrapper method to allow use of $watch instead of $watchCollection +app.directive('bmPeWatchLiteral', function($parse) { + function retZero() { + return 0; + } + + return { + restrict: 'A', + compile: function($element, $attrs) { + $element.text( $attrs.bmPeWatchLiteral ); + return function($scope, $element, $attrs) { + $scope.$watch( $parse($attrs.bmPeWatchLiteral, retZero) ); + }; + } + }; +}); + +app.controller('DataController', function($scope, $rootScope) { + var totalRows = 2000; + + var data = $scope.data = []; + + $scope.func = function() {}; + + for (var i=0; i +
+
+

+ Tests the execution of $parse()ed expressions. Each test tries to isolate specific expression types. Expressions should (probably) not be constant so they get evaluated per digest. +

+ +
    +
  • + + +
  • + +
  • + + +
  • + +
  • + + +
  • + +
  • + + +
  • + +
  • + + +
  • + +
  • + + +
  • + +
  • + + +
  • + +
  • + + +
  • +
+ + + +
    +
  • + + + + + + + + + + + + +
  • + +
  • + + + + + + + + + + + + + +
  • + +
  • + + + + + + + + + + + + +
  • + +
  • + + + + + + + + + + + + +
  • + +
  • + + + + + + + + + + + + +
  • + +
  • + + + + + + + + + + + + + + +
  • + +
  • + + + + + + +
  • + +
  • + + + + + + + + + + + + +
  • + + +
+
+
+ \ No newline at end of file diff --git a/src/ng/parse.js b/src/ng/parse.js index b869f8666945..2e7de5ea3e08 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -80,12 +80,21 @@ function ensureSafeFunction(obj, fullExpression) { } } +//Keyword constants +var CONSTANTS = createMap(); +forEach({ + 'null':function(){return null;}, + 'true':function(){return true;}, + 'false':function(){return false;}, + 'undefined':function(){} +}, function(constFunc, name) { + constFunc.constant = constFunc.literal = constFunc.$$parseShared = true; + CONSTANTS[name] = constFunc; +}); + +//Operators - will be wrapped by binaryFn/unaryFn/assignment/filter var OPERATORS = extend(createMap(), { /* jshint bitwise : false */ - 'null':function(){return null;}, - 'true':function(){return true;}, - 'false':function(){return false;}, - undefined:noop, '+':function(self, locals, a,b){ a=a(self, locals); b=b(self, locals); if (isDefined(a)) { @@ -258,7 +267,7 @@ Lexer.prototype = { }, readIdent: function() { - var parser = this; + var parserText = this.text; var ident = ''; var start = this.index; @@ -305,30 +314,11 @@ Lexer.prototype = { } } - - var token = { + this.tokens.push({ index: start, - text: ident - }; - - var fn = OPERATORS[ident]; - - if (fn) { - token.fn = fn; - token.constant = true; - } else { - var getter = getterFn(ident, this.options, this.text); - // TODO(perf): consider exposing the getter reference - token.fn = extend(function $parsePathGetter(self, locals) { - return getter(self, locals); - }, { - assign: function(self, value) { - return setter(self, ident, value, parser.text); - } - }); - } - - this.tokens.push(token); + text: ident, + fn: CONSTANTS[ident] || getterFn(ident, this.options, parserText) + }); if (methodName) { this.tokens.push({ @@ -397,6 +387,7 @@ var Parser = function (lexer, $filter, options) { Parser.ZERO = extend(function () { return 0; }, { + $$parseShared: true, constant: true }); @@ -531,13 +522,10 @@ Parser.prototype = { // TODO(size): maybe we should not support multiple statements? return (statements.length === 1) ? statements[0] - : function(self, locals) { + : function $parseStatements(self, locals) { var value; - for (var i = 0; i < statements.length; i++) { - var statement = statements[i]; - if (statement) { - value = statement(self, locals); - } + for (var i = 0, ii = statements.length; i < ii; i++) { + value = statements[i](self, locals); } return value; }; @@ -548,13 +536,10 @@ Parser.prototype = { filterChain: function() { var left = this.expression(); var token; - while (true) { - if ((token = this.expect('|'))) { - left = this.binaryFn(left, token.fn, this.filter()); - } else { - return left; - } + while ((token = this.expect('|'))) { + left = this.binaryFn(left, token.fn, this.filter()); } + return left; }, filter: function() { @@ -601,7 +586,7 @@ Parser.prototype = { this.text.substring(0, token.index) + '] can not be assigned to', token); } right = this.ternary(); - return function(scope, locals) { + return function $parseAssignment(scope, locals) { return left.assign(scope, right(scope, locals), locals); }; } @@ -627,13 +612,10 @@ Parser.prototype = { logicalOR: function() { var left = this.logicalAND(); var token; - while (true) { - if ((token = this.expect('||'))) { - left = this.binaryFn(left, token.fn, this.logicalAND()); - } else { - return left; - } + while ((token = this.expect('||'))) { + left = this.binaryFn(left, token.fn, this.logicalAND()); } + return left; }, logicalAND: function() { @@ -695,9 +677,9 @@ Parser.prototype = { }, fieldAccess: function(object) { - var parser = this; + var parserText = this.text; var field = this.expect().text; - var getter = getterFn(field, this.options, this.text); + var getter = getterFn(field, this.options, parserText); return extend(function $parseFieldAccess(scope, locals, self) { return getter(self || object(scope, locals)); @@ -705,13 +687,13 @@ Parser.prototype = { assign: function(scope, value, locals) { var o = object(scope, locals); if (!o) object.assign(scope, o = {}); - return setter(o, field, value, parser.text); + return setter(o, field, value, parserText); } }); }, objectIndex: function(obj) { - var parser = this; + var parserText = this.text; var indexFn = this.expression(); this.consume(']'); @@ -721,15 +703,15 @@ Parser.prototype = { i = indexFn(self, locals), v; - ensureSafeMemberName(i, parser.text); + ensureSafeMemberName(i, parserText); if (!o) return undefined; - v = ensureSafeObject(o[i], parser.text); + v = ensureSafeObject(o[i], parserText); return v; }, { assign: function(self, value, locals) { - var key = ensureSafeMemberName(indexFn(self, locals), parser.text); + var key = ensureSafeMemberName(indexFn(self, locals), parserText); // prevent overwriting of Function.constructor which would break ensureSafeObject check - var o = ensureSafeObject(obj(self, locals), parser.text); + var o = ensureSafeObject(obj(self, locals), parserText); if (!o) obj.assign(self, o = {}); return o[key] = value; } @@ -791,9 +773,9 @@ Parser.prototype = { } this.consume(']'); - return extend(function(self, locals) { + return extend(function $parseArrayLiteral(self, locals) { var array = []; - for (var i = 0; i < elementFns.length; i++) { + for (var i = 0, ii = elementFns.length; i < ii; i++) { array.push(elementFns[i](self, locals)); } return array; @@ -824,9 +806,9 @@ Parser.prototype = { } this.consume('}'); - return extend(function(self, locals) { + return extend(function $parseObjectLiteral(self, locals) { var object = {}; - for (var i = 0; i < keyValues.length; i++) { + for (var i = 0, ii = keyValues.length; i < ii; i++) { var keyValue = keyValues[i]; object[keyValue.key] = keyValue.value(self, locals); } @@ -944,9 +926,14 @@ function getterFn(path, options, fullExp) { var evaledFnGetter = new Function('s', 'l', code); // s=scope, l=locals /* jshint +W054 */ evaledFnGetter.toString = valueFn(code); + evaledFnGetter.assign = function(self, value) { + return setter(self, path, value, path); + }; + fn = evaledFnGetter; } + fn.$$parseShared = true; getterFnCache[path] = fn; return fn; } @@ -1014,6 +1001,21 @@ function $ParseProvider() { this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { $parseOptions.csp = $sniffer.csp; + function wrapSharedExpression(exp) { + var wrapped = exp; + + if (exp.$$parseShared) { + wrapped = function $parseWrapper(self, locals) { + return exp(self, locals); + }; + wrapped.literal = exp.literal; + wrapped.constant = exp.constant; + wrapped.assign = exp.assign; + } + + return wrapped; + } + return function $parse(exp, interceptorFn) { var parsedExpression, oneTime, cacheKey; @@ -1036,6 +1038,9 @@ function $ParseProvider() { 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; } diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 265f12c67b54..31b7b18d4459 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -722,7 +722,7 @@ describe('parser', function() { scope.$eval('a.toString.constructor = 1', scope); }).toThrowMinErr( '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: a.toString.constructor = 1'); + 'Expression: a.toString.constructor'); });