The Ember.js Run Loop

September 24, 2014

Reading time ~63 minutes

Introduction

Ember.js is an ambitious Javascript MVC framework. It not only provides a clear way to build a web application using router, controller and view. but also utilizes a clever structure, the run loop, to schedule the jobs in an Ember application so as to avoid unnecessary computations.

The run loop is entirely wrapped in the implementation of Ember. So developers building light web applications don’t need to worry about explicitly invoking it. Nevertheless, it is worthwhile for us to take a deep look at its implementation.

Basic Idea

Ember.js arranges jobs into six different queues:

  • 1
    
    sync
    
  • 1
    
    actions
    
  • 1
    
    routerTransitions
    
  • 1
    
    render
    
  • 1
    
    afterRender
    
  • 1
    
    destroy
    

sync

Synchronization jobs. e.g. all the bindings in Ember.js.

actions

A general work queue. Typically contains scheduled jobs.

routerTransitions

Contains router transition jobs.

render

Contains rendering jobs, which typically updates DOM. e.g. template rendering.

afterRender

Contains scheduled jobs that should be triggered after the render jobs finish.

destroy

Contains jobs to destroy objects.


The priority of these queues is first to last, from

1
sync
to
1
destroy
.

The algorithm

In a run loop, Ember.js will look at each queue in the order of priority. If a queue has pending tasks, the run loop will execute them. Obviously, this will possibly schedule more tasks. So the run loop continues to execute the tasks in the above order until there is no pending jobs.

Advantages

The Ember run loop separates logics and rendering so that it avoids running many unnecessary codes, e.g. updates the bindings.

To illustrate this, let me use an example from the official website.

We have an object,

1
User
,

var User = Ember.Object.extend({
  firstName: null,
  lastName: null,
  fullName: function() {
    return this.get('firstName') + ' ' + this.get('lastName');
  }.property('firstName', 'lastName')
});

a simple template,

and a few Javascript code.

var user = User.create({firstName:'Tom', lastName:'Huda'});
user.set('firstName', 'Yehuda');
user.set('lastName', 'Katz');

Without the run loop, the above code should ask the browser to render the template three times:

var user = User.create({firstName:'Tom', lastName:'Huda'});
// object created, first render

user.set('firstName', 'Yehuda');
// attributes updated, second render,  and  are updated

user.set('lastName', 'Katz');
// attributes updated, third render,  and  are updated

But with the run loop, the rendering jobs in the

1
render
queue won’t be executed before all the above code is done:

var user = User.create({firstName:'Tom', lastName:'Huda'});
user.set('firstName', 'Yehuda');
user.set('lastName', 'Katz');
// first render,  and  are updated

Implementation

Code structure

  • 1
    
    ember-metal/lib/run_loop.js
    
    is where the run loop code lies. It provides all the API methods developers need to know. Moreover, it adds
    1
    
    sync
    
    ,
    1
    
    actions
    
    and
    1
    
    destroy
    
    queues.
  • 1
    
    ember-metal/lib/backburner.js
    
    is the underlying actual run loop handler used by
    1
    
    run_loop.js
    
    .
  • 1
    
    ember-views/lib/ext.js
    
    adds
    1
    
    render
    
    and
    1
    
    afterRender
    
    queues.
  • 1
    
    ember-routing/lib/ext/run_loop.js
    
    adds the
    1
    
    routerTransitions
    
    queue.

Backburner structure

1
backburner.js
provides a class of run loop controller,
1
Backburner
. Each
1
Backburner
handles one or more similar
1
DeferredActionQueues
objects, aka the run loops. Moreover, each
1
DeferredActionQueues
is consisted of
1
queue
s, which is the place the scheduled jobs rest.

In conclusion, the

1
Backburner
starts
1
DeferredActionQueues
to run different jobs. Each
1
DeferredActionQueues
executed the jobs in a specific priority order by sending them to different
1
queue
s.

Actual Code

  • Initiating the actual handler,

    1
    
    backburner
    
    .

    1
    
    run_loop.js
    

function onBegin(current) {
  run.currentRunLoop = current;
}

function onEnd(current, next) {
  run.currentRunLoop = next;
}

var Backburner = requireModule('backburner').Backburner;
var backburner = new Backburner(['sync', 'actions', 'destroy'], {
  sync: {
    before: beginPropertyChanges,
    after: endPropertyChanges
  },
  defaultQueue: 'actions',
  onBegin: onBegin,
  onEnd: onEnd,
  onErrorTarget: Ember,
  onErrorMethod: 'onerror'
});

