什么是Ember RunLoop,它如何工作?


96

我试图了解Ember RunLoop的工作原理以及使它滴答作响的原因。我看了文档,但是仍然有很多疑问。我有兴趣更好地了解RunLoop的工作原理,以便在以后不得不推迟执行某些代码的情况下,可以在其名称空间中选择适当的方法。

  • Ember RunLoop何时启动。它依赖于路由器,视图或控制器还是其他?
  • 大概需要多长时间(我知道这很愚蠢,并且要取决于很多事情,但是我正在寻找一个一般性的想法,或者是否可能需要最小或最大的运行循环时间)
  • RunLoop是否一直在执行,还是只是指示从开始执行到结束执行的时间,可能不会运行一段时间。
  • 如果从一个RunLoop内部创建视图,是否可以保证在循环结束时所有视图的内容都将进入DOM?

如果这些是非常基本的问题,请原谅我,我认为了解这些内容将帮助像我这样的菜鸟更好地使用Ember。


5
没有关于运行循环的出色文档。我本周将尝试在其上放一个简短的幻灯片。
路加·梅里亚

2
@LukeMelia这个问题仍然非常需要您的注意,看起来其他人也在寻找相同的信息。如果有机会分享关于RunLoop的见解,那就太好了。
2013年

Answers:


199

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并非100%准确,但是RunLoop背后的核心概念和动机通常仍然适用于Ember;只有一些实现细节有所不同。但是,关于您的问题:

Ember RunLoop何时启动。它依赖于路由器,视图或控制器还是其他?

所有基本的用户事件(例如键盘事件,鼠标事件等)都会触发运行循环。这样可以确保在将控制权还给系统之前,捕获事件(鼠标/键盘/定时器/等)对绑定属性所做的任何更改都将在Ember的数据绑定系统中完全传播。因此,移动鼠标,按下键,单击按钮等,都将启动运行循环。

大概需要多长时间(我知道这很愚蠢,并且要取决于很多事情,但是我正在寻找一个一般性的想法,或者是否可能需要最小或最大的运行循环时间)

RunLoop绝不会跟踪花费所有时间在系统中传播所有更改,然后在达到最大时间限制后停止RunLoop;确切地说,RunLoop将始终运行到完成,并且直到所有已到期的计时器都已被调用,绑定被传播,以及它们的绑定被传播等等,都不会停止。显然,从单个事件传播的更改越多,RunLoop完成所需的时间就越长。这是一个(非常不公平的)示例,与没有运行循环的另一个框架(主干)相比,RunLoop如何陷入繁琐的更改中:http : //jsfiddle.net/jashkenas/CGSd5/。故事的寓意:RunLoop确实可以在Ember中完成大多数事情,而这正是Ember强大功能的所在,但是如果您发现自己想以每秒60帧的速度用Javascript动画30个圆,那么可能比依靠Ember的RunLoop更好。

RunLoop是否一直在执行,还是只是指示从开始执行到结束执行的时间,可能不会运行一段时间。

它并非始终执行-它必须在某个时刻将控制权返回给系统,否则您的应用会挂起-它不同于服务器上的运行循环,该循环具有a while(true)并持续无限,直到服务器收到关闭信号。Ember RunLoop没有关闭信号,while(true)只是响应用户/计时器事件而启动。

如果从一个RunLoop内部创建视图,是否可以保证在循环结束时所有视图的内容都将进入DOM?

让我们看看是否可以解决这个问题。一个从SC到灰烬RunLoop大的变化是,而不是循环来回之间invokeOnceinvokeLast(你约了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库将插入render并插入afterRender队列,尤其是在actions队列之后。我将解释为什么可能要在一秒钟之后。首先,RunLoop算法:

RunLoop算法与以上SC运行循环文章中所述的算法几乎相同:

  • 您运行RunLoop之间的代码.begin().end(),只有在灰烬你会想,而不是运行在您的代码Ember.run,这将在内部调用begin,并end为您服务。(只有Ember代码库中的内部运行循环代码仍使用beginend,因此您应该坚持使用Ember.run
  • 之后end()被调用,然后RunLoop踢成齿轮传播通过代码传递给块每一个微小变动Ember.run的功能。这包括传播绑定属性的值,渲染对DOM的视图更改等,这些动作(绑定,渲染DOM元素等)的执行顺序由上述Ember.run.queues数组确定:
  • 运行循环将在第一个队列上开始sync。它将运行syncEmber.run代码安排到队列中的所有操作。这些动作本身也可以安排在同一RunLoop期间执行更多的动作,这取决于RunLoop来确保它执行所有动作,直到刷新所有队列为止。这样做的方法是,在每个队列的末尾,RunLoop将浏览所有以前刷新的队列,并查看是否已安排任何新操作。如果是这样,它必须从最早的队列开始,执行未执行的计划动作,然后清除该队列,继续跟踪其步骤,并在必要时重新开始,直到所有队列完全为空。

这就是算法的本质。这就是绑定数据通过应用程序传播的方式。您可以期望,一旦RunLoop运行完成,所有绑定的数据都将被完全传播。那么,DOM元素呢?

队列的顺序(包括Ember Views库添加的队列)在这里很重要。请注意renderafterRender之后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队列中。如果观察者和绑定都“监视”了相同的属性,则在更新绑定之前,观察者将始终被100%的时间调用。

scheduleOnceEmber.run.once

Ember自动更新模板的最大效率提升之一是基于这样一个事实,即借助RunLoop,可以将多个相同的RunLoop操作合并(如果需要,可以“反跳”)为一个操作。如果您查看run_loop.js内部原理,您会看到促进此行为的功能是相关功能scheduleOnceEm.run.once。它们之间的区别并不重要,要知道它们的存在,以及它们如何在队列中丢弃重复的操作以防止运行循环中大量肿,浪费的计算。

计时器呢?

即使“计时器”是上面列出的默认队列之一,Ember也仅在其RunLoop测试用例中引用该队列。根据上述文章中有关定时器是最后要触发的事情的一些描述,似乎在SproutCore时代中会使用这种队列。在Ember中,timers不使用队列。相反,可以通过内部管理的setTimeout事件(请参见invokeLaterTimers函数)来启动RunLoop,该事件足够智能以遍历所有现有计时器,触发所有已到期的计时器,确定最早的将来计时器并设置内部计时器。setTimeout仅针对该事件,它将在运行时再次启动RunLoop。这种方法比让每个计时器调用setTimeout并自动唤醒的效率更高,因为在这种情况下,只需要进行一次setTimeout调用,并且RunLoop足够聪明,可以触发所有可能同时关闭的不同计时器时间。

sync队列进一步消除抖动

这是运行循环中的一个片段,位于运行循环中所有队列的循环中间。请注意该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);
}

希望这可以帮助。我绝对必须学到很多东西才能写这个东西,这很重要。


3
很棒的文章!我听说有传言说“观察者立即开火”的情况可能会在某个时候发生变化,使它们像装订一样延迟。
乔·利斯

@JoLiss是啊,我觉得我已经听说过了几个月......不知道是否/何时它会让进去。
亚历山大·华莱士Matchneer

1
Brendan Briggs在2014年1月的Ember.js纽约市聚会上做了关于Run Loop的精彩演讲。此处的视频:youtube.com/watch?v = iCZUKFNXA0k
Luke Melia 2014年

1
这个答案是我找到的关于Ember Run Loop的最佳资源,非常好!我最近根据您的工作发布了有关Run Loop的详尽教程,我希望它描述该机制的更多细节。可在以下网站上
KubaNiechciał15年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.