can-queues
A light weight queue system for scheduling tasks.
Object
The can-queues
package exports an object with queue constructors, shared instances, and helpers
methods. The following describes the properties of the can-queues
object:
{
Queue, // The Queue type constructor
PriorityQueue, // The PriorityQueue type constructor
CompletionQueue, // The CompletionQueue type constructor
notifyQueue, // A Queue used to tell objects that
// derive a value that they should be updated.
deriveQueue, // A PriorityQueue used update values.
domUIQueue, // A CompletionQueue used for updating the DOM or other
// UI after state has settled, but before user tasks
mutateQueue, // A Queue used to register tasks that might
// update other values.
batch: {
start, // A function used to prevent the automatic flushing
// of the NOTIFY_QUEUE.
stop // A function used to begin flushing the NOTIFY_QUEUE.
},
enqueueByQueue, // A helper function used to queue a bunch of tasks.
stack, // A function that returns an array of all the queue
// tasks up to this point of a flush for debugging.
// Returns an empty array in production.
logStack, // A function that logs the result of `this.stack()`.
// Doesn't do anything in production.
log // Logs tasks as they are enqueued and/or run.
}
Use Cases
can-queues
is used by CanJS to order task execution. A task
is simply
the calling of a function, usually a callback function within an event binding.
There are two main reasons tasks are ordered:
- performance - It can be beneficial to order some tasks to happen at the same time. For example, those that change the DOM. CanJS performs all DOM mutations together in the DOMUI queue. This helps avoid expensive browser layout reflows. Read On Layout & Web Performance for background on why browser layout reflows hurts performance.
- determinism - Ordering tasks can provide assurances about the state of an application at a particular point in time.
Let's explore the determinism use case a bit more with a
small example to shows what a lack of determinism would look like. In the following example,
person
observable is created, with two observations that derive values from its
values:
const person = observe( { name: "Fran", age: 15 } );
const info = new Observation( () => {
return person.name + " is " + person.age;
} );
const canVote = new Observation( () => {
return person.age >= 18;
} );
Now let's say we listened to when info
and canVote
changed and used the other value
to print a message:
info.on( function( newInfo ) {
console.log( "info: " + newInfo + ", canVote:" + canVote.get() );
} );
canVote.on( function( newCanVote ) {
console.log( "canVote: " + newCanVote + ", info: " + info.get() );
} );
If person.age
is set to 19
, info
and canVote
are each updated and their event handlers dispatched. If the updates to info
and canVote
immediately
dispatched their events, you would see something like:
person.age = 19;
// console.log("info: Fran is 19, canVote: false")
// console.log("info: Fran is 19, canVote: true")
Notice that canVote
is false
. This is because canVote
has not been updated
yet. CanJS avoids this problem by scheduling callbacks in queues. All "user" events
like the ones above (registered with .on()
) happen last in the mutateQueue. info
and canVote
update their values in the deriveQueue. info
and canVote
are notified of
the age
change in the notifyQueue queue.
In CanJS, all user event handlers are able to read other values and have those values reflect all prior state changes (mutations).
Use
Use can-queues
enqueue and run tasks in one of the following queues:
- notifyQueue - Tasks that notify observables "deriving" observables that a source value has changed.
- deriveQueue - Tasks that update the value of a "deriving" observable.
- domUIQueue - Tasks that update the DOM.
- mutateQueue - Tasks that might cause other mutations that add tasks to one of the previous queues.
For example, the following enqueues and runs a console.log("Hello World")
in the mutateQueue
:
import queues from "can-queues";
queues.batch.start();
queues.mutateQueue.enqueue( console.log, console, [ "say hi" ] );
queues.batch.stop();
A task
in can-queues
is just:
- a
function
(console.log
), - its
this
(console
), - and its arguments
["say hi"]
batch.start begins collecting tasks and batch.stop flushes any
enqueued tasks. batch.start and batch.stop can be nested and only fire
when the number of queues.batch.start()
calls equals the number of queues.batch.stop()
calls.
queues.batch.start();
queues.batch.start();
queues.batch.start();
queues.mutateQueue.enqueue( console.log, console, [ "say hi" ] );
queues.batch.stop();
queues.batch.stop();
queues.batch.stop(); //-> logs "say hi"
The enqueueByQueue helper can enqueue multiple tasks and starts and stops a batch. For example, the following will log "running a task" in every queue.
queues.enqueueByQueue( {
notify: [ console.log ],
derive: [ console.log ],
domUI: [ console.log ],
mutate: [ console.log ]
}, console, [ "running a task" ] );
When enqueuing tasks, to assist with debugging, PLEASE:
- Give your functions useful names:
//!steal-remove-start Object.defineProperty( this.update, "name", { value: canReflect.getName( this ) + ".update" } ); //!steal-remove-end
- Use the
reasonLog
(described in enqueueByQueue's documentation):queues.notifyQueue.enqueue( this.update, this, [], null //!steal-remove-start /* jshint laxcomma: true */ , [ canReflect.getName( context ), "changed" ] /* jshint laxcomma: false */ //!steal-remove-end );
CanJS is much easier to debug if queued tasks can be easily traced to their source in meaningful ways.
Understanding task order
This section describes the order in which tasks run. Tasks run in a particular order within a queue, determined by the type of queue:
- Basic Queue - Run in first-in-first out.
- CompletionQueue - Run in first-in-first out, but each task must complete completely before the next task is run.
- PriorityQueue - Like a
CompletionQueue
, but run tasks in order of their priority.
The queues themselves run in a particular order. can-queues
runs the task queues in the following order:
- notifyQueue - Tasks that notify observables "deriving" observables that a source value has changed.
- deriveQueue - Tasks that update the value of a "deriving" observable.
- domUIQueue - Tasks that update the DOM.
- mutateQueue - Tasks that might cause other mutations that add tasks to one of the previous queues.
This means that once the notifyQueue
has completed, the deriveQueue
's tasks will start and so
on. Lets see a brief example where we:
- Create a person,
- Derive an
info
value from it, - Update the DOM with the value of
info
, - Listen to changes in
age
and log the new value.
const person = new observe.Object( { name: "Fran", age: 15 } );
const info = new Observation( function updateInfo() {
return person.name + " is " + person.age;
} );
const frag = stache( "<h2>{{info}}</h2>" )( { info: info } );
document.body.appendChild( frag );
person.on( "age", function logAgeChanged( newVal ) {
console.log( "Age changed to ", newVal );
} );
person.age = 22;
When person.age
changes, this will:
- Enqueue a notify task (
onAgeChange
) that notifiesinfo
that one of its source values is changing. - Enqueue a mutate task (
logAgeChanged
). - Run
info
'sonAgeChange
task. This then enqueue and run a derive task (updateInfo
) that will derive the newinfo
value. info
will then enqueue any event handlers listening for changes to its value.stache
(via can-view-live) is listening to changes in the domUI queue. can-view-live'ssetInnerHTML
is enqueued.setInnerHTML
is run, updating the page.- The domUI queue is empty, so the mutate task's are run.
logAgeChanged
is run.
NOTE: Tasks in earlier queues will "preempt" tasks in later queues. For example, if a deriveQueue task enqueues a notifyQueue task, the notifyQueue tasks will be flushed before continuing on to additional deriveQueue tasks.
Debugging
can-queues
lets you trace the task execution with two different methods:
- log - log when tasks are enqueued, flushed, or both.
- logStack - log the tasks that resulted in the current method being run.
Consider the following code that derives an info
value from the person
observable:
const person = new observe.Object( { name: "Fran", age: 15 } );
const info = new Observation( function updateInfo() {
return person.name + " is " + person.age;
} );
info.on( function onInfoChanged( newVal ) {
console.log( "info changed" );
} );
person.age = 22;
If you wanted to know what caused onInfoChanged
to run, you could call
.logStack()
. It would log something similar to the following:
ObserveObject{} set age to 22 NOTIFY ran task: Observation<updateInfo>.onDependencyChange ▶ { ... } DERIVE ran task: Observation<updateInfo>.update ▶ { ... } MUTATE ran task: onInfoChanged ▶ { ... }
.logStack()
logs each task in the task queue and the reason the initial task was queued. The name of
each task and the queue it ran in is logged. You'll also notice that the task object itself
is logged (shown as ▶ { ... }
above). That object contains references to the following:
{
fn, // The function that was run
context, // The context (`this`) the function was called on
args, // The arguments the function was passed
meta // Additional information about the task
}
.log()
is used to log every task as it is enqueued and flushed. If queues.log()
was called
prior to person.age
being set, the following would be logged:
NOTIFY enqueuing: Observation<updateInfo>.onDependencyChange ▶ { ... } NOTIFY running : Observation<updateInfo>.onDependencyChange ▶ { ... } DERIVE enqueuing: Observation<updateInfo>.update ▶ { ... } DERIVE running : Observation<updateInfo>.update ▶ { ... } MUTATE enqueuing: onInfoChanged ▶ { ... } MUTATE running : onInfoChanged ▶ { ... }
Typically, knowing when tasks are enqueued is not helpful for debugging so it's generally more useful to only log when tasks are flushed with:
queues.log( "flush" );
How it works
can-queues
works by first creating the queue-state.js
module that simply tracks the lastTask
that has been executed. Queues update and use this state to provide logStack.
Then, the different queues are created:
can-queues uses those queues to create the notifyQueue, deriveQueue, domUIQueue and mutateQueue, wires them up so when one is done, the next is flushed. It also creates the batch
and other methods.