1
backburner.js

function Backburner(queueNames, options) {
  this.queueNames = queueNames;
  this.options = options || {};
  if (!this.options.defaultQueue) {
    this.options.defaultQueue = queueNames[0];
  }
  this.instanceStack = [];
  this._debouncees = [];
  this._throttlers = [];
}

Backburner.prototype = {
  queueNames: null,
  options: null,
  currentInstance: null,
  instanceStack: null,
  begin: function() {
    var options = this.options,
        onBegin = options && options.onBegin,
        previousInstance = this.currentInstance;

    if (previousInstance) {
      this.instanceStack.push(previousInstance);
    }

    this.currentInstance = new DeferredActionQueues(this.queueNames, options);
    if (onBegin) {
      onBegin(this.currentInstance, previousInstance);
    }
  },

  end: function() {
    var options = this.options,
        onEnd = options && options.onEnd,
        currentInstance = this.currentInstance,
        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);
        }
      }
    }
  },
  ... // Other methods will be discussed later
};

In

1
backburner.begin
and
1
backburner.end
, we can see that
1
instanceStack
is used as a stack of run loops. Together with the
1
onBegin
and
1
onEnd
functions provided in
1
run_loop.js
, it is able to handle run loops that are invoked during the execution of another run loop.

Note that

1
defaultQueue
here is specified as
1
actions
. If we don’t explicitly set
1
defaultQueue
,
1
queueNames[0]
will be chosen as
1
defaultQueue
.
1
defaultQueue
is the queue where jobs are scheduled into if no target queue is specified.

  • 1
    
    Ember.run
    

    1
    
    run_loop.js
    

function run() {
  return apply(backburner, backburner.run, arguments);
}

1
backburner.js

Backburner.prototype = {
  run: function(target, method /*, args */) {
    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);

    // guard against Safari 6's double-finally bug
    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();
        }
      }
    }
  },
  ...
};

...

DeferredActionQueues.prototype = {
  flush: function() {
    var queues = this.queues,
        queueNames = this.queueNames,
        queueName, queue, queueItems, priorQueueNameIndex,
        queueNameIndex = 0, numberOfQueues = queueNames.length,
        options = this.options,
        onError = options.onError || (options.onErrorTarget && options.onErrorTarget[options.onErrorMethod]),
        invoke = onError ? this.invokeWithOnError : this.invoke;

    outerloop:
    while (queueNameIndex < numberOfQueues) {
      queueName = queueNames[queueNameIndex];
      queue = queues[queueName];
      queueItems = queue._queueBeingFlushed = queue._queue.slice();
      queue._queue = [];

      var queueOptions = queue.options, // TODO: write a test for this
          before = queueOptions && queueOptions.before,
          after = queueOptions && queueOptions.after,
          target, method, args, stack,
          queueIndex = 0, numberOfQueueItems = queueItems.length;

      if (numberOfQueueItems && before) { before(); }

      while (queueIndex < numberOfQueueItems) {
        target = queueItems[queueIndex];
        method = queueItems[queueIndex+1];
        args   = queueItems[queueIndex+2];
        stack  = queueItems[queueIndex+3]; // Debugging assistance

        if (isString(method)) { method = target[method]; }

        // method could have been nullified / canceled during flush
        if (method) {
          invoke(target, method, args, onError);
        }

        queueIndex += 4;
      }

      queue._queueBeingFlushed = null;
      if (numberOfQueueItems && after) { after(); }

      if ((priorQueueNameIndex = indexOfPriorQueueWithActions(this, queueNameIndex)) !== -1) {
        queueNameIndex = priorQueueNameIndex;
        continue outerloop;
      }

      queueNameIndex++;
    }
  },
  ...
};

Asks

1
backburner
to initiate a new run loop to run the job in arguments. It wraps the execution in
1
begin
and
1
end
calls so that the
1
flush
call ensures that bindings are updated and events are responded.

  • 1
    
    Ember.run.join
    

    1
    
    run_loop.js
    

run.join = function(target, method /* args */) {
  if (!run.currentRunLoop) {
    return apply(Ember, run, arguments);
  }

  var args = slice.call(arguments);
  args.unshift('actions');
  apply(run, run.schedule, args);
};

Works almost the same as

1
Ember.run
. However, if there already exists a run loop, instead of initiating a new one,
1
Ember.run.join
will schedule the job into the existing run loop in the
1
actions
queue.

  • 1
    
    Ember.run.bind
    

    1
    
    run_loop.js
    

