From 283ea257309f8c91f80942c894ec4193b60fd725 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 21 Nov 2013 21:50:41 -0800 Subject: [PATCH] feat(interpolate): use $watchSet to remove memory pressure --- lib/core/interpolate.dart | 83 ++++++++++++++------------------ lib/core_dom/ng_mustache.dart | 19 ++++---- test/core/interpolate_spec.dart | 85 +++++++++++++-------------------- 3 files changed, 79 insertions(+), 108 deletions(-) diff --git a/lib/core/interpolate.dart b/lib/core/interpolate.dart index 07d55f4cf..ca5ece0b0 100644 --- a/lib/core/interpolate.dart +++ b/lib/core/interpolate.dart @@ -5,6 +5,26 @@ String _endSymbol = '}}'; num _startSymbolLength = _startSymbol.length; num _endSymbolLength = _endSymbol.length; +class Interpolation { + final String template; + final List seperators; + final List watchExpressions; + Function setter = (_) => _; + + Interpolation(this.template, this.seperators, this.watchExpressions); + + String call(List parts, [_, __]) { + var str = []; + for(var i = 0, ii = parts.length; i < ii; i++) { + str.add(seperators[i]); + var value = parts[i]; + str.add(value == null ? '' : '$value'); + } + str.add(seperators.last); + return setter(str.join('')); + } +} + /** * Compiles a string with markup into an interpolation function. This service * is used by the HTML [Compiler] service for data binding. @@ -15,10 +35,9 @@ num _endSymbolLength = _endSymbol.length; * expect(exp({name:'Angular'}).toEqual('Hello Angular!'); */ class Interpolate { - Parser _parse; - ExceptionHandler _exceptionHandler; + final Parser _parse; - Interpolate(Parser this._parse, ExceptionHandler this._exceptionHandler); + Interpolate(Parser this._parse); /** * Compile markup text into interpolation function. @@ -29,67 +48,35 @@ class Interpolate { * Strings with no embedded expression will return null for the * interpolation function. */ - Expression call(String text, [bool mustHaveExpression = false]) { + Interpolation call(String template, [bool mustHaveExpression = false]) { num startIndex; num endIndex; num index = 0; - List chunks = []; - num length = text.length; + num length = template.length; bool hasInterpolation = false; String exp; - List concat = []; - Expression fn; + List separators = []; + List watchExpressions = []; while(index < length) { - if ( ((startIndex = text.indexOf(_startSymbol, index)) != -1) && - ((endIndex = text.indexOf(_endSymbol, startIndex + _startSymbolLength)) != -1) ) { - if (index != startIndex) { - chunks.add(text.substring(index, startIndex)); - } - fn = _parse(exp = text.substring(startIndex + _startSymbolLength, endIndex)); - chunks.add(fn); - fn.exp = exp; + if ( ((startIndex = template.indexOf(_startSymbol, index)) != -1) && + ((endIndex = template.indexOf(_endSymbol, startIndex + _startSymbolLength)) != -1) ) { + separators.add(template.substring(index, startIndex)); + exp = template.substring(startIndex + _startSymbolLength, endIndex); + watchExpressions.add((_parse(exp)..exp = exp).eval); index = endIndex + _endSymbolLength; hasInterpolation = true; } else { // we did not find anything, so we have to add the remainder to the chunks array - if (index != length) { - chunks.add(text.substring(index)); - } + separators.add(template.substring(index)); index = length; } } - - if ((length = chunks.length) == 0) { - // we added, nothing, must have been an empty string. - chunks.add(''); - length = 1; + if (separators.length == watchExpressions.length) { + separators.add(''); } - if (!mustHaveExpression || hasInterpolation) { - fn = new Expression((context, [locals]) { - try { - for(var i = 0, ii = length, chunk; i attrs[attrName] = text; - attrSetter(''); - scope.$watch(interpolateFn.eval, attrSetter, markup.trim()); + Interpolation interpolation = interpolate(match[2]); + interpolation.setter = (text) => attrs[attrName] = text; + interpolation.setter(''); + scope.$watchSet(interpolation.watchExpressions, interpolation.call, markup.trim()); } } diff --git a/test/core/interpolate_spec.dart b/test/core/interpolate_spec.dart index 8299d98ad..4a1e912a9 100644 --- a/test/core/interpolate_spec.dart +++ b/test/core/interpolate_spec.dart @@ -15,115 +15,98 @@ main() { })); it('should suppress falsy objects', inject((Interpolate $interpolate) { - expect($interpolate('{{undefined}}').eval(null)).toEqual(''); - expect($interpolate('{{undefined+undefined}}').eval(null)).toEqual(''); - expect($interpolate('{{null}}').eval(null)).toEqual(''); - expect($interpolate('{{a.b}}').eval(null)).toEqual(''); + expect($interpolate('{{undefined}}')([null])).toEqual(''); + expect($interpolate('{{undefined+undefined}}')([null])).toEqual(''); + expect($interpolate('{{null}}')([null])).toEqual(''); + expect($interpolate('{{a.b}}')([null])).toEqual(''); })); it('should jsonify objects', inject((Interpolate $interpolate) { - expect($interpolate('{{ {} }}').eval(null)).toEqual('{}'); - expect($interpolate('{{ true }}').eval(null)).toEqual('true'); - expect($interpolate('{{ false }}').eval(null)).toEqual('false'); + expect($interpolate('{{ {} }}')([{}])).toEqual('{}'); + expect($interpolate('{{ true }}')([true])).toEqual('true'); + expect($interpolate('{{ false }}')([false])).toEqual('false'); })); - it('should rethrow exceptions', inject((Interpolate $interpolate, Scope $rootScope) { - $rootScope.err = () { - throw 'oops'; - }; - expect(() { - $interpolate('{{err()}}').eval($rootScope); - }).toThrow(r"$interpolate error! Can't interpolate: {{err()}}"); - })); it('should return interpolation function', inject((Interpolate $interpolate, Scope $rootScope) { $rootScope.name = 'Misko'; var fn = $interpolate('Hello {{name}}!'); - expect(fn.eval($rootScope)).toEqual('Hello Misko!'); - expect(fn.eval($rootScope)).toEqual('Hello Misko!'); + expect(fn(['Misko'])).toEqual('Hello Misko!'); })); it('should ignore undefined model', inject((Interpolate $interpolate) { - expect($interpolate("Hello {{'World' + foo}}").eval(null)).toEqual('Hello World'); + expect($interpolate("Hello {{'World' + foo}}")(['World'])).toEqual('Hello World'); })); - it('should ignore undefined return value', inject((Interpolate $interpolate, Scope $rootScope) { - $rootScope.foo = () => null; - expect($interpolate("Hello {{'World' + foo()}}").eval($rootScope)).toEqual('Hello World'); - })); - it('should use toString to conver objects to string', inject((Interpolate $interpolate, Scope $rootScope) { - $rootScope.obj = new ToStringableObject(); - expect($interpolate("Hello, {{obj}}!").eval($rootScope)).toEqual('Hello, World!'); + expect($interpolate("Hello, {{obj}}!")([new ToStringableObject()])).toEqual('Hello, World!'); })); describe('parseBindings', () { it('should Parse Text With No Bindings', inject((Interpolate $interpolate) { - var parts = $interpolate("a").parts; + var parts = $interpolate("a").seperators; expect(parts.length).toEqual(1); expect(parts[0]).toEqual("a"); })); it('should Parse Empty Text', inject((Interpolate $interpolate) { - var parts = $interpolate("").parts; + var parts = $interpolate("").seperators; expect(parts.length).toEqual(1); expect(parts[0]).toEqual(""); })); it('should Parse Inner Binding', inject((Interpolate $interpolate) { - var parts = $interpolate("a{{b}}C").parts; - expect(parts.length).toEqual(3); + var parts = $interpolate("a{{b}}C").seperators; + expect(parts.length).toEqual(2); expect(parts[0]).toEqual("a"); - expect(parts[1].exp).toEqual("b"); - expect(parts[1].eval({'b':123})).toEqual(123); - expect(parts[2]).toEqual("C"); + expect(parts[1]).toEqual("C"); })); it('should Parse Ending Binding', inject((Interpolate $interpolate) { - var parts = $interpolate("a{{b}}").parts; + var parts = $interpolate("a{{b}}").seperators; expect(parts.length).toEqual(2); expect(parts[0]).toEqual("a"); - expect(parts[1].exp).toEqual("b"); - expect(parts[1].eval({'b':123})).toEqual(123); + expect(parts[1]).toEqual(""); })); it('should Parse Begging Binding', inject((Interpolate $interpolate) { - var parts = $interpolate("{{b}}c").parts; + var parts = $interpolate("{{b}}c").seperators; expect(parts.length).toEqual(2); - expect(parts[0].exp).toEqual("b"); + expect(parts[0]).toEqual(""); expect(parts[1]).toEqual("c"); })); it('should Parse Loan Binding', inject((Interpolate $interpolate) { - var parts = $interpolate("{{b}}").parts; - expect(parts.length).toEqual(1); - expect(parts[0].exp).toEqual("b"); + var parts = $interpolate("{{b}}").seperators; + expect(parts.length).toEqual(2); + expect(parts[0]).toEqual(""); + expect(parts[1]).toEqual(""); })); it('should Parse Two Bindings', inject((Interpolate $interpolate) { - var parts = $interpolate("{{b}}{{c}}").parts; - expect(parts.length).toEqual(2); - expect(parts[0].exp).toEqual("b"); - expect(parts[1].exp).toEqual("c"); + var parts = $interpolate("{{b}}{{c}}").seperators; + expect(parts.length).toEqual(3); + expect(parts[0]).toEqual(""); + expect(parts[1]).toEqual(""); + expect(parts[2]).toEqual(""); })); it('should Parse Two Bindings With Text In Middle', inject((Interpolate $interpolate) { - var parts = $interpolate("{{b}}x{{c}}").parts; + var parts = $interpolate("{{b}}x{{c}}").seperators; expect(parts.length).toEqual(3); - expect(parts[0].exp).toEqual("b"); + expect(parts[0]).toEqual(""); expect(parts[1]).toEqual("x"); - expect(parts[2].exp).toEqual("c"); + expect(parts[2]).toEqual(""); })); it('should Parse Multiline', inject((Interpolate $interpolate) { - var parts = $interpolate('"X\nY{{A\n+B}}C\nD"').parts; - expect(parts.length).toEqual(3); + var parts = $interpolate('"X\nY{{A\n+B}}C\nD"').seperators; + expect(parts.length).toEqual(2); expect(parts[0]).toEqual('"X\nY'); - expect(parts[1].exp).toEqual('A\n+B'); - expect(parts[2]).toEqual('C\nD"'); + expect(parts[1]).toEqual('C\nD"'); })); }); });