goog.async.Deferred
named
goog.deferred.Deferred
.
goog.deferred.Deferred
Source Code
goog.async.Deferred
Source Code
goog.deferred.Deferred
Unit Test Source Code
Run goog.deferred.Deferred
Unit Test
One point of discussion is whether the callback()
and
errback()
methods should be extended to
take a variable number of arguments.
On one hand, this would be more convenient than having to package multiple arguments to a callback or errback in an object literal.
On the other hand, I think that the concept of a Deferred is incredibly
important, to the point that I think there should be support for
parameterization in the Closure Compiler's type system as there is for
Array
and Object
. For example, the following are
meaningful type expressions: Array.<number>
and
Object.<boolean>
.
Therefore, ultimately I would like to see support for
goog.deferred.Deferred.<A,B>
, which would imply that the
methods for that instance would become:
/** * @param {!function(A)} callback * @param {?Object=} scope */ goog.deferred.Deferred.prototype.addCallback = function(callback, scope) { //... /** * @param {!function(B)} errback * @param {?Object=} scope */ goog.deferred.Deferred.prototype.addErrback = function(errback, scope) { //... /** * @param {A} result */ goog.deferred.Deferred.prototype.callback = function(result) { //... /** * @param {B} result */ goog.deferred.Deferred.prototype.errback = function(result) { //...
Arguably, if callbacks and errbacks take a single argument,
this is much more intuitive. Also, goog.deferred.Deferred.<A>
would be a shorthand for goog.deferred.Deferred.<A,A>
(or maybe goog.deferred.Deferred.<A,*>
).
In addition to simplifying the API of goog.async.Deferred
,
goog.deferred.Deferred
also introduces some new methods:
map()
bind()
addScopeCheckedCallback()
The names of methods in this class are subject to debate. Personally, when I
first started using goog.async.Deferred
, I was really thrown by
callback()
vs. addCallback()
. One option would be to
refer to a Deferred
whose value is set as determined,
in which case callback()
and errback()
could become
determineSuccess()
and determineFailure()
,
respectively (though those names have their own drawbacks).
Admittedly, bind()
and map()
may be too easily
confused with things like goog.bind()
or
goog.array.map()
.
addScopeCheckedCallback()
is perhaps an uncomfortably long name.
Deferred
is accepted, then a new
implementation of DeferredList
will also be provided.
Below is the syntax-highlighted source code for
goog.deferred.Deferred
:
1 /** 2 * @fileoverview goog.deferred.Deferred is similar to goog.async.Deferred, but 3 * has some important differences in its design: 4 * <ul> 5 * <li>When errback(arg) is called, arg is not wrapped in an Error. 6 * <li>When callback(arg) is called and arg is an Error, the functions 7 * registered via addCallback() are called with arg rather than those 8 * registered via addErrback(). 9 * <li>There is no automatic chaining when a callback or errback returns a 10 * Deferred. 11 * <li>There is no such thing as a canceller or defaultScope. 12 * </ul> 13 * The goal of these design changes is to make operations with 14 * goog.deferred.Deferred more explicit (and hopefully less error-prone) than 15 * when working with goog.async.Deferred. 16 * 17 * @author bolinfest@gmail.com (Michael Bolin) 18 */ 19 goog.provide('goog.deferred.Deferred'); 20 goog.provide('goog.deferred.Deferred.State'); 21 22 goog.require('goog.array'); 23 goog.require('goog.asserts'); 24 goog.require('goog.functions'); 25 26 27 /** 28 * @constructor 29 */ 30 goog.deferred.Deferred = function() {}; 31 32 33 /** @enum {number} */ 34 goog.deferred.Deferred.State = { 35 NO_RESULT: 1, 36 SUCCESS: 2, 37 ERROR: 3 38 }; 39 40 41 /** 42 * @type {!goog.deferred.Deferred.State} 43 * @private 44 */ 45 goog.deferred.Deferred.prototype.state_ = 46 goog.deferred.Deferred.State.NO_RESULT; 47 48 49 /** 50 * @type {?Array} 51 * @private 52 */ 53 goog.deferred.Deferred.prototype.callbacks_; 54 55 56 /** 57 * @type {?Array} 58 * @private 59 */ 60 goog.deferred.Deferred.prototype.errbacks_; 61 62 63 /** 64 * @type {*} 65 * @private 66 */ 67 goog.deferred.Deferred.prototype.result_; 68 69 70 /** 71 * @param {!Function} callback 72 * @param {?Object=} scope 73 * @return {!goog.deferred.Deferred} 74 */ 75 goog.deferred.Deferred.prototype.addCallback = function(callback, scope) { 76 return this.addCallbacks_(callback, null /* errback */, scope); 77 }; 78 79 80 /** 81 * @param {!Function} errback 82 * @param {?Object=} scope 83 * @return {!goog.deferred.Deferred} 84 */ 85 goog.deferred.Deferred.prototype.addErrback = function(errback, scope) { 86 return this.addCallbacks_(null /* callback */, errback, scope); 87 }; 88 89 90 /** 91 * @param {!Function} callback 92 * @param {!Function} errback 93 * @param {?Object=} scope 94 * @return {!goog.deferred.Deferred} 95 */ 96 goog.deferred.Deferred.prototype.addCallbacks = function( 97 callback, errback, scope) { 98 return this.addCallbacks_(callback, errback, scope); 99 }; 100 101 102 /** 103 * @param {!Function} f 104 * @param {?Object=} scope 105 * @return {!goog.deferred.Deferred} 106 */ 107 goog.deferred.Deferred.prototype.addBoth = function(f, scope) { 108 return this.addCallbacks_(f, f, scope); 109 }; 110 111 112 /** 113 * @param {?Function} callback 114 * @param {?Function} errback 115 * @param {?Object=} scope 116 * @return {!goog.deferred.Deferred} 117 * @private 118 */ 119 goog.deferred.Deferred.prototype.addCallbacks_ = function( 120 callback, errback, scope) { 121 scope = scope || null; 122 123 // If this Deferred has already been determined, then simply call the 124 // appropriate function with the result and exit. 125 if (this.hasResult_()) { 126 var f = this.isSuccess_() ? callback : errback; 127 if (f) f.call(scope, this.result_); 128 return this; 129 } 130 131 if (callback) { 132 var callbacks = this.callbacks_ || (this.callbacks_ = []); 133 callbacks.push(goog.bind(callback, scope)); 134 } 135 if (errback) { 136 var errbacks = this.errbacks_ || (this.errbacks_ = []); 137 errbacks.push(goog.bind(errback, scope)); 138 } 139 return this; 140 }; 141 142 143 /** 144 * @return {boolean} whether this result has been determined 145 * @private 146 */ 147 goog.deferred.Deferred.prototype.hasResult_ = function() { 148 return this.state_ != goog.deferred.Deferred.State.NO_RESULT; 149 }; 150 151 152 /** 153 * @return {boolean} 154 * @private 155 */ 156 goog.deferred.Deferred.prototype.isSuccess_ = function() { 157 return this.state_ == goog.deferred.Deferred.State.SUCCESS; 158 }; 159 160 /** 161 * @param {*} result 162 */ 163 goog.deferred.Deferred.prototype.callback = function(result) { 164 this.setResult_(result, goog.deferred.Deferred.State.SUCCESS); 165 }; 166 167 168 /** 169 * @param {*} result 170 */ 171 goog.deferred.Deferred.prototype.errback = function(result) { 172 this.setResult_(result, goog.deferred.Deferred.State.ERROR); 173 }; 174 175 176 /** 177 * @param {*} result 178 * @param {!goog.deferred.Deferred.State} state 179 * @private 180 */ 181 goog.deferred.Deferred.prototype.setResult_ = function(result, state) { 182 if (this.hasResult_()) { 183 throw new Error('goog.deferred.Deferred already has a result'); 184 } 185 this.state_ = state; 186 this.result_ = result; 187 188 // If this is a SUCCESS, then the errbacks will never get called (and vice 189 // versa), so they should be dereferenced so they can be garbage collected. 190 var functions; 191 if (this.isSuccess_()) { 192 delete this.errbacks_; 193 } else { 194 delete this.callbacks_; 195 } 196 197 this.fire_(); 198 }; 199 200 201 /** 202 * @private 203 */ 204 goog.deferred.Deferred.prototype.fire_ = function() { 205 goog.asserts.assert(this.hasResult_()); 206 207 // Call each of the appropriate registered functions with the result. 208 var isSuccess = this.isSuccess_(); 209 var result = this.result_; 210 var functions = isSuccess ? this.callbacks_ : this.errbacks_; 211 var exceptions; 212 if (functions) { 213 goog.array.forEach(functions, function(f) { 214 try { 215 f(result); 216 } catch (e) { 217 exceptions = exceptions || []; 218 exceptions.push(e); 219 } 220 }); 221 } 222 223 // Now that all of the registered functions have been called, delete all of 224 // their references. 225 if (isSuccess) { 226 delete this.callbacks_; 227 } else { 228 delete this.errbacks_; 229 } 230 231 if (exceptions) { 232 goog.array.forEach(exceptions, this.throwException_, this); 233 } 234 }; 235 236 237 /** 238 * @param {*} e 239 */ 240 goog.deferred.Deferred.prototype.throwException_ = function(e) { 241 goog.global.setTimeout(function() { throw e; }, 0); 242 }; 243 244 245 /** 246 * Assuming that this object is of type (Deferred A), it takes a function of 247 * (A -> B) and returns a new deferred of type (Deferred B): 248 * 249 * (Deferred A) -> (A -> B) -> (Deferred B) 250 * 251 * @param {!Function} callbackTransform 252 * @param {?Object=} scope 253 * @return {!goog.deferred.Deferred} 254 */ 255 goog.deferred.Deferred.prototype.mapCallback = function( 256 callbackTransform, scope) { 257 return this.map(callbackTransform, goog.functions.identity, scope); 258 }; 259 260 261 /** 262 * @param {!Function} errbackTransform 263 * @param {?Object=} scope 264 * @return {!goog.deferred.Deferred} 265 */ 266 goog.deferred.Deferred.prototype.mapErrback = function( 267 errbackTransform, scope) { 268 return this.map(goog.functions.identity, errbackTransform, scope); 269 }; 270 271 272 /** 273 * @param {!Function} callbackTransform 274 * @param {!Function} errbackTransform 275 * @param {?Object=} scope 276 * @return {!goog.deferred.Deferred} 277 */ 278 goog.deferred.Deferred.prototype.map = function( 279 callbackTransform, errbackTransform, scope) { 280 scope = scope || null; 281 var deferred = new goog.deferred.Deferred(); 282 this.addCallback(function(result) { 283 var transformedResult = callbackTransform.call(scope, result); 284 deferred.callback(transformedResult); 285 }); 286 this.addErrback(function(result) { 287 var transformedResult = errbackTransform.call(scope, result); 288 deferred.errback(transformedResult); 289 }); 290 return deferred; 291 }; 292 293 294 /** 295 * Takes a deferred and binds it to a function that returns a deferred to 296 * produce a new deferred whose callbacks will receive the value from the new 297 * deferred that is produced by the function. This is perhaps better expressed 298 * in an ML-like syntax: 299 * 300 * (Deferred A) -> (A -> Deferred B) -> (Deferred B) 301 * 302 * @param {!function(*): !goog.deferred.Deferred} f that takes an argument of type 303 * A and returns a goog.deferred.Deferred whose callbacks will receive an 304 * argument of type B 305 * @param {?Object=} scope An optional scope to call function f in 306 * @return {!goog.deferred.Deferred} whose callbacks will receive an argument of 307 * type B 308 */ 309 goog.deferred.Deferred.prototype.bind = function(f, scope) { 310 var outDeferred = new goog.deferred.Deferred(); 311 this.addCallback(function(value) { 312 var resultingDeferred = f.call(scope, value); 313 resultingDeferred.addCallbacks( 314 outDeferred.callback, outDeferred.errback, outDeferred); 315 }); 316 return outDeferred; 317 }; 318 319 320 /** 321 * Adds a callback to this Deferred with a scope object that extends 322 * goog.Disposable. When the value of this Deferred is supplied, it will only 323 * call the callback if the scope has not been disposed. 324 * @param {!Function} callback 325 * @param {!goog.Disposable} scope 326 * @return {!goog.deferred.Deferred} 327 */ 328 goog.deferred.Deferred.prototype.addScopeCheckedCallback = function( 329 callback, scope) { 330 return this.addScopeCheckedCallbacks_(callback, null /* errback */, scope); 331 }; 332 333 334 /** 335 * Adds an errback to this Deferred with a scope object that extends 336 * goog.Disposable. When the value of this Deferred is supplied, it will only 337 * call the errback if the scope has not been disposed. 338 * @param {!Function} errback 339 * @param {!goog.Disposable} scope 340 * @return {!goog.deferred.Deferred} 341 */ 342 goog.deferred.Deferred.prototype.addScopeCheckedErrback = function( 343 errback, scope) { 344 return this.addScopeCheckedCallbacks_(null /* callback */, errback, scope); 345 }; 346 347 348 /** 349 * Adds a callback and errback to this Deferred with a scope object that extends 350 * goog.Disposable. When the value of this Deferred is supplied, it will only 351 * call the callback if the scope has not been disposed. 352 * @param {!Function} callback 353 * @param {!Function} errback 354 * @param {!goog.Disposable} scope 355 * @return {!goog.deferred.Deferred} 356 */ 357 goog.deferred.Deferred.prototype.addScopeCheckedCallbacks = function( 358 callback, errback, scope) { 359 return this.addScopeCheckedCallbacks_(callback, errback, scope); 360 }; 361 362 /** 363 * Adds a callback and errback to this Deferred with a scope object that extends 364 * goog.Disposable. When the value of this Deferred is supplied, it will only 365 * call the callback if the scope has not been disposed. 366 * @param {?Function} callback 367 * @param {?Function} errback 368 * @param {!goog.Disposable} scope 369 * @return {!goog.deferred.Deferred} 370 */ 371 goog.deferred.Deferred.prototype.addScopeCheckedCallbacks_ = function( 372 callback, errback, scope) { 373 var scopeCheck = function(cb) { 374 return cb ? function(value) { 375 if (!scope.isDisposed()) { 376 cb.call(scope, value) 377 } 378 } : null; 379 }; 380 return this.addCallbacks_(scopeCheck(callback), scopeCheck(errback)); 381 }; 382 383 384 /** 385 * @param {*} result 386 * @return {!goog.deferred.Deferred} 387 */ 388 goog.deferred.Deferred.success = function(result) { 389 var deferred = new goog.deferred.Deferred(); 390 deferred.callback(result); 391 return deferred; 392 }; 393 394 395 /** 396 * @param {*} result 397 * @return {!goog.deferred.Deferred} 398 */ 399 goog.deferred.Deferred.failure = function(result) { 400 var deferred = new goog.deferred.Deferred(); 401 deferred.errback(result); 402 return deferred; 403 }; 404