run.bind = function(target, method /* args*/) {
  var args = slice.call(arguments);
  return function() {
    return apply(run, run.join, args.concat(slice.call(arguments)));
  };
};

Provides a callback method for third party modules to use.

  • 1
    
    Ember.run.begin
    
    and
    1
    
    Ember.run.end
    

    1
    
    run_loop.js
    

run.begin = function() {
  backburner.begin();
};

run.end = function() {
  backburner.end();
};

Directly asks backburner to start or end the current run loop.

  • 1
    
    Ember.run.schedule
    
    ,
    1
    
    Ember.run.scheduleOnce
    
    and
    1
    
    Ember.run.once
    

    1
    
    run_loop.js
    

run.schedule = function(queue, target, method) {
  checkAutoRun();
  apply(backburner, backburner.schedule, arguments);
};

run.scheduleOnce = function(queue, target, method) {
  checkAutoRun();
  return apply(backburner, backburner.scheduleOnce, arguments);
};

run.once = function(target, method) {
  checkAutoRun();
  var args = slice.call(arguments);
  args.unshift('actions');
  return apply(backburner, backburner.scheduleOnce, args);
};

1
backburner.js

Backburner.prototype = {
  defer: function(queueName, target, method /* , args */) {
    if (!method) {
      method = target;
      target = null;
    }

    if (isString(method)) {
      method = target[method];
    }

    var stack = this.DEBUG ? new Error() : undefined,
        args = arguments.length > 3 ? slice.call(arguments, 3) : undefined;
    if (!this.currentInstance) { createAutorun(this); }
    return this.currentInstance.schedule(queueName, target, method, args, false, stack);
  },

  deferOnce: function(queueName, target, method /* , args */) {
    if (!method) {
      method = target;
      target = null;
    }

    if (isString(method)) {
      method = target[method];
    }

    var stack = this.DEBUG ? new Error() : undefined,
        args = arguments.length > 3 ? slice.call(arguments, 3) : undefined;
    if (!this.currentInstance) { createAutorun(this); }
    return this.currentInstance.schedule(queueName, target, method, args, true, stack);
  },
  ...
};

Backburner.prototype.schedule = Backburner.prototype.defer;
Backburner.prototype.scheduleOnce = Backburner.prototype.deferOnce;

function createAutorun(backburner) {
  backburner.begin();
  backburner._autorun = global.setTimeout(function() {
    backburner._autorun = null;
    backburner.end();
  });
}

...

DeferredActionQueues.prototype = {
  schedule: function(queueName, target, method, args, onceFlag, stack) {
    var queues = this.queues,
        queue = queues[queueName];

    if (!queue) { throw new Error("You attempted to schedule an action in a queue (" + queueName + ") that doesn't exist"); }

    if (onceFlag) {
      return queue.pushUnique(target, method, args, stack);
    } else {
      return queue.push(target, method, args, stack);
    }
  },
  ...
};

All these three methods schedule jobs to the current run loop. The different is that

1
scheduleOnce
ensures that the job is only executed once and that
1
once
is the convenient method for
1
scheduleOnce
on the
1
actions
queue. All of them invoke
1
backburner.schedule
, which asks the current run loop,
1
currentInstance
to push the job into corresponding queue.

  • 1
    
    Ember.run.later
    
    ,
    1
    
    Ember.run.next
    
    ,
    1
    
    Ember.run.hasScheduledTimers
    
    ,
    1
    
    Ember.run.cancel
    
    and
    1
    
    Ember.run.cancelTimers
    

    1
    
    run_loop.js
    

run.later = function(target, method) {
  return apply(backburner, backburner.later, arguments);
};

run.next = function() {
  var args = slice.call(arguments);
  args.push(1);
  return apply(backburner, backburner.later, args);
};

run.hasScheduledTimers = function() {
  return backburner.hasTimers();
};

run.cancel = function(timer) {
  return backburner.cancel(timer);
};

run.cancelTimers = function () {
  backburner.cancelTimers();
};

1
backburner.js

