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

Posted

技术标签:

【中文标题】什么是 Ember RunLoop,它是如何工作的?【英文标题】:What is Ember RunLoop and how does it work? 【发布时间】:2012-11-15 21:05:40 【问题描述】:

我正在尝试了解 Ember RunLoop 的工作原理以及它的运行原理。我看过the documentation,但仍然有很多问题。我有兴趣更好地了解 RunLoop 的工作原理,以便我可以在其名称空间中选择适当的方法,当我不得不推迟执行某些代码时。

Ember RunLoop 何时开始。它是否依赖于路由器或视图或控制器或其他东西? 大约需要多长时间(我知道这很愚蠢,并且依赖于许多事情,但我正在寻找一个总体思路,或者是否存在运行循环可能需要的最短或最长时间) RunLoop 是一直在执行,还是只是表示从执行开始到结束的一段时间,可能有一段时间不运行。 如果视图是从一个 RunLoop 中创建的,是否可以保证在循环结束时其所有内容都会进入 DOM?

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

【问题讨论】:

没有关于运行循环的优秀文档。本周我将尝试在上面制作一个简短的幻灯片。 @LukeMelia 这个问题仍然非常需要您的关注,看起来其他人正在寻找相同的信息。如果您有机会分享您对 RunLoop 的见解,那就太好了。 【参考方案1】:

2013 年 10 月 9 日更新:查看这个运行循环的交互式可视化:https://machty.s3.amazonaws.com/ember-run-loop-visual/index.html

2013 年 5 月 9 日更新:以下所有基本概念仍然是最新的,但截至 this commit,Ember Run Loop 实现已被拆分为一个名为 @ 的单独库987654323@,有一些非常小的 API 差异。

首先,请阅读以下内容:

http://blog.sproutcore.com/the-run-loop-part-1/

http://blog.sproutcore.com/the-run-loop-part-2/

它们并非 100% 准确于 Ember,但 RunLoop 背后的核心概念和动机仍然普遍适用于 Ember;只有一些实现细节不同。但是,关于您的问题:

Ember RunLoop 何时开始。它是否依赖于路由器或视图或控制器或其他东西?

所有基本的用户事件(例如键盘事件、鼠标事件等)都会启动运行循环。这保证了捕获的(鼠标/键盘/计时器/等)事件对绑定属性所做的任何更改都会在整个 Ember 的数据绑定系统中完全传播,然后再将控制权返回给系统。因此,移动鼠标、按键、单击按钮等都会启动运行循环。

大约需要多长时间(我知道这很愚蠢,并且依赖于许多事情,但我正在寻找一个总体思路,或者是否有一个 runloop 可能需要的最短或最长时间)

RunLoop 永远不会跟踪通过系统传播所有更改并在达到最大时间限制后停止 RunLoop 所花费的时间;相反,RunLoop 将始终运行到完成,并且不会停止,直到所有过期的计时器都被调用,绑定传播,也许 它们的 绑定传播,等等。显然,需要从单个事件传播的更改越多,RunLoop 完成所需的时间就越长。这是一个(非常不公平的)示例,说明与另一个没有运行循环的框架(主干)相比,RunLoop 如何因传播更改而陷入困境:http://jsfiddle.net/jashkenas/CGSd5/。故事的寓意:RunLoop 对于您想要在 Ember 中做的大多数事情都非常快,这也是 Ember 的强大功能所在,但是如果您发现自己想用 javascript 以每秒 60 帧的速度制作 30 个圆圈,那么可能比依赖 Ember 的 RunLoop 更好。

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

它不是一直执行的——它必须在某个时候将控制权返回给系统,否则你的应用程序会挂起——它不同于服务器上的运行循环,例如,具有while(true)并无限运行,直到服务器收到关闭信号... Ember RunLoop 没有这样的while(true),但只是响应用户/计时器事件而启动。

如果从一个 RunLoop 中创建视图,是否保证其所有内容在循环结束时都会进入 DOM?

