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

Ara*_*ras 96 ember.js

我试图了解Ember RunLoop是如何工作的以及是什么让它成功.我查看了文档,但仍然有很多问题.我有兴趣更好地理解RunLoop的工作原理,所以我可以在其名称空间中选择适当的方法,当我不得不推迟执行某些代码时.

  • Ember RunLoop什么时候开始.它依赖于路由器或视图或控制器或其他东西?
  • 它花了多长时间(我知道这很愚蠢,要求并依赖于许多事情,但我正在寻找一个大概,或者如果有一个最小或最大的时间,一个runloop可能会采取)
  • RunLoop是否始终执行,或者它只是指示从开始到结束执行的一段时间,并且可能不会运行一段时间.
  • 如果从一个RunLoop中创建一个视图,它是否保证在循环结束时它的所有内容都会进入DOM?

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

Ale*_*eer 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完成所需的时间越长.这是一个(非常不公平)的例子,说明与没有运行循环的另一个框架(Backbone)相比,RunLoop如何陷入传播变化:http://jsfiddle.net/jashkenas/CGSd5/.故事的道德:RunLoop对于你在Ember中想要做的大多数事情都非常快,而且它是Ember的力量所在,但如果你发现自己想要用每秒60帧的Javascript动画30个圈子,那里可能是比依靠Ember的RunLoop更好的方法.

RunLoop是否始终执行,或者它只是指示从开始到结束执行的一段时间,并且可能不会运行一段时间.

它不会一直执行 - 它必须在某个时刻将控制权返回给系统,否则你的应用程序会挂起 - 它与服务器上的运行循环不同,该服务器具有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库插入renderafterRender队列,特别是在actions队列之后.我会明白为什么可能会在一秒钟之内.首先,RunLoop算法:

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

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

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

队列的顺序,包括Ember Views库添加的队列,在这里很重要.请注意,render然后afterRendersync,和action.该sync队列包含了所有传播绑定数据的操作.(action之后,只在Ember源中使用很少).基于上述算法,可以保证在RunLoop到达render队列时,所有数据绑定都将完成同步.这是设计的:您不希望同步数据绑定之前执行渲染DOM元素的昂贵任务,因为这可能需要使用更新的数据重新呈现DOM元素 - 显然效率非常低且错误 -清空所有RunLoop队列的倾向性方法.因此,在渲染render队列中的DOM元素之前,Ember会智能地完成所有数据绑定工作.

所以,最后,回答你的问题,是的,你可以期待任何必要的DOM渲染将在时间Ember.run结束时发生.这里有一个jsFiddle演示:http://jsfiddle.net/machty/6p6XJ/328/

关于RunLoop的其他信息

观察者与绑定

值得注意的是,Observers和Bindings虽然具有响应"监视"属性中的更改的类似功能,但在RunLoop的上下文中表现完全不同.正如我们所见,绑定传播被调度到sync队列中,最终由RunLoop执行.另一方面,观察者在被监视的属性发生变化时立即触发,而不必首先被安排到RunLoop队列中.如果Observer和绑定都"监视"相同的属性,则观察者将始终比绑定更新时的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队列进行去抖动

这是来自运行循环的片段,在循环的中间通过运行循环中的所有队列.请注意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);
}
Run Code Online (Sandbox Code Playgroud)

希望这可以帮助.我一定要学习写这个东西,这有点重要.

  • @Alexander哇!做得好!我无法更好地解释这一点,而且我一直在使用该代码. (27认同)
  • @Alexander感谢您抽出宝贵时间撰写如此深入而富有启发性的答案.你有关于Ember的博客吗?我想订阅! (3认同)
  • 很棒的写作!我听说有传言说"观察员立即开火"的事情可能会在某些时候发生变化,使他们像绑定一样延迟. (3认同)
  • Brendan Briggs 在 2014 年 1 月的 Ember.js 纽约聚会上做了关于 Run Loop 的精彩演讲。视频在这里:http://www.youtube.com/watch?v=iCZUKFNXA0k (2认同)