Hi,
I have recently dedicated some time to learn what the Ember Run Loop, Backburner, was doing under the hood, I think if you want to understand why you should wrap your own logic within Ember.run() you should definitely read the source code.
So I will explain step-by-step what happens when you do Ember.run.
Ember creates a new instance of the Backburner object.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
backburner = new Backburner( | |
['syc', 'actions', 'destroy'], | |
{ | |
GUID_KEY: GUID_KEY, | |
sync: { | |
before: beginPropertyChanges, | |
after: endPropertyChanges | |
}, | |
defaultQueue: 'actions', | |
onBegin: onBegin, | |
onEnd: onEnd, | |
onErrorTarget: Ember, | |
onErrorMethod: 'onerror' | |
} | |
); |
new Backburner(queueNames, options)
Stores queueNames and options, if there is a defaultQueue it sets the first queue in the Array to be it.
It also creates properties such as instanceStack, and private ones, _debouncees, _throttlers and _timers and returns itself.
GUID_KEY
It helps to find a queue, I will be talking about this on another blogpost.
onBegin and onEnd callbacks.
Internally backburner has a begin method that calls the onBegin callback, if it exists, when it is done creating instances of deferredActionQueues with the queueNames ember has passed in as arguments.
It also has an end method defined that as you can guess calls the onEnd callback, if it exists, after it is done flushing the current instance queue.
Ember uses those two callbacks to set the currentRunLoop property.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function onBegin(current) { | |
run.currentRunLoop = current; | |
} | |
function onEnd(current, next) { | |
run.currentRunLoop = next; | |
} |
The onBegin callback takes two arguments, in this case Ember is only interested in the currentInstance, which are currentInstance and previousInstance.
Ember defines and passes a onErrorTarget and onErrorMethod which backburner will use, if it exists, when it tries to flush your queues so that you can handle any errors.
It also passes some specific config for the sync queue as you have seen. So before backburners attemps to flush items in a queue, sync in this case, if the before callback exists it will apply it. The same thing happens for the after callback, it gets called once it is done flushing a queue.
Then Ember defines the run method:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default run; | |
function run() { | |
return backburner.run.apply(backburner, arguments) | |
} |
When you call Ember.run you’re actually applying the run method in the backburner context and passing whatever arguments you have passed in. The apply here is similar to call the only difference is that apply allows you to pass your arguments as in an array type and call you neeed to pass every single argument comma-separated.
So far so good right?
Ok, now that you know all the setup let’s find out what happens when you do:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Ember.run(function() { | |
console.log('true'); | |
}); |
The Backburner’s run method takes a target, a method and options as arguments. If the onError callback exists it will assign that to a local variable. It gets this information from the options property passed in when creating a new instance of the Backburner object.
Then it calls its internal begin method which is responsible for checking wether or not a previousInstance exists and if it does it pushes it onto the instanceStack list and then creates a completely new instance of DeferredActionQueues, which will talk about later, passing the queueNames and options and assigns it to be its currentInstance and the finally it calls the onBegin callback if it exists.
Let’s have a look at the DeferredActionQueues object.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function DeferredActionQueues(queueNames, options) { | |
var queues = this.queues = Object.create(null); | |
this.queueNames = queueNames || []; | |
this.options = options; | |
each(queueNames, function(queueName){ | |
queues[queueName] = new Queue(queueName, options[queueName], options); | |
}); | |
} |
There is more to it and you can have a look at the source code here.
Got to love closures!! haha
Alright, as you can see it instantiates an “empty” object and assigns that to the queues local variable and it creates three properties, queues, queueNames and options. It then iterates over each key in queueNames array and creates uses its queueName value as the key to access that queue later on in the queues local variable, that points to an “empty” object, and the value is an instance of the Queue object. It finally returns and instance of its own which would be something like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
DeferredActionQueues {queues: Object, queueNames: Array[3], options: Object} | |
// expanded | |
DeferredActionQueues { | |
queues: { | |
"actions": Queue, | |
"sync": Queue, | |
"destroy": Queue | |
}, | |
queueNames: Array[ "actions", "sync", "destroy" ], | |
options: Object | |
} |
Pretty good right? I hope you’re enjoying reading this because I enjoyed writing it as well as learning it.
Let’s have a look at the Queue object.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Queue(name, options, globalOptions) { | |
this.name = name; | |
this.globalOptions = globalOptions; | |
this.options = options; | |
this._queue = []; | |
this.targetQueues = Object.create(null); | |
this._queueBeingFlushed = undefined; | |
} |
I guess that’s very straightforward and I do not have to explain that.
One of the key things to know is that it creates a new instance of the DeferredActionQueue passing the queueNames and options every time you call Ember.run. As you can see it here:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Ember.run.currentRunLoop | |
// undefined | |
Ember.run(function() { | |
console.log(Ember.run.currentRunLoop); | |
}); | |
// DeferredActionQueues {queues: Object, queueNames: Array[6], options: Object, schedule: function, invoke: function…} | |
// You can run that in your browser and have a better look at it. |
Let’s go back to the Backburner run method:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
run: function(target, method, /* options, arguments */) { | |
var onError = getOnError(this.options); | |
this.begin(); | |
if (!method) { | |
method = target; | |
target = null; | |
} | |
if (isString(method)){ | |
method = target[method]; | |
} | |
var args = slice.call(arguments, 2); | |
var didFinally = false; | |
if (onError){ | |
try { | |
return method.apply(target, args); | |
} catch(error) { | |
onError(error); | |
} finally { | |
if (!didFinally) { | |
didFinally = true; | |
this.end(); | |
} | |
} | |
} else { | |
try { | |
return method.apply(target, args); | |
} finally { | |
if (!didFinally) { | |
didFinally = true; | |
this.end(); | |
} | |
} | |
} | |
} |
I started explaining a little bit what it does above but I had to explain something else. So It checks whether or not method is present and if it isn’t it assigns target to method and target to be null. This is because you may have passed a function instead of a target, an object followed by a string which is the method to be called, so that later on when it applies that function it won’t error.
If onError is present it then applies the method and if an error occurs it will call the onError callback and pass whatever the error was. Finally it calls the end internal method.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
end: function() { | |
var options = this.options; | |
var onEnd = options && options.ondEnd; | |
var currentInstance = this.currentInstance; | |
var nextInstance = null; | |
var finallyAlreadyCalled = false; | |
try { | |
currentInstance.flush(); | |
} finally { | |
if (!finallyAlreadyCalled) { | |
finallyAlreadyCalled = true; | |
this.currentInstance = null; | |
if (this.instanceStack.length) { | |
nextInstance = this.instanceStack.pop(); | |
this.currentInstance = nextInstance; | |
} | |
if (onEnd) { | |
onEnd(currentInstance, nextInstance) | |
} | |
} | |
} | |
} |
As you can see it defines and sets some instance variables and it tries to flush the currentInstance, which is an instance of the DeferredActionQueues, and finally it assigns currentInstance to null and checks wether or not the instanceStack is 0. If it isn’t it then assigns the nextInstance, pops the last queue in the instanceStack, and the n assigns the currentInstance property to be this nextInstance. Finally it calls the onEnd callback passing it the currentInstance and the nextInstance.
Whenever the end backbone internal method calls currentInstance.flush() it sends a message, flush, to the the instance of the DeferredActionQueues object.
So what does flush do? Let’s have a look:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
flush: function() { | |
var queues = this.queues; | |
var queueNames = this.queueNames; | |
var queueName, queue, queueItems, priorQueueNameIndex; | |
var queueNameIndex = 0; | |
var numberOfQueues = queueNames.length; | |
var options = this.options; | |
while(queueNameIndex < numberOfQueues) { | |
queueName = queueNames[queueNameIndex]; | |
queue = queues[queueName]; | |
var numberOfQueueItems = queue._queue.length; | |
if (numberOfQueueItems === 0) { | |
queueNameIndex++; | |
} else { | |
queue.flush(false, /* async */); | |
queueNameIndex = 0; | |
} | |
} | |
} |
It loops over the numberOfQueues and checks whether numberOfQueueItems is 0 and if it is the increments queueNameIndex which means it will go onto the next queue in this case the actions queue. This is very important because if you schedule something in the first queue inside the last queue but before you do that you schedule something else in the ‘actions’ queue it will flush what’s scheduled in the first queue and the the rest.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Ember.run(function() { | |
Ember.run.schedule('destroy', function() { | |
Ember.run.schedule('actions', function() { console.log('actions'); }); | |
Ember.run.schedule('sync', function() { console.log('sync'); }); | |
}); | |
}); | |
// sync | |
// actions | |
// WHILE LOOP ORDER | |
// Queue {name: "sync", globalOptions: Object, options: undefined, _queue: Array[0], targetQueues: Object…} | |
// numberOfQueueItems is 0 | |
// Queue {name: "actions", globalOptions: Object, options: undefined, _queue: Array[0], targetQueues: Object…} | |
// numberOfQueueItems is 0 | |
// Queue {name: "destroy", globalOptions: Object, options: undefined, _queue: Array[4], targetQueues: Object…} | |
// numberOfQueueItems is 4 | |
// Queue {name: "sync", globalOptions: Object, options: undefined, _queue: Array[4], targetQueues: Object…} | |
// numberOfQueueItems is 4 | |
// Queue {name: "sync", globalOptions: Object, options: undefined, _queue: Array[0], targetQueues: Object…} | |
// numberOfQueueItems is 0 | |
// Queue {name: "actions", globalOptions: Object, options: undefined, _queue: Array[4], targetQueues: Object…} | |
// numberOfQueueItems is 4 | |
// Queue {name: "sync", globalOptions: Object, options: undefined, _queue: Array[0], targetQueues: Object…} | |
// numberOfQueueItems is 0 | |
// Queue {name: "actions", globalOptions: Object, options: undefined, _queue: Array[0], targetQueues: Object…} | |
// numberOfQueueItems is 0 | |
// Queue {name: "destroy", globalOptions: Object, options: undefined, _queue: Array[0], targetQueues: Object…} | |
// numberOfQueueItems is 0 |
I logged out the order that instances of Queue get flushed so that you can have a better understanding why it assigns queueNameIndex to 0 in order to start over again.
Finally it flushes everything that you have schedule in the queue. So let’s recap, DeferredActionQueues keeps track of instances of the queues you have passed in. The Queue object is the actual queue that stores everything you have scheduled. Let’s have a look at the Queue#flush.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
flush: { | |
var queue = this._queue; | |
var length = queue.length; | |
if (length == 0) { | |
return; | |
} | |
var globalOptions = this.globalOptions; | |
var options = this.options; | |
var before = options && options.before; | |
var after = options && options.after; | |
var onError = globalOptions.onError || (globalOptions.onErrorTarget && | |
globalOptions.onErrorTarget[globalOptions.onErrorMethod]); | |
var target, method, args, errorRecordedForStack; | |
var invoke = onError ? this.invokeWithOnError : this.invoke; | |
this.targetQueues = Object.create(null); | |
var queueItems = this._queueBeingFlushed = this._queue.slice(); | |
this._queue = []; | |
if (before) { | |
before(); | |
} | |
for (var i = 0; i < legnth; i += 4) { | |
target = queueItems[i]; | |
method = queueItems[i+1]; | |
args = queueItems[i+2]; | |
errorRecordedForStack = queueItems[i+3]; | |
if (isString(method)) { | |
invoke(target, method, args, onError, errorRecordedForStack); | |
} | |
} | |
if (after) { | |
after(); | |
} | |
this._queueBeingFlushed = undefined; | |
if (sync !== false && this._queue.length > 0) { | |
// check if new items have been added | |
this.flush(true); | |
} | |
} |
This method is responsible for flushing everything that has been scheduled in the defined queues. It is very smart and once it is done “flushing” it then checks whether or not new items have been scheduled in a given queue by checking if sync !== false and queue.length is greater than 0.
So let’s have a look how they are testing this.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
test("Queue#flush should be recursive if new items are added", function() { | |
expect(2); | |
var bb = new Backburner(['one']); | |
var count = 0; | |
bb.run(function(){ | |
function increment() { | |
if (++count < 3) { | |
bb.schedule('one', increment); | |
} | |
if (count === 3) { | |
bb.schedule('one', increment); | |
} | |
} | |
increment(); | |
equal(count, 1, 'should not have run yet'); | |
bb.currentInstance.queues.one.flush(); | |
equal(count, 4, 'should have run all scheduled methods, even ones added during flush'); | |
}); | |
}); |
NOTE: In the Queue#flush method before it invokes it checks wether or not the method is truthy this is because when you cancel a scheduled method and that method is in the queue that is being flushed it does not mess with that array all it does is nullify the method so that it won’t run it. I will write a blog post about cancelling scheduled methods.
As you can see during flush it assigns queueItems and this._queueBeingFlushed to a clone of this._queue and this._queue to an empty array. It then invokes (i.e executes all scheduled methods) what was scheduled in the queue. It runs the after callback if it was defined assigns this._queueBeingFlushed to undefined and in the test example when it was done flushing the queue it actually scheduled more methods in the queue, therefore this._queue now is greater than 0 and undefined is !== false so it will rerun, apply flush again, the queue passing true as its argument so that when it is done flushing again if new items have been scheduled it will do the same thing until this._queue is not greater than 0. This is quite a lot to take in feel free to ask me any question below.
Finally the invoke method, as you can see if onError callback exists it will invokeWithOnError otherwise it will just invoke. If something goes wrong whilst it is flushing a queue it will call this callback passing the error message onto it.
Queue#invokeWithOnError
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
invokeWithOnError: function(target, method, args, onError, errorRecordedForStack) { | |
try { | |
if (args && args.length > 0) { | |
method.apply(target, args); | |
} else { | |
method.call(target); | |
} | |
} catch(error) { | |
onError(error, errorRecordedForStack); | |
} | |
} |
Queue#invoke
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
invoke: function(target, method, args, _, _errorRecordedForStack) { | |
if (args && args.length > 0) { | |
method.apply(target, args); | |
} else { | |
method.call(target); | |
} | |
} |
Alright, there you have it. I know it is quite a long post but I tried to cover as much as I could and I still think there are some stuff missing. Please do comment below if you think I should improve it in some way.
I hope you have enjoyed reading it, I will be writing more about Backburner so keep checking my blog.