DeferredActionQueues.prototype = {
  setTimeout: function() {
    var args = slice.call(arguments),
        length = args.length,
        method, wait, target,
        methodOrTarget, methodOrWait, methodOrArgs;

    if (length === 0) {
      return;
    } else if (length === 1) {
      method = args.shift();
      wait = 0;
    } else if (length === 2) {
      methodOrTarget = args[0];
      methodOrWait = args[1];

      if (isFunction(methodOrWait) || isFunction(methodOrTarget[methodOrWait])) {
        target = args.shift();
        method = args.shift();
        wait = 0;
      } else if (isCoercableNumber(methodOrWait)) {
        method = args.shift();
        wait = args.shift();
      } else {
        method = args.shift();
        wait =  0;
      }
    } else {
      var last = args[args.length - 1];

      if (isCoercableNumber(last)) {
        wait = args.pop();
      } else {
        wait = 0;
      }

      methodOrTarget = args[0];
      methodOrArgs = args[1];

      if (isFunction(methodOrArgs) || (isString(methodOrArgs) &&
                                      methodOrTarget !== null &&
                                      methodOrArgs in methodOrTarget)) {
        target = args.shift();
        method = args.shift();
      } else {
        method = args.shift();
      }
    }

    var executeAt = (+new Date()) + parseInt(wait, 10);

    if (isString(method)) {
      method = target[method];
    }

    var onError = getOnError(this.options);

    function fn() {
      if (onError) {
        try {
          method.apply(target, args);
        } catch (e) {
          onError(e);
        }
      } else {
        method.apply(target, args);
      }
    }

    // find position to insert
    var i = searchTimer(executeAt, timers);

    timers.splice(i, 0, executeAt, fn);

    updateLaterTimer(this, executeAt, wait);

    return fn;
  },

  cancelTimers: function() {
    var clearItems = function(item) {
      clearTimeout(item[2]);
    };

    each(this._throttlers, clearItems);
    this._throttlers = [];

    each(this._debouncees, clearItems);
    this._debouncees = [];

    if (this._laterTimer) {
      clearTimeout(this._laterTimer);
      this._laterTimer = null;
    }
    timers = [];

    if (this._autorun) {
      clearTimeout(this._autorun);
      this._autorun = null;
    }
  },

  hasTimers: function() {
    return !!timers.length || !!this._debouncees.length || !!this._throttlers.length || this._autorun;
  },

  cancel: function(timer) {
    var timerType = typeof timer;

    if (timer && timerType === 'object' && timer.queue && timer.method) { // we're cancelling a deferOnce
      return timer.queue.cancel(timer);
    } else if (timerType === 'function') { // we're cancelling a setTimeout
      for (var i = 0, l = timers.length; i < l; i += 2) {
        if (timers[i + 1] === timer) {
          timers.splice(i, 2); // remove the two elements
          return true;
        }
      }
    } else if (Object.prototype.toString.call(timer) === "[object Array]"){ // we're cancelling a throttle or debounce
      return this._cancelItem(findThrottler, this._throttlers, timer) ||
               this._cancelItem(findDebouncee, this._debouncees, timer);
    } else {
      return; // timer was null or not a timer
    }
  },
  ...
};

function updateLaterTimer(self, executeAt, wait) {
  if (!self._laterTimer || executeAt < self._laterTimerExpiresAt) {
    self._laterTimer = global.setTimeout(function() {
      self._laterTimer = null;
      self._laterTimerExpiresAt = null;
      executeTimers(self);
    }, wait);
    self._laterTimerExpiresAt = executeAt;
  }
}

function executeTimers(self) {
  var now = +new Date(),
      time, fns, i, l;

  self.run(function() {
    i = searchTimer(now, timers);

    fns = timers.splice(0, i);

    for (i = 1, l = fns.length; i < l; i += 2) {
      self.schedule(self.options.defaultQueue, null, fns[i]);
    }
  });

  if (timers.length) {
    updateLaterTimer(self, timers[0], timers[0] - now);
  }
}

function searchTimer(time, timers) {
  var start = 0,
      end = timers.length - 2,
      middle, l;

  while (start < end) {
    // since timers is an array of pairs 'l' will always
    // be an integer
    l = (end - start) / 2;

    // compensate for the index in case even number
    // of pairs inside timers
    middle = start + l - (l % 2);

    if (time >= timers[middle]) {
      start = middle + 2;
    } else {
      end = middle;
    }
  }

  return (time >= timers[start]) ? start + 2 : start;
}

This set of functions, together with

