英文原文:http://emberjs.com/guides/understanding-ember/run-loop/

Ember内部及大部分为应用编写的代码都在一个运行循环中执行。运行循环用来做批量处理,并将任务以一种最高效的方式来执行。

运行循环通过将工作分配到特定的队列来完成任务。队列具有优先级,并严格按照优先级来执行。

为什么这样有用?

通常批处理相似的工作都能得到好处。Web浏览器也实现了相似的批处理来完成对DOM的修改。

考虑如下的HTML片段:

  1. <div id="foo"></div>
  2. <div id="bar"></div>
  3. <div id="baz"></div>

并执行如下代码:

  1. foo.style.height = "500px" // write
  2. foo.offsetHeight // read (recalculate style, layout, expensive!)
  3. bar.style.height = "400px" // write
  4. bar.offsetHeight // read (recalculate style, layout, expensive!)
  5. baz.style.height = "200px" // write
  6. baz.offsetHeight // read (recalculate style, layout, expensive!)

在本例中,一系列代码要求浏览器重新计算样式,并在每步之后重新进行布局。然而,如果能够将相似的工作放在一起,那么浏览器就有可能只需要执行一次重新计算样式和重新布局。

  1. foo.style.height = "500px" // write
  2. bar.style.height = "400px" // write
  3. baz.style.height = "200px" // write
  4. foo.offsetHeight // read (recalculate style, layout, expensive!)
  5. bar.offsetHeight // read (fast since style and layout is already known)
  6. baz.offsetHeight // read (fast since style and layout is already known)

有趣的是,这种模式对于其他类型的工作也适用。本来将相似的工作进行批量处理就能得到较好的流水作业,也有利于进行深入的优化。

下面从一个User对象开始,来看Ember优化的一个类似的例子:

  1. var User = Ember.Object.extend({
  2. firstName: null,
  3. lastName: null,
  4. fullName: function() {
  5. return this.get('firstName') + ' ' + this.get('lastName');
  6. }.property('firstName', 'lastName')
  7. });

下面的模板用来显示其属性:

  1. {{firstName}}
  2. {{fullName}}

如果不在运行循环中执行下列代码:

  1. var user = User.create({firstName:'Tom', lastName:'Huda'});
  2. user.set('firstName', 'Yehuda');
  3. // {{firstName}} and {{fullName}} are updated
  4. user.set('lastName', 'Katz');
  5. // {{lastName}} and {{fullName}} are updated

浏览器将会渲染模板两次。

  1. var user = User.create({firstName:'Tom', lastName:'Huda'});
  2. user.set('firstName', 'Yehuda');
  3. user.set('lastName', 'Katz');
  4. // {{firstName}} {{lastName}} and {{fullName}} are updated

然后,如果上述代码在运行循环中执行,浏览器将会在所有属性都被设置好后,只重新渲染一次模板。

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

如上例所示,由于用户属性值最后并没有发生改变,当这段代码在运行循环中执行时,模板并不会被重新渲染!

当然这些场景也可以一个个问题来进行优化,然而能保持开放性相对来说更好。使用运行循环,可以为此类优化问题实现应用范围内的全局优化,而不单单是一个个场景。

Ember中运行循环是如何工作的?

如之前所述,任务(函数调用)被分配到队列中,而队列会按照优先级来进行处理直到全部完成。

那么都有些什么队列,它们的优先级又是怎么样排序的呢?

  1. Ember.run.queues
  2. // => ["sync", "actions", "routerTransitions", "render", "afterRender", "destroy"]

由于优先级是从前至后的,因此”sync”队列的优先级比”render”或者”destroy”队列的要高。

这些队列里面都发生了些什么?

  • sync队列包含绑定同步的任务
  • actions队列是最普通的工作队列,通常包含待执行的计划任务。例如:承诺
  • routerTransitions队列包含路由的转换任务
  • render队列包含将要进行渲染的任务,通常都是对DOM的更新操作
  • afterRender包含之前计划进行渲染的任务完成后需要执行的任务。对于第三方修改DOM的库来说非常有用,因为这意味着任务会在DOM全部被更新后才执行
  • destroy队列包含完成其他任务计划销毁额对象的清理任务

队列中的任务以什么顺序执行?

算法按照下面的方式工作:

  1. 将包含等待任务的具有最高优先级的队列设置为CURRENT_QUEUE,如果没有任何队列中包含等待执行的任务,运行循环完成
  2. 将一个新的临时队列定义为WORK_QUEUE
  3. CURRENT_QUEUE中的任务移动到WORK_QUEUE
  4. 按顺序处理WORK_QUEUE中的所有任务
  5. 返回第一步开始执行

内部示例

与编写高层的应用代码不同,Ember内部会调用各种运行循环来计划函数的执行,这里拨开所有的面纱,直接展示原始的运行循环交互。

大部分Ember应用并不需要直接操作这些API,但是理解本示例将能更好的理解运行循环算法,有助于成为更为优秀的Ember开发者。

常见问题

对于Ember入门需要了解哪些内容?

对于基础的Ember应用开发场景,不需要了解任何关于运行循环的内容。所有道路已经铺设完毕,可以完全不需要与运行循环打交道。

对于编写一个实际的应用需要了解哪些内容?

不直接使用运行循环并不影响构建一个好的应用,因此能不用就不要用。

哪些场景需要理解运行循环?

最常见的问题是集成一个非Ember接口的异步回调。例如:

  • AJAX回调
  • DOM更新和事件回调
  • Websocket回调
  • setTimeoutsetInterval回调
  • postMessagemessageChannel事件处理器

在回调被出发时,应该开始一个运行循环。

如何通知Ember开始一个运行循环?

  1. $('a').click(function(){
  2. Ember.run(function(){ // begin loop
  3. // Code that results in jobs being scheduled goes here
  4. }); // end loop, jobs are flushed and executed
  5. });

如果忘记在一个异步处理器中启动一个运行循环会发生什么?

如上所述,任何非Ember的异步回调应该放到Ember.run中。如果没有,Ember会尝试自动添加一个。下面是一个大概会发生的情况的示例代码:

  1. $('a').click(function(){
  2. // Ember or runloop related code.
  3. Ember.run.start();
  4. // 1. we detect you need a run-loop
  5. // 2. we start one for you, but we don't really know when it ends, so we guess
  6. nextTick(function() {
  7. Ember.run.end()
  8. }, 0);
  9. });

这样做并没有达到最佳效果,因为当前的JS依然允许在运行循环清空前结束,这样就意味着有时候浏览器会有机会去做一些其他的事情,例如垃圾回收。垃圾回收如果在数据变更和DOM重新渲染的过程中执行,会导致明显的延迟,应该竟可能避免。

在测试模式下,为什么运行循环自动运行是被关闭的?

一些Ember测试助手都是承诺,需要等待运行循环为空才能履行。如果有代码不在运行循环内,会导致其过早履行,并给出错误的测试失败。关闭自动运行可以帮助找到这些场景,能为测试和应用都带来帮助。