更新于2013年10月9日:查看下面这个交互式的运行循环可视化工具:https://machty.s3.amazonaws.com/ember-run-loop-visual/index.html
更新于2013年5月9日:以下所有基本概念仍然是最新的,但是从这个提交版本开始,Ember Run Loop 的实现已经被拆分成一个叫做 backburner.js 的单独库,并且有一些微小的 API 差别。
首先,阅读以下文章:
http://blog.sproutcore.com/the-run-loop-part-1/
http://blog.sproutcore.com/the-run-loop-part-2/
它们可能不完全符合 Ember 的情况,但是运行循环背后的核心概念和动机仍然适用于 Ember;只是一些实现细节可能有所不同。接下来回答你的问题:
Ember RunLoop 何时启动?它是否取决于路由、视图、控制器或其他东西?
所有基本的用户事件(例如,键盘事件、鼠标事件等)都将启动运行循环。这确保了在将控制权返回给系统之前,由捕获的(鼠标/键盘/定时器等)事件对绑定属性所做的任何更改都在 Ember 的数据绑定系统中完全传播。因此,移动鼠标、按键、单击按钮等都会启动运行循环。
大致需要多长时间(我知道这个问题有点愚蠢,需要考虑许多因素,但我正在寻找一个大致的想法,或者是否存在最短或最长时间段)
运行循环的执行时间是依赖于复杂度和事件量的,没有固定的最短或最长时间。通常情况下,运行循环应该尽可能快地执行,并且它会在一个单线程环境下运行,以避免竞争条件。
在任何时候,RunLoop都不会跟踪通过系统传播所有更改所需的时间,并在达到最大时间限制后停止RunLoop;相反,RunLoop将始终运行到完成,并且直到调用所有过期的计时器、传播绑定以及可能是它们的绑定等,才会停止。显然,需要从单个事件中传播的更改越多,RunLoop完成所需的时间就越长。这是一个(非常不公平的)例子,演示了RunLoop如何与另一个没有RunLoop的框架(Backbone)相比,会因为传播更改而陷入困境:
http://jsfiddle.net/jashkenas/CGSd5/ 。故事的寓意是:RunLoop对于Ember中的大多数想做的事情来说真的很快,而且它是Ember强大的地方之一,但是如果您希望以60帧每秒的速度使用Javascript动画30个圆圈,则可能有比依赖Ember的RunLoop更好的方法。
RunLoop并非始终执行--必须在某个时间点将控制权返回给系统,否则您的应用程序将挂起--它与服务器上的运行循环不同,该服务器具有
while(true)
且一直运行到服务器收到关闭信号为止......Ember RunLoop没有这样的
while(true)
,只有在响应用户/计时器事件时才会启动。
让我们看看是否可以弄清楚这个问题。从SC到Ember RunLoop的一个重大变化是,Ember不再在
invokeOnce
和
invokeLast
之间来回循环(您可以在与SproutCore的RL有关的第一个链接中的图表中看到),而是提供了一系列“队列”,在运行循环过程中,您可以通过指定操作属于哪个队列(源代码示例:
Ember.run.scheduleOnce('render', bindView, 'rerender');
)来安排要调用的函数。
如果你查看源代码中的run_loop.js
,你会看到Ember.run.queues = ['sync', 'actions', 'destroy', 'timers'];
,但是如果你在Ember应用程序中打开JavaScript调试器并评估Ember.run.queues
,你会得到一个更完整的队列列表:["sync", "actions", "render", "afterRender", "destroy", "timers"]
。Ember保持其代码库非常模块化,并使您的代码以及其自己代码在库的不同部分中插入更多的队列成为可能。在这种情况下,Ember Views库特别在actions
队列之后插入了render
和afterRender
队列。我马上就会讲到为什么。首先是RunLoop算法:
RunLoop算法与上述运行循环文章中描述的基本相同:
- 你可以在RunLoop
.begin()
和.end()
之间运行你的代码,但在Ember中,你应该使用Ember.run
来代替,它会内部调用begin
和end
。(只有Ember代码库中的内部运行循环代码仍然使用begin
和end
,所以你应该直接使用Ember.run
)
- 在调用
end()
之后,RunLoop会启动以传播通过Ember.run
函数传递的代码块所做出的每个更改。这包括传播绑定属性的值,将视图更改呈现到DOM等。这些操作(绑定、渲染DOM元素等)执行的顺序由上面描述的Ember.run.queues
数组决定:
- 运行循环将从第一个队列开始,即
sync
队列。它将运行所有由Ember.run
代码安排到sync
队列中的操作。这些操作本身也可能安排更多的操作,在此同一RunLoop中执行,而运行循环必须确保它执行每个操作,直到所有队列都被刷新。它这样做的方式是,在每个队列的末尾,RunLoop将查看所有已刷新的队列,看看是否安排了任何新的操作。如果有,则必须从具有未执行安排操作的最早队列的开头开始刷新队列,继续跟踪其步骤并在必要时重新开始,直到所有队列完全为空。
这就是算法的实质。这是绑定数据如何在应用程序中传播的方式。一旦RunLoop运行到完成,您可以期望所有绑定数据都将完全传播。那么DOM元素呢?
队列的顺序,包括由Ember Views库添加的那些队列,在这里非常重要。注意render
和afterRender
在sync
和action
之后。 sync
队列包含用于传播绑定数据的所有操作。(此后,action
在Ember源代码中仅被稀疏使用)。基于上述算法,可以保证当RunLoop到达render
队列时,所有的数据绑定都已完成同步。这是设计如此的:您不希望在同步数据绑定之前执行渲染DOM元素等昂贵的任务,因为这可能需要使用更新的数据重新渲染DOM元素 - 显然是一种非常低效和容易出错的方法来清空所有RunLoop队列。因此,Ember会智能地在渲染render
队列中的DOM元素之前处理所有可行的数据绑定工作。
因此,最后回答您的问题,是的,可以期望在Ember.run
完成时已经进行了必要的DOM渲染。下面是一个jsFiddle演示:http://jsfiddle.net/machty/6p6XJ/328/
RunLoop的其他要点
观察者与绑定器
需要注意的是,观察者和绑定器尽管具有相似的“监视”属性变化的功能,但在RunLoop的上下文中完全不同。正如我们所看到的,绑定传播会被安排到sync
队列中,最终由RunLoop执行。另一方面,当监视的属性发生更改时,观察者立即触发而无需首先安排进入RunLoop队列。如果一个观察者和一个绑定都“监视”同一个属性,则观察者总是比绑定更早地更新。
scheduleOnce
和Ember.run.once
Ember自动更新模板的一个重要效率提升基于RunLoop,多个相同的RunLoop操作可以合并(如果您愿意,可以称之为“去抖动”)成一个单独的操作。如果查看run_loop.js
的内部,您将看到实现此行为的函数是相关的scheduleOnce
和Em.run.once
函数。它们之间的区别并不是很重要,知道它们的存在以及如何丢弃队列中的重复操作以防止运行循环期间进行大量臃肿、浪费的计算即可。
那么定时器呢?
即使“timers”是上面列出的默认队列之一,Ember只在他们的RunLoop测试用例中提到了该队列。从以上关于timer的描述来看,这样的队列似乎在SproutCore时代被使用过,因为timer是最后一个触发的事情。在Ember中,不使用“timers”队列。相反, RunLoop 可以通过内部管理的 setTimeout 事件(请参见 invokeLaterTimers 函数),智能地循环遍历所有现有的定时器,启动所有已过期的定时器,确定最早的下一个定时器,并仅为该事件设置内部 setTimeout,当它触发时再次启动 RunLoop。与每个计时器调用 setTimeout 并唤醒自身相比,这种方法更高效,因为在这种情况下,只需要进行一个 setTimeout 调用,而 RunLoop 足够聪明可以同时启动多个计时器。
使用sync队列进一步减少抖动
这是RunLoop中的一段代码片段,在循环遍历RunLoop中的所有队列时。请注意,对于 sync 队列的特殊情况:由于 sync 是一个特别易变的队列,在其中数据向各个方向传播,因此会调用 Ember.beginPropertyChanges()
来防止观察者被触发,接着调用 Ember.endPropertyChanges()
。这是明智的:在刷新 sync 队列的过程中,很可能对象上的属性会多次更改,直到最终值才会停止,您不希望每次更改都立即触发观察者而浪费资源。
if (queueName === 'sync')
{
log = Ember.LOG_BINDINGS;
if (log)
{
Ember.Logger.log('Begin: Flush Sync Queue');
}
Ember.beginPropertyChanges();
Ember.tryFinally(tryable, Ember.endPropertyChanges);
if (log)
{
Ember.Logger.log('End: Flush Sync Queue');
}
}
else
{
forEach.call(queue, iter);
}