Skip to content

Commit b004276

Browse files
committed
beef up $defer service
- adding concept of named, manually flushable queues - unifying the api for defering stuff via queues and setTimeout - adding support for canceling tasks
1 parent bbff9cf commit b004276

File tree

2 files changed

+194
-62
lines changed

2 files changed

+194
-62
lines changed

src/service/defer.js

+83-11
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,92 @@
77
* @requires $updateView
88
*
99
* @description
10-
* Delegates to {@link angular.service.$browser.defer $browser.defer}, but wraps the `fn` function
11-
* into a try/catch block and delegates any exceptions to
12-
* {@link angular.services.$exceptionHandler $exceptionHandler} service.
10+
* Defers an execution of a function using various queues.
1311
*
14-
* In tests you can use `$browser.defer.flush()` to flush the queue of deferred functions.
12+
* When `queue` argument is undefined or set to `'$setTimeout'`, the defered function `fn` will be
13+
* delegated to {@link angular.service.$browser.defer $browser.defer}, which will result in
14+
* in a `setTimeout` timer to be registered with delay set to the `delay` value. The function
15+
* `fn` will be wrapped into {@link angular.scope.$apply rootScope.$apply}, in order to to allow the
16+
* deferred function to participate in angular's app life-cycle.
1517
*
16-
* @param {function()} fn A function, who's execution should be deferred.
17-
* @param {number=} [delay=0] of milliseconds to defer the function execution.
18+
* In tests you can use `$defer.flush('$setTimeout') or `$browser.defer.flush()` to flush the queue
19+
* of deferred functions in the `'$setTimeout'` queue.
20+
*
21+
* When `queue` argument is defined, the `fn` function will be put into a queue specified by this
22+
* argument. To flush the queue in application or test, call `$defer.flush(queueName)`.
23+
*
24+
* Angular uses a queue called `'$burp'`, to execute task synchronously with regards to the $apply
25+
* cycle. This queue is flushed right after `$digest` (hence the name).
26+
*
27+
* A task can be removed from any execution queue (if it hasn't executed yet), by calling
28+
*`$defer.cancel(cancelToken)`, where `cancelToken` is the return value of calling the $defer
29+
* function when registering `fn`.
30+
*
31+
* @param {string=} [queue='$setTimeout'] The name of the deferral queue.
32+
* @param {function()} fn A task — function, execution of which should be deferred.
33+
* @param {(number|string)=} [delay=0] of milliseconds to defer the function execution in the
34+
* $setTimeout queue.
35+
* @returns {*} A token, which can be passed into $defer.cancel() method to cancel the deferred
36+
* task.
1837
*/
19-
angularServiceInject('$defer', function($browser) {
20-
var scope = this;
21-
return function(fn, delay) {
22-
$browser.defer(function() {
38+
angularServiceInject('$defer', function($browser, $exceptionHandler) {
39+
var scope = this,
40+
queues = {},
41+
canceledTasks = {},
42+
idGenerator = 0,
43+
setTimeoutQ = '$setTimeout';
44+
45+
function defer(queue, fn, delay) {
46+
if (isFunction(queue)) {
47+
delay = fn;
48+
fn = queue;
49+
queue = setTimeoutQ;
50+
}
51+
52+
if (queue != setTimeoutQ) {
53+
var id = idGenerator++;
54+
(queues[queue] || (queues[queue] = [])).push({id: id, fn: fn});
55+
return {q: queue, id: id};
56+
}
57+
58+
return $browser.defer(function() {
2359
scope.$apply(fn);
2460
}, delay);
2561
};
26-
}, ['$browser', '$exceptionHandler', '$updateView']);
62+
63+
64+
defer.flush = function(queue) {
65+
assertArg(queue, 'queue');
66+
67+
if (queue == setTimeoutQ) {
68+
$browser.defer.flush();
69+
}
70+
71+
forEach(queues[queue], function(task) {
72+
try {
73+
if (!(canceledTasks[queue] && canceledTasks[queue][task.id])) {
74+
task.fn();
75+
}
76+
} catch(e) {
77+
$exceptionHandler(e);
78+
}
79+
});
80+
81+
queues[queue] = [];
82+
canceledTasks[queue] = {};
83+
}
84+
85+
86+
defer.cancel = function(cancelToken) {
87+
if (isUndefined(cancelToken)) return;
88+
89+
if (cancelToken.q) {
90+
(canceledTasks[cancelToken.q] || (canceledTasks[cancelToken.q] = {}))[cancelToken.id] = true;
91+
} else {
92+
$browser.defer.cancel(deferId);
93+
}
94+
}
95+
96+
97+
return defer;
98+
}, ['$browser', '$exceptionHandler']);

test/service/deferSpec.js

+111-51
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,136 @@
11
describe('$defer', function() {
2-
var scope, $browser, $defer, $exceptionHandler;
3-
4-
beforeEach(function(){
5-
scope = angular.scope(angular.service,
6-
{'$exceptionHandler': jasmine.createSpy('$exceptionHandler')});
7-
$browser = scope.$service('$browser');
8-
$defer = scope.$service('$defer');
9-
$exceptionHandler = scope.$service('$exceptionHandler');
10-
});
112

12-
afterEach(function(){
13-
dealoc(scope);
14-
});
3+
describe('setTimeout backed deferral', function() {
4+
var scope, $browser, $defer, $exceptionHandler;
155

6+
beforeEach(function(){
7+
scope = angular.scope(angular.service,
8+
{'$exceptionHandler': jasmine.createSpy('$exceptionHandler')});
9+
$browser = scope.$service('$browser');
10+
$defer = scope.$service('$defer');
11+
$exceptionHandler = scope.$service('$exceptionHandler');
12+
});
1613

17-
it('should delegate functions to $browser.defer', function() {
18-
var counter = 0;
19-
$defer(function() { counter++; });
14+
afterEach(function(){
15+
dealoc(scope);
16+
});
2017

21-
expect(counter).toBe(0);
2218

23-
$browser.defer.flush();
24-
expect(counter).toBe(1);
19+
it('should delegate functions to $browser.defer', function() {
20+
var counter = 0;
21+
$defer(function() { counter++; });
2522

26-
$browser.defer.flush(); //does nothing
27-
expect(counter).toBe(1);
23+
expect(counter).toBe(0);
2824

29-
expect($exceptionHandler).not.toHaveBeenCalled();
30-
});
25+
$browser.defer.flush();
26+
expect(counter).toBe(1);
3127

28+
$browser.defer.flush(); //does nothing
29+
expect(counter).toBe(1);
3230

33-
it('should delegate exception to the $exceptionHandler service', function() {
34-
$defer(function() {throw "Test Error";});
35-
expect($exceptionHandler).not.toHaveBeenCalled();
31+
expect($exceptionHandler).not.toHaveBeenCalled();
32+
});
3633

37-
$browser.defer.flush();
38-
expect($exceptionHandler).toHaveBeenCalledWith("Test Error");
39-
});
4034

35+
it('should delegate exception to the $exceptionHandler service', function() {
36+
$defer(function() {throw "Test Error";});
37+
expect($exceptionHandler).not.toHaveBeenCalled();
4138

42-
it('should call eval after each callback is executed', function() {
43-
var eval = this.spyOn(scope, '$eval').andCallThrough();
39+
$browser.defer.flush();
40+
expect($exceptionHandler).toHaveBeenCalledWith("Test Error");
41+
});
4442

45-
$defer(function() {});
46-
expect(eval).wasNotCalled();
4743

48-
$browser.defer.flush();
49-
expect(eval).wasCalled();
44+
it('should call $apply after each callback is executed', function() {
45+
var eval = this.spyOn(scope, '$apply').andCallThrough();
5046

51-
eval.reset(); //reset the spy;
47+
$defer(function() {});
48+
expect(eval).wasNotCalled();
5249

53-
$defer(function() {});
54-
$defer(function() {});
55-
$browser.defer.flush();
56-
expect(eval.callCount).toBe(2);
57-
});
50+
$browser.defer.flush();
51+
expect(eval).wasCalled();
52+
53+
eval.reset(); //reset the spy;
5854

55+
$defer(function() {});
56+
$defer(function() {});
57+
$browser.defer.flush();
58+
expect(eval.callCount).toBe(2);
59+
});
5960

60-
it('should call eval even if an exception is thrown in callback', function() {
61-
var eval = this.spyOn(scope, '$eval').andCallThrough();
6261

63-
$defer(function() {throw "Test Error";});
64-
expect(eval).wasNotCalled();
62+
it('should call $apply even if an exception is thrown in callback', function() {
63+
var eval = this.spyOn(scope, '$apply').andCallThrough();
6564

66-
$browser.defer.flush();
67-
expect(eval).wasCalled();
65+
$defer(function() {throw "Test Error";});
66+
expect(eval).wasNotCalled();
67+
68+
$browser.defer.flush();
69+
expect(eval).wasCalled();
70+
});
71+
72+
73+
it('should allow you to specify the delay time', function(){
74+
var defer = this.spyOn($browser, 'defer');
75+
$defer(noop, 123);
76+
expect(defer.callCount).toEqual(1);
77+
expect(defer.mostRecentCall.args[1]).toEqual(123);
78+
});
6879
});
6980

70-
it('should allow you to specify the delay time', function(){
71-
var defer = this.spyOn($browser, 'defer');
72-
$defer(noop, 123);
73-
expect(defer.callCount).toEqual(1);
74-
expect(defer.mostRecentCall.args[1]).toEqual(123);
81+
82+
describe('queue based deferral', function() {
83+
var scope, defer, log;
84+
85+
beforeEach(function() {
86+
scope = angular.scope();
87+
$defer = scope.$service('$defer');
88+
log = [];
89+
});
90+
91+
92+
it('should allow a task to be scheduled and executed upon flush()', function() {
93+
var id = $defer('myQueue', function() { log.push('work'); });
94+
expect(id).toBeDefined();
95+
expect(log).toEqual([]);
96+
97+
$defer.flush('wrongQueue');
98+
expect(log).toEqual([]);
99+
100+
$defer.flush('myQueue');
101+
expect(log).toEqual(['work']);
102+
});
103+
104+
105+
it('should allow a task to be overriden by another task', function() {
106+
$defer('myQueue', function() { log.push('work 0') });
107+
var id = $defer('myQueue', function() { log.push('work 1') });
108+
$defer('myQueue', function() { log.push('work 2') });
109+
$defer('myQueue', function() { log.push('work 3') });
110+
$defer.cancel(id);
111+
112+
$defer.flush('myQueue');
113+
expect(log).toEqual(['work 0', 'work 2', 'work 3']);
114+
});
115+
116+
117+
it('should ignore attempts to overide flushed tasks', function() {
118+
var id = $defer('myQueue', function() { log.push('work 0') });
119+
$defer.flush('myQueue');
120+
121+
$defer('myQueue', function() { log.push('work 1') });
122+
$defer.cancel(id);
123+
$defer.flush('myQueue');
124+
125+
expect(log).toEqual(['work 0', 'work 1']);
126+
});
127+
128+
129+
it('should generate different ids for tasks', function() {
130+
var id1 = $defer('myQueue', function() {});
131+
var id2 = $defer('myQueue', function() {});
132+
var id3 = $defer('myQueue', function() {});
133+
expect(id1.id < id2.id < id3.id).toBe(true);
134+
});
75135
});
76136
});

0 commit comments

Comments
 (0)