1
Ember.run.throttle
and
1
Ember.run.debounce
below enables developers to schedule delayed jobs. These functions maintain an array,
1
timers
, to handle the delayed jobs.
1
timers
is an array of
1
(executeAt, fn)
pairs.

  • 1
    
    searchTimer
    
    finds the place in
    1
    
    timers
    
    where a new delayed job should be inserted.
  • 1
    
    executeTimers
    
    runs all the delayed jobs that should be executed up till now.
  • 1
    
    updateLaterTimer
    
    uses the native
    1
    
    setTimeOut
    
    function to schedule
    1
    
    executeTimers
    
    at the time when next delayed job should be executed. (Note: I think there should be a
    1
    
    clearTimeout
    
    here, but I’m not sure of this.)
  • 1
    
    hasScheduledTimers
    
    returns true if and only if there are delayed jobs pending.

With these three functions,

  • 1
    
    Ember.run.later
    
    uses
    1
    
    searchTimer
    
    to place the job in
    1
    
    timers
    
    array and calls
    1
    
    updateLaterTimer
    
    to update the next
    1
    
    executeTimers
    
    call.
  • 1
    
    Ember.run.next
    
    schedules a job 1ms later. What it actually does is asking Ember to start a new run loop of the job right after the current one.
  • 1
    
    Ember.run.hasScheduledTimers
    
    returns the value of
    1
    
    hasScheduledTimers
    
    .
  • 1
    
    Ember.run.cancel
    
    cancels a specific timer by removing it from
    1
    
    timers
    
    and other relative collections.
  • 1
    
    Ember.run.cancelTimers
    
    cancels all the pending timers by clearing the
    1
    
    timers
    
    array and other relative attributes.

  • 1
    
    Ember.run.sync
    

    1
    
    run_loop.js
    

run.sync = function() {
  if (backburner.currentInstance) {
    backburner.currentInstance.queues.sync.flush();
  }
};

1
backburner.js

Queue.prototype = {
  // TODO: remove me, only being used for Ember.run.sync
  flush: function() {
    var queue = this._queue,
        globalOptions = this.globalOptions,
        options = this.options,
        before = options && options.before,
        after = options && options.after,
        onError = globalOptions.onError || (globalOptions.onErrorTarget && globalOptions.onErrorTarget[globalOptions.onErrorMethod]),
        target, method, args, stack, i, l = queue.length;

    if (l && before) { before(); }
    for (i = 0; i < l; i += 4) {
      target = queue[i];
      method = queue[i+1];
      args   = queue[i+2];
      stack  = queue[i+3]; // Debugging assistance

      // TODO: error handling
      if (args && args.length > 0) {
        if (onError) {
          try {
            method.apply(target, args);
          } catch (e) {
            onError(e);
          }
        } else {
          method.apply(target, args);
        }
      } else {
        if (onError) {
          try {
            method.call(target);
          } catch(e) {
            onError(e);
          }
        } else {
          method.call(target);
        }
      }
    }
    if (l && after) { after(); }

    // check if new items have been added
    if (queue.length > l) {
      this._queue = queue.slice(l);
      this.flush();
    } else {
      this._queue.length = 0;
    }
  },
  ...
};

1
Ember.run.sync
is actually the only method that uses
1
flush
on a specific queue. The Ember developer team is trying to figure out a way to replace it with other functions. Nevertheless, it is still helpful since there indeed are places we need to synchronize the bindings.

  • 1
    
    Ember.run.debounce
    
    and
    1
    
    Ember.run.throttle
    

    1
    
    run_loop.js
    

run.debounce = function() {
  return apply(backburner, backburner.debounce, arguments);
};

run.throttle = function() {
  return apply(backburner, backburner.throttle, arguments);
};

1
backburner.js

Queue.prototype = {
  debounce: function(target, method /* , args, wait, [immediate] */) {
    var self = this,
        args = arguments,
        immediate = pop.call(args),
        wait,
        index,
        debouncee,
        timer;

    if (isNumber(immediate) || isString(immediate)) {
      wait = immediate;
      immediate = false;
    } else {
      wait = pop.call(args);
    }

    wait = parseInt(wait, 10);
    // Remove debouncee
    index = findDebouncee(target, method, this._debouncees);

    if (index > -1) {
      debouncee = this._debouncees[index];
      this._debouncees.splice(index, 1);
      clearTimeout(debouncee[2]);
    }

    timer = global.setTimeout(function() {
      if (!immediate) {
        self.run.apply(self, args);
      }
      var index = findDebouncee(target, method, self._debouncees);
      if (index > -1) {
        self._debouncees.splice(index, 1);
      }
    }, wait);

    if (immediate && index === -1) {
      self.run.apply(self, args);
    }

    debouncee = [target, method, timer];

    self._debouncees.push(debouncee);

    return debouncee;
  },

  throttle: function(target, method /* , args, wait, [immediate] */) {
    var self = this,
        args = arguments,
        immediate = pop.call(args),
        wait,
        throttler,
        index,
        timer;

    if (isNumber(immediate) || isString(immediate)) {
      wait = immediate;
      immediate = true;
    } else {
      wait = pop.call(args);
    }

    wait = parseInt(wait, 10);

    index = findThrottler(target, method, this._throttlers);
    if (index > -1) { return this._throttlers[index]; } // throttled

    timer = global.setTimeout(function() {
      if (!immediate) {
        self.run.apply(self, args);
      }
      var index = findThrottler(target, method, self._throttlers);
      if (index > -1) {
        self._throttlers.splice(index, 1);
      }
    }, wait);

    if (immediate) {
      self.run.apply(self, args);
    }

    throttler = [target, method, timer];

    this._throttlers.push(throttler);

    return throttler;
  },
  ...
};