让我们看看能不能解决这个问题。从 SC 到 Ember RunLoop 的一个重大变化是,Ember 不是在 invokeOnceinvokeLast 之间来回循环(您在第一个关于 SproutCore 的 RL 链接的图表中看到),而是为您提供了一个“ queues',在运行循环过程中,您可以通过指定动作所属的队列来安排动作(在运行循环期间调用的函数)(来自源的示例:Ember.run.scheduleOnce('render', bindView, 'rerender');)。

如果您查看源代码中的run_loop.js,您会看到Ember.run.queues = ['sync', 'actions', 'destroy', 'timers'];,但是如果您在Ember 应用程序的浏览器中打开您的JavaScript 调试器并评估Ember.run.queues,您会得到更完整的队列列表:@ 987654338@。 Ember 保持他们的代码库非常模块化,它们使您的代码以及库的单独部分中的自己的代码可以插入更多队列。在这种情况下,Ember Views 库插入 renderafterRender 队列,特别是在 actions 队列之后。我马上就会明白为什么会这样。一、RunLoop算法:

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

您在 RunLoop .begin().end() 之间运行您的代码,仅在 Ember 中您需要在 Ember.run 中运行您的代码,这将在内部为您调用 beginend。 (Ember 代码库中只有内部运行循环代码仍然使用beginend,所以你应该坚持使用Ember.run) 在调用end() 之后,RunLoop 将启动以传播由传递给Ember.run 函数的代码块所做的每一个更改。这包括传播绑定属性的值、将视图更改渲染到 DOM 等。这些操作(绑定、渲染 DOM 元素等)的执行顺序由上述Ember.run.queues 数组确定: 运行循环将从第一个队列开始,即sync。它将运行由Ember.run 代码安排到sync 队列中的所有操作。这些动作本身也可能在同一个 RunLoop 期间安排更多动作执行,并且由 RunLoop 确保它执行每个动作,直到所有队列都被刷新。它这样做的方式是,在每个队列的末尾,RunLoop 将查看所有先前刷新的队列,并查看是否已安排任何新操作。如果是这样,它必须从具有未执行的预定操作的最早队列的开头开始并清除队列,继续跟踪其步骤并在必要时重新开始,直到所有队列完全为空。

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

队列的顺序,包括由 Ember 视图库添加的队列,在这里很重要。请注意,renderafterRendersyncaction 之后。 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。它们之间的区别并不像知道它们存在那样重要,以及它们如何丢弃队列中的重复操作以防止在运行循环期间进行大量臃肿、浪费的计算。

计时器呢?

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

使用sync 队列进一步去抖动

这是运行循环中的一个 sn-p,位于运行循环中所有队列的循环中间。注意sync 队列的特殊情况:因为sync 是一个特别不稳定的队列,其中数据在各个方向传播,所以调用Ember.beginPropertyChanges() 以防止任何观察者被触发,然后调用@ 987654379@。这是明智的:如果在刷新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);

希望这会有所帮助。为了写这个东西,我肯定需要学习很多东西,这很重要。

【讨论】:

好文章!我听说“观察者立即开火”的事情可能会在某个时候改变,使它们像绑定一样延迟。 @JoLiss 是的,我觉得我已经听说了几个月......不确定它是否/何时会进入。 Brendan Briggs 在 2014 年 1 月的 Ember.js NYC 聚会上做了一个关于 Run Loop 的精彩演讲。视频在这里:youtube.com/watch?v=iCZUKFNXA0k 这个答案是我找到的关于 Ember Run Loop 的最佳资源,非常棒!我最近根据您的工作发布了一个关于运行循环的详尽教程,我希望能描述该机制的更多细节。可在此处获取on.netguru.co/ember-ebook-form

以上是关于什么是 Ember RunLoop,它是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章

没有服务器的Ember Js App

RunLoop基础

RunLoop的学习总结

RunLoop的学习总结

RunLoop的学习总结

Runloop