From d53329021c24feb12a4ff21ff0122e5fe15f73b4 Mon Sep 17 00:00:00 2001 From: Yvem Date: Fri, 11 Sep 2015 16:43:57 +0200 Subject: [PATCH] feat(perfs): use $evalAsync() for calling listeners in an optimized way --- README.md | 6 ++-- angular-primus.js | 26 ++++++++-------- test/angular-primus.js | 68 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 77 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 5c9a3d3..4a9bffc 100644 --- a/README.md +++ b/README.md @@ -67,14 +67,16 @@ angular.module('controllers.primus', ['primus']) ### about $on and $filteredOn `$filteredOn` takes as filter either : -* a function, taking the received data as arguments and returning true/false = match/don't match * an object, whom keys will be deep-matched for correspondance with the 1st param of received data, using [lodash matches(...)](https://lodash.com/docs#matches). Example of a deep matching : ```javascript primus.$on('node:update', {content: {id: 23, type: 'image'}}, …) ``` +* a function, taking the received data as arguments and returning true/false = match/don't match + +Both `$on` and `$filteredOn` will call the listener **in Angular context**, in an optimized way via [$evalAsync](http://www.bennadel.com/blog/2751-scope-applyasync-vs-scope-evalasync-in-angularjs-1-3.htm). So if you have several listeners on the same event, they will all get executed in the same $digest phase. -Both `$on` and `$filteredOn` will call the listener **in Angular context** (scope.apply). However, `$filteredOn` will not trigger any apply if the received data doesn't match the given filter. This is desirable if your Angular app is heavy. +`$filteredOn` will not trigger any apply if the received data doesn't match the given filter. This is desirable if your Angular app is heavy. ## License diff --git a/angular-primus.js b/angular-primus.js index 9f2f8da..e0a7bf8 100644 --- a/angular-primus.js +++ b/angular-primus.js @@ -23,7 +23,7 @@ function primusProvider() { /** * Listen on events of a given type. - * Calls the listener inside a $rootScope.$apply. + * Calls the listener inside in Angular context ($evalAsync) * * @param {String} event * @param {Function} listener @@ -31,25 +31,25 @@ function primusProvider() { */ primus.$on = function $on(event, listener) { - // Wrap primus event with $rootScope.$apply. - primus.on(event, applyListener); + // run the listener in Angular context + primus.on(event, listenerInAngularContext); - function applyListener() { + function listenerInAngularContext() { var args = arguments; - $rootScope.$apply(function () { + $rootScope.$evalAsync(function () { listener.apply(null, args); }); } // Return the deregistration function return function $off() { - primus.removeListener(event, applyListener); + primus.removeListener(event, listenerInAngularContext); }; }; /** * Listen on events of a given type, with a filtering pattern. - * If the pattern matches, calls the listener inside a $rootScope.$apply. + * If the pattern matches, calls the listener in Angular context ($evalAsync) * * @param {String} event * @param {Object|Function} matchPattern @@ -60,8 +60,6 @@ function primusProvider() { */ primus.$filteredOn = function $filteredOn(event, matchPattern, listener) { - // Wrap primus event with $rootScope.$apply. - primus.on(event, applyListener); var checkMatch; if (_.isFunction(matchPattern)) @@ -71,18 +69,22 @@ function primusProvider() { else throw new Error('angular-primus $filteredOn() : matchPattern must be a function or an object !'); - function applyListener() { + // run the listener in Angular context + primus.on(event, filteredListenerInAngularContext); + + function filteredListenerInAngularContext() { var args = arguments; var isMatching = checkMatch(args[0]); if (! isMatching) return; - $rootScope.$apply(function () { + + $rootScope.$evalAsync(function () { listener.apply(null, args); }); } // Return the deregistration function return function $off() { - primus.removeListener(event, applyListener); + primus.removeListener(event, filteredListenerInAngularContext); }; }; diff --git a/test/angular-primus.js b/test/angular-primus.js index c159f27..51804a3 100644 --- a/test/angular-primus.js +++ b/test/angular-primus.js @@ -101,19 +101,22 @@ describe('Primus provider', function () { primus = $injector.get('primus'); })); - it('should wrap method in $rootScope.$apply', function () { + it('should call the listener in Angular context', function () { var watchSpy = sinon.spy(); $rootScope.$watch(watchSpy); - expect(watchSpy).to.not.be.called; + expect(watchSpy).to.not.have.been.called; var listener = sinon.spy(); primus.$on('customEvent', listener); primus.emit('customEvent'); + // thanks to $evalAsync, listener exec is scheduled in an angular timeout, + // which won't happen in tests. Trigger it : + $rootScope.$digest(); - expect(listener).to.be.called; - expect(watchSpy).to.be.called; + expect(listener, 'listener').to.have.been.called; + expect(watchSpy, 'watch').to.have.been.called; }); it('should return a deregistration method', function () { @@ -122,6 +125,7 @@ describe('Primus provider', function () { off(); primus.emit('customEvent'); + $rootScope.$digest(); expect(myListener).to.not.be.called; }); @@ -157,6 +161,10 @@ describe('Primus provider', function () { // base listenerSpy.reset(); primus.emit('customEvent', {itemId: 1}); + // thanks to $evalAsync, listener exec is scheduled in an angular timeout, + // which won't happen in tests. Trigger it : + $rootScope.$digest(); + expect(listenerSpy, 'listenerSpy').to.have.been.calledOnce; // variant @@ -167,6 +175,8 @@ describe('Primus provider', function () { foo: 42 } }); + $rootScope.$digest(); + expect(listenerSpy, 'listenerSpy').to.have.been.calledOnce; }); @@ -175,14 +185,19 @@ describe('Primus provider', function () { // not the same value primus.emit('customEvent', {itemId: 11}); + // thanks to $evalAsync, listener exec is scheduled in an angular timeout, + // which won't happen in tests. Trigger it : + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.not.have.been.called; // not the same key / missing key primus.emit('customEvent', {id: 1}); + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.not.have.been.called; // nothing at all primus.emit('customEvent', 42); + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.not.have.been.called; }); @@ -192,6 +207,9 @@ describe('Primus provider', function () { // base listenerSpy.reset(); primus.emit('customEvent', {content: {id: 1}}); + // thanks to $evalAsync, listener exec is scheduled in an angular timeout, + // which won't happen in tests. Trigger it : + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.have.been.calledOnce; // variant @@ -203,6 +221,7 @@ describe('Primus provider', function () { foo: 42 } }); + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.have.been.calledOnce; }); @@ -211,14 +230,19 @@ describe('Primus provider', function () { // not the same value primus.emit('customEvent', {content: {id: 11}}); + // thanks to $evalAsync, listener exec is scheduled in an angular timeout, + // which won't happen in tests. Trigger it : + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.not.have.been.called; // not the same key / missing key primus.emit('customEvent', {content: {itemId: 1}}); + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.not.have.been.called; // nothing at all primus.emit('customEvent', 42); + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.not.have.been.called; }); @@ -228,6 +252,9 @@ describe('Primus provider', function () { // base listenerSpy.reset(); primus.emit('customEvent', {id: 1, content: {id: 1}}); + // thanks to $evalAsync, listener exec is scheduled in an angular timeout, + // which won't happen in tests. Trigger it : + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.have.been.calledOnce; // variant @@ -240,6 +267,7 @@ describe('Primus provider', function () { bar: 33 } }); + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.have.been.calledOnce; }); @@ -248,22 +276,29 @@ describe('Primus provider', function () { // not the same value - 1 primus.emit('customEvent', {id: 11, content: {id: 1}}); + // thanks to $evalAsync, listener exec is scheduled in an angular timeout, + // which won't happen in tests. Trigger it : + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.not.have.been.called; // not the same value - 2 primus.emit('customEvent', {id: 1, content: {id: 11}}); + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.not.have.been.called; // missing key - 1 primus.emit('customEvent', {id: 1}); + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.not.have.been.called; // missing key - 2 primus.emit('customEvent', {content: {id: 1}}); + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.not.have.been.called; // nothing at all primus.emit('customEvent', 42); + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.not.have.been.called; }); @@ -279,10 +314,14 @@ describe('Primus provider', function () { listenerSpy.reset(); primus.emit('customEvent', {id: 1}); + // thanks to $evalAsync, listener exec is scheduled in an angular timeout, + // which won't happen in tests. Trigger it : + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.have.been.calledOnce; listenerSpy.reset(); primus.emit('customEvent', {id: 3}); + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.have.been.calledOnce; }); @@ -290,36 +329,46 @@ describe('Primus provider', function () { primus.$filteredOn('customEvent', testFunction, listenerSpy); primus.emit('customEvent', {id: 0}); + // thanks to $evalAsync, listener exec is scheduled in an angular timeout, + // which won't happen in tests. Trigger it : + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.not.have.been.called; - primus.emit('customEvent', {id: 20}); + primus.emit('customEvent', {id: 20}); + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.not.have.been.called; }); }); }); - it('when calling callback, should wrap it in $rootScope.$apply', function () { + it('when calling callback, should call it in Angular context', function () { expect(listenerSpy, 'listenerSpy').to.not.have.been.called; expect(watchSpy, 'watchSpy').to.not.have.been.called; primus.$filteredOn('customEvent', {id: 1}, listenerSpy); primus.emit('customEvent', {id: 1}); - + // thanks to $evalAsync, listener exec is scheduled in an angular timeout, + // which won't happen in tests. Trigger it : + $rootScope.$digest(); + expect(listenerSpy, 'listenerSpy').to.have.been.calledOnce; expect(digestWasInProgress).to.be.true; expect(watchSpy, 'watchSpy').to.have.been.calledTwice; }); - it('when NOT calling callback, should NOT call $rootScope.$apply', function () { + it('when NOT calling callback, should NOT trigger a $rootScope $digest', function () { expect(listenerSpy, 'listenerSpy').to.not.have.been.called; expect(watchSpy, 'watchSpy').to.not.have.been.called; primus.$filteredOn('customEvent', {id: 1}, listenerSpy); primus.emit('customEvent', {id: 2}); - expect(listenerSpy, 'listenerSpy').to.not.have.been.called; expect(watchSpy, 'watchSpy').to.not.have.been.called; + $rootScope.$digest(); + + expect(listenerSpy, 'listenerSpy').to.not.have.been.called; + expect(watchSpy, 'watchSpy').to.have.been.calledTwice; // still, due to our explicit $rootScope.$digest }); it('should return a working deregistration method', function () { @@ -327,6 +376,7 @@ describe('Primus provider', function () { off(); primus.emit('customEvent', {id: 1}); + $rootScope.$digest(); expect(listenerSpy, 'listenerSpy').to.not.have.been.called; });