#### What do they do?

  • 1
    
    Ember.run.debounce
    
    ensures a job is never executed within a specific time interval.
  • 1
    
    Ember.run.throttle
    
    ensures a job is never executed more frequently than a specific time interval.

#### How do they do it?

The functions use

1
_debouncees
and
1
_throttlers
to maintain the list of jobs that are using these functions. They also use the native
1
setTimeOut
to schedule of calls.

Note:

1
Ember.run.cancel
can also use return values of these functions to cancel debounce and throttle timers.

  • 1
    
    Ember.run._addQueue
    

    1
    
    run_loop.js
    

run._addQueue = function(name, after) {
  if (indexOf.call(run.queues, name) === -1) {
    run.queues.splice(indexOf.call(run.queues, after)+1, 0, name);
  }
};

Adds a new queue,

1
name
. All jobs of this queue is executed after
1
after
.
1
ember-routing
and
1
ember-views
use this method to add
1
render
,
1
afterRender
and
1
routerTransitions
queue.

How is the run loop used?

Using the run loop is amazingly simple. For instance, in

1
ember-views/lib/views/view.js
,

 _insertElementLater: function(fn) {
  this._scheduledInsert = run.scheduleOnce('render', this, '_insertElement', fn);
},

_insertElement: function (fn) {
  this._scheduledInsert = null;
  this.currentState.insertElement(this, fn);
},

...

replaceIn: function(target) {
  Ember.assert("You tried to replace in (" + target + ") but that isn't in the DOM", jQuery(target).length > 0);
  Ember.assert("You cannot replace an existing Ember.View. Consider using Ember.ContainerView instead.", !jQuery(target).is('.ember-view') && !jQuery(target).parents().is('.ember-view'));

  this._insertElementLater(function() {
    jQuery(target).empty();
    this.$().appendTo(target);
  });
}

1
replaceIn
uses the
1
Ember.run.scheduleOnce
method to schedule a replace in the
1
render
queue.

When does Ember.js start a run loop?

As we have seen in the above code, Ember.js uses the run loop everywhere. But besides these run loop calls from inside Ember.js, when does Ember.js start a run loop?

To answer this question, let’s take a look at this piece of code from

1
ember-views/lib/system/event_dispatcher.js
,

_dispatchEvent: function(object, evt, eventName, view) {
  var result = true;

  var handler = object[eventName];
  if (typeOf(handler) === 'function') {
    result = run(object, handler, evt, view);
    // Do not preventDefault in eventManagers.
    evt.stopPropagation();
  }
  else {
    result = this._bubbleEvent(view, evt, eventName);
  }

  return result;
},

_bubbleEvent: function(view, evt, eventName) {
  return run(view, view.handleEvent, eventName, evt);
}

Notice the

1
Ember.run
calls in these functions. Every event sent to Ember.js initiates a run loop. The event handle will then possibly trigger some binding jobs and render jobs. After all the jobs are down (no pending jobs), the event is successfully handled and the run loop is completed.

Conclusion

The run loop is a smart idea to avoid unneeded computations, which is largely favorable in web app development. Also, Ember.js provides a detailed API for the developers to fully take advantage of the run loop structure.

An Interesting Math Problem

Hello! It’s been quite a while since my last post. This post I’ll present you a math problem, and a clever proof.

Hello! It's been quite a while since my last post. This post I'll present you a math problem, and a clever proof. Here is the problem st...… Continue reading

Neural Network to Predict DotA Game Results

Published on January 25, 2015

SAT Solver with Su Doku!

Published on October 10, 2014