1 // ========================================================================== 2 // Project: SproutCore Costello - Property Observing Library 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 sc_require('ext/function'); 9 sc_require('private/observer_set'); 10 11 12 //@if(debug) 13 // When in debug mode, users can log deferred calls (such as .invokeOnce()) by 14 // setting SC.LOG_DEFERRED_CALLS. We'll declare the variable explicitly to make 15 // life easier for people who want to enter it inside consoles that auto- 16 // complete. 17 if (!SC.LOG_DEFERRED_CALLS) SC.LOG_DEFERRED_CALLS = false; 18 //@endif 19 20 /** 21 @class 22 23 The run loop provides a universal system for coordinating events within 24 your application. The run loop processes timers as well as pending 25 observer notifications within your application. 26 27 To use a RunLoop within your application, you should make sure your event 28 handlers always begin and end with SC.RunLoop.begin() and SC.RunLoop.end() 29 30 The RunLoop is important because bindings do not fire until the end of 31 your run loop is reached. This improves the performance of your 32 application. 33 34 Example: 35 36 This is how you could write your mouseup handler in jQuery: 37 38 $('#okButton').on('click', function () { 39 SC.run(function () { 40 41 // handle click event... 42 43 }); // allows bindings to trigger... 44 }); 45 46 @extends SC.Object 47 @since SproutCore 1.0 48 */ 49 SC.RunLoop = SC.Object.extend( 50 /** @scope SC.RunLoop.prototype */ { 51 52 /** 53 Call this method whenver you begin executing code. 54 55 This is typically invoked automatically for you from event handlers and 56 the timeout handler. If you call setTimeout() or setInterval() yourself, 57 you may need to invoke this yourself. 58 59 @returns {SC.RunLoop} receiver 60 */ 61 beginRunLoop: function () { 62 this._start = new Date().getTime(); // can't use Date.now() in runtime 63 64 //@if(debug) 65 if (SC.LOG_OBSERVERS) { 66 SC.Logger.log("-- SC.RunLoop.beginRunLoop at %@".fmt(this._start)); 67 } 68 //@endif 69 70 this._runLoopInProgress = YES; 71 this._flushinvokeNextQueue(); 72 return this; 73 }, 74 75 /** 76 YES when a run loop is in progress 77 78 @property 79 @type Boolean 80 */ 81 isRunLoopInProgress: function () { 82 return this._runLoopInProgress; 83 }.property(), 84 85 /** 86 Call this method whenever you are done executing code. 87 88 This is typically invoked automatically for you from event handlers and 89 the timeout handler. If you call setTimeout() or setInterval() yourself 90 you may need to invoke this yourself. 91 92 @returns {SC.RunLoop} receiver 93 */ 94 endRunLoop: function () { 95 // at the end of a runloop, flush all the delayed actions we may have 96 // stored up. Note that if any of these queues actually run, we will 97 // step through all of them again. This way any changes get flushed 98 // out completely. 99 100 //@if(debug) 101 if (SC.LOG_OBSERVERS) { 102 SC.Logger.log("-- SC.RunLoop.endRunLoop ~ flushing application queues"); 103 } 104 //@endif 105 106 this.flushAllPending(); 107 108 this._start = null; 109 110 //@if(debug) 111 if (SC.LOG_OBSERVERS) { 112 SC.Logger.log("-- SC.RunLoop.endRunLoop ~ End"); 113 } 114 //@endif 115 116 SC.RunLoop.lastRunLoopEnd = Date.now(); 117 this._runLoopInProgress = NO; 118 119 // If there are members in the invokeNextQueue, be sure to schedule another 120 // run of the run loop. 121 var queue = this._invokeNextQueue; 122 if (queue && queue.getMembers().length) { 123 this._invokeNextTimeout = this.scheduleRunLoop(SC.RunLoop.lastRunLoopEnd); 124 } 125 126 return this; 127 }, 128 129 /** 130 Repeatedly flushes all bindings, observers, and other queued functions until all queues are empty. 131 */ 132 flushAllPending: function () { 133 var didChange; 134 135 do { 136 didChange = this.flushApplicationQueues(); 137 if (!didChange) didChange = this._flushinvokeLastQueue(); 138 } while (didChange); 139 }, 140 141 142 /** 143 Invokes the passed target/method pair once at the end of the runloop. 144 You can call this method as many times as you like and the method will 145 only be invoked once. 146 147 Usually you will not call this method directly but use invokeOnce() 148 defined on SC.Object. 149 150 Note that in development mode only, the object and method that call this 151 method will be recorded, for help in debugging scheduled code. 152 153 @param {Object} target 154 @param {Function} method 155 @returns {SC.RunLoop} receiver 156 */ 157 invokeOnce: function (target, method) { 158 //@if(debug) 159 // Calling invokeOnce outside of a run loop causes problems when coupled 160 // with time dependent methods invokeNext and invokeLater, because the 161 // time dependent methods execute at the start of the next run of the run 162 // loop and may fire before the invokeOnce code in this case. 163 var isRunLoopInProgress = this.get('isRunLoopInProgress'); 164 if (!isRunLoopInProgress) { 165 SC.warn("Developer Warning: invokeOnce called outside of the run loop, which may cause unexpected problems. Check the stack trace below for what triggered this, and see http://blog.sproutcore.com/1-10-upgrade-invokefoo/ for more."); 166 console.trace(); 167 } 168 //@endif 169 // normalize 170 if (method === undefined) { 171 method = target; 172 target = this; 173 } 174 175 var deferredCallLoggingInfo; // Used only in debug mode 176 177 //@if(debug) 178 // When in debug mode, SC.Object#invokeOnce() will pass in the originating 179 // method, target, and stack. That way, we'll record the interesting parts 180 // rather than having most of these calls seemingly coming from 181 // SC.Object#invokeOnce(). 182 // 183 // If it was not specified, we'll record the originating function ourselves. 184 var shouldLog = SC.LOG_DEFERRED_CALLS; 185 if (shouldLog) { 186 var originatingTarget = arguments[2], 187 originatingMethod = arguments[3], 188 originatingStack = arguments[4]; 189 190 if (!originatingTarget) originatingTarget = null; // More obvious when debugging 191 if (!originatingMethod) { 192 originatingStack = SC._getRecentStack(); 193 originatingMethod = originatingStack[0]; 194 } 195 196 deferredCallLoggingInfo = { 197 originatingTarget: originatingTarget, 198 originatingMethod: originatingMethod, 199 originatingStack: originatingStack 200 }; 201 } 202 //@endif 203 204 if (typeof method === "string") method = target[method]; 205 if (!this._invokeQueue) this._invokeQueue = SC.ObserverSet.create(); 206 if (method) this._invokeQueue.add(target, method, undefined, deferredCallLoggingInfo); 207 return this; 208 }, 209 210 /** 211 Invokes the passed target/method pair at the very end of the run loop, 212 once all other delayed invoke queues have been flushed. Use this to 213 schedule cleanup methods at the end of the run loop once all other work 214 (including rendering) has finished. 215 216 If you call this with the same target/method pair multiple times it will 217 only invoke the pair only once at the end of the runloop. 218 219 Usually you will not call this method directly but use invokeLast() 220 defined on SC.Object. 221 222 Note that in development mode only, the object and method that call this 223 method will be recorded, for help in debugging scheduled code. 224 225 @param {Object} target 226 @param {Function} method 227 @returns {SC.RunLoop} receiver 228 */ 229 invokeLast: function (target, method) { 230 //@if(debug) 231 // Calling invokeLast outside of a run loop causes problems when coupled 232 // with time dependent methods invokeNext and invokeLater, because the 233 // time dependent methods execute at the start of the next run of the run 234 // loop and may fire before the invokeLast code in this case. 235 var isRunLoopInProgress = this.get('isRunLoopInProgress'); 236 if (!isRunLoopInProgress) { 237 SC.warn("Developer Warning: invokeLast called outside of the run loop, which may cause unexpected problems. Check the stack trace below for what triggered this, and see http://blog.sproutcore.com/1-10-upgrade-invokefoo/ for more."); 238 console.trace(); 239 } 240 //@endif 241 242 // normalize 243 if (method === undefined) { 244 method = target; 245 target = this; 246 } 247 248 var deferredCallLoggingInfo; // Used only in debug mode 249 250 //@if(debug) 251 // When in debug mode, SC.Object#invokeOnce() will pass in the originating 252 // method, target, and stack. That way, we'll record the interesting parts 253 // rather than having most of these calls seemingly coming from 254 // SC.Object#invokeOnce(). 255 // 256 // If it was not specified, we'll record the originating function ourselves. 257 var shouldLog = SC.LOG_DEFERRED_CALLS; 258 if (shouldLog) { 259 var originatingTarget = arguments[2], 260 originatingMethod = arguments[3], 261 originatingStack = arguments[4]; 262 263 if (!originatingTarget) originatingTarget = null; // More obvious when debugging 264 if (!originatingMethod) { 265 originatingStack = SC._getRecentStack(); 266 originatingMethod = originatingStack[0]; 267 } 268 269 deferredCallLoggingInfo = { 270 originatingTarget: originatingTarget, 271 originatingMethod: originatingMethod, 272 originatingStack: originatingStack 273 }; 274 } 275 //@endif 276 277 278 if (typeof method === "string") method = target[method]; 279 if (!this._invokeLastQueue) this._invokeLastQueue = SC.ObserverSet.create(); 280 this._invokeLastQueue.add(target, method, undefined, deferredCallLoggingInfo); 281 return this; 282 }, 283 284 /** 285 Invokes the passed target/method pair once at the beginning of the next 286 run of the run loop, before any other methods (including events) are 287 processed. Use this to defer painting to make views more responsive. 288 289 If you call this with the same target/method pair multiple times it will 290 only invoke the pair only once at the beginning of the next runloop. 291 292 Usually you will not call this method directly but use invokeNext() 293 defined on SC.Object. 294 295 @param {Object} target 296 @param {Function} method 297 @returns {SC.RunLoop} receiver 298 */ 299 invokeNext: function (target, method) { 300 // normalize 301 if (method === undefined) { 302 method = target; 303 target = this; 304 } 305 306 if (typeof method === "string") method = target[method]; 307 if (!this._invokeNextQueue) this._invokeNextQueue = SC.ObserverSet.create(); 308 this._invokeNextQueue.add(target, method); 309 return this; 310 }, 311 312 /** 313 Executes any pending events at the end of the run loop. This method is 314 called automatically at the end of a run loop to flush any pending 315 queue changes. 316 317 The default method will invoke any one time methods and then sync any 318 bindings that might have changed. You can override this method in a 319 subclass if you like to handle additional cleanup. 320 321 This method must return YES if it found any items pending in its queues 322 to take action on. endRunLoop will invoke this method repeatedly until 323 the method returns NO. This way if any if your final executing code 324 causes additional queues to trigger, then can be flushed again. 325 326 @returns {Boolean} YES if items were found in any queue, NO otherwise 327 */ 328 flushApplicationQueues: function () { 329 var hadContent = NO, 330 // execute any methods in the invokeQueue. 331 queue = this._invokeQueue; 332 if (queue && queue.getMembers().length) { 333 this._invokeQueue = null; // reset so that a new queue will be created 334 hadContent = YES; // needs to execute again 335 queue.invokeMethods(); 336 } 337 338 // flush any pending changed bindings. This could actually trigger a 339 // lot of code to execute. 340 return SC.Binding.flushPendingChanges() || hadContent; 341 }, 342 343 _flushinvokeLastQueue: function () { 344 var queue = this._invokeLastQueue, hadContent = NO; 345 if (queue && queue.getMembers().length) { 346 this._invokeLastQueue = null; // reset queue. 347 hadContent = YES; // has targets! 348 if (hadContent) queue.invokeMethods(); 349 } 350 return hadContent; 351 }, 352 353 _flushinvokeNextQueue: function () { 354 var queue = this._invokeNextQueue, hadContent = NO; 355 if (queue && queue.getMembers().length) { 356 this._invokeNextQueue = null; // reset queue. 357 hadContent = YES; // has targets! 358 if (hadContent) queue.invokeMethods(); 359 } 360 return hadContent; 361 }, 362 363 /** @private 364 Schedules the run loop to run at the given time. If the run loop is 365 already scheduled to run earlier nothing will change, but if the run loop 366 is not scheduled or it is scheduled later, then it will be rescheduled 367 to the value of nextTimeoutAt. 368 369 @returns timeoutID {Number} The ID of the timeout to start the next run of the run loop 370 */ 371 scheduleRunLoop: function (nextTimeoutAt) { 372 /*jshint eqnull:true*/ 373 // If there is no run loop scheduled or if the scheduled run loop is later, reschedule. 374 if (this._timeoutAt == null || this._timeoutAt > nextTimeoutAt) { 375 // clear existing... 376 if (this._timeout) { clearTimeout(this._timeout); } 377 378 // reschedule 379 var delay = Math.max(0, nextTimeoutAt - Date.now()); 380 this._timeout = setTimeout(this._timeoutDidFire, delay); 381 this._timeoutAt = nextTimeoutAt; 382 } 383 384 return this._timeout; 385 }, 386 387 /** @private 388 Invoked when a timeout actually fires. Simply cleanup, then begin and end 389 a runloop. Note that this function will be called with 'this' set to the 390 global context, hence the need to lookup the current run loop. 391 */ 392 _timeoutDidFire: function () { 393 var rl = SC.RunLoop.currentRunLoop; 394 rl._timeout = rl._timeoutAt = rl._invokeNextTimeout = null; // cleanup 395 SC.run(); // begin/end runloop to trigger timers. 396 }, 397 398 /** @private Unschedule the run loop that is scheduled for the given timeoutID */ 399 unscheduleRunLoop: function () { 400 // Don't unschedule if the timeout is shared with an invokeNext timeout. 401 if (!this._invokeNextTimeout) { 402 clearTimeout(this._timeout); 403 this._timeout = this._timeoutAt = null; // cleanup 404 } 405 } 406 407 }); 408 409 410 //@if(debug) 411 /** 412 Will return the recent stack as a hash with numerical keys, for nice output 413 in some browsers’ debuggers. The “recent” stack is capped at 6 entries. 414 415 This is used by, amongst other places, SC.LOG_DEFERRED_CALLS. 416 417 @returns {Hash} 418 */ 419 SC._getRecentStack = function () { 420 var currentFunction = arguments.callee.caller, 421 i = 0, 422 stack = {}, 423 first = YES, 424 functionName; 425 426 while (currentFunction && i < 10) { 427 // Skip ourselves! 428 if (first) { 429 first = NO; 430 } else { 431 functionName = currentFunction.displayName || currentFunction.toString().substring(0, 40); 432 stack[i++] = functionName; 433 } 434 currentFunction = currentFunction.caller; 435 } 436 437 return stack; 438 }; 439 //@endif 440 441 442 443 /** 444 The current run loop. This is created automatically the first time you 445 call begin(). 446 447 @type SC.RunLoop 448 */ 449 SC.RunLoop.currentRunLoop = null; 450 451 /** 452 The default RunLoop class. If you choose to extend the RunLoop, you can 453 set this property to make sure your class is used instead. 454 455 @type Class 456 */ 457 SC.RunLoop.runLoopClass = SC.RunLoop; 458 459 /** 460 Begins a new run loop on the currentRunLoop. If you are already in a 461 runloop, this method has no effect. 462 463 @returns {SC.RunLoop} receiver 464 */ 465 SC.RunLoop.begin = function () { 466 var runLoop = this.currentRunLoop; 467 if (!runLoop) runLoop = this.currentRunLoop = this.runLoopClass.create(); 468 runLoop.beginRunLoop(); 469 return this; 470 }; 471 472 /** 473 Ends the run loop on the currentRunLoop. This will deliver any final 474 pending notifications and schedule any additional necessary cleanup. 475 476 @returns {SC.RunLoop} receiver 477 */ 478 SC.RunLoop.end = function () { 479 var runLoop = this.currentRunLoop; 480 if (!runLoop) { 481 throw new Error("SC.RunLoop.end() called outside of a runloop!"); 482 } 483 runLoop.endRunLoop(); 484 return this; 485 }; 486 487 488 /** 489 Call this to kill the current run loop--stopping all propagation of bindings 490 and observers and clearing all timers. 491 492 This is useful if you are popping up an error catcher: you need a run loop 493 for the error catcher, but you don't want the app itself to continue 494 running. 495 */ 496 SC.RunLoop.kill = function () { 497 this.currentRunLoop = this.runLoopClass.create(); 498 return this; 499 }; 500 501 /** 502 Returns YES when a run loop is in progress 503 504 @return {Boolean} 505 */ 506 SC.RunLoop.isRunLoopInProgress = function () { 507 if (this.currentRunLoop) return this.currentRunLoop.get('isRunLoopInProgress'); 508 return NO; 509 }; 510 511 /** 512 Executes a passed function in the context of a run loop. If called outside a 513 runloop, starts and ends one. If called inside an existing runloop, is 514 simply executes the function unless you force it to create a nested runloop. 515 516 If an exception is thrown during execution, we give an error catcher the 517 opportunity to handle it before allowing the exception to bubble again. 518 519 @param {Function} callback callback to execute 520 @param {Object} target context for callback 521 @param {Boolean} if YES, starts/ends a new runloop even if one is already running 522 */ 523 SC.run = function (callback, target, forceNested) { 524 var alreadyRunning = SC.RunLoop.isRunLoopInProgress(); 525 526 // Only use a try/catch block if we have an ExceptionHandler 527 // since in some browsers try/catch causes a loss of the backtrace 528 if (SC.ExceptionHandler && SC.ExceptionHandler.enabled) { 529 try { 530 if (forceNested || !alreadyRunning) SC.RunLoop.begin(); 531 if (callback) callback.call(target); 532 if (forceNested || !alreadyRunning) SC.RunLoop.end(); 533 } catch (e) { 534 var handled = SC.ExceptionHandler.handleException(e); 535 536 // If the exception was not handled, throw it again so the browser 537 // can deal with it (and potentially use it for debugging). 538 // (We don't throw it in IE because the user will see two errors) 539 if (!handled && !SC.browser.isIE) { 540 throw e; 541 } 542 } 543 } else { 544 if (forceNested || !alreadyRunning) SC.RunLoop.begin(); 545 if (callback) callback.call(target); 546 if (forceNested || !alreadyRunning) SC.RunLoop.end(); 547 } 548 }; 549 550 /** 551 Wraps the passed function in code that ensures a run loop will 552 surround it when run. 553 */ 554 SC.RunLoop.wrapFunction = function (func) { 555 var ret = function () { 556 var alreadyRunning = SC.RunLoop.isRunLoopInProgress(); 557 if (!alreadyRunning) SC.RunLoop.begin(); 558 var ret = arguments.callee.wrapped.apply(this, arguments); 559 if (!alreadyRunning) SC.RunLoop.end(); 560 return ret; 561 }; 562 ret.wrapped = func; 563 return ret; 564 }; 565