JavaScript的并发模型基于"event loop"。这个模型与其他在C或者Java中的模型着实不同。
运行时概念
下面的内容解释了一个理论上的模型。现代JavaScript引擎着重实现和优化了描述的几个语义。
可视化描述
栈
函数调用形成了一个frames的栈。
function f(b){ var a = 12; return a+b+35;}function g(x){ var m = 4; return f(m*x);}g(21);
调用g的时候,创建了第一个frame,包含了
g的变量以及其本地变量。当
g
调用 f 的时候,第二个frame就被创建、并置于第一个frame之上,包含了f的参数和本地变量。当f返回时,最上层的frame就出栈了(剩下g函数调用的frame)。当g返回的时候,栈就空了。
堆
对象被分配在一个堆中,一个用以表示一个内存中大的未被组织的区域。
队列
一个JavaScript运行时包含了一个待处理的消息队列。每一个消息都与一个函数相关联。当栈为空时,从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因此而创建的一个初始栈结构)。当栈再次为空的时候,也就意味着消息处理结束。
事件循环
之所以称为 事件循环,是因为它经常被用于类似如下的方式来实现:
while(queue.waitForMessage()){ queue.processNextMessage(); }
如果当前没有任何消息,queue.waitForMessage
会同步等待消息到来。
"执行至完成"
每一个消息执行完成后,其它消息才会被执行。当你分析你的程序时,这点提供了一些优秀的特性,包括当一个函数运行时,它不能被取代且会在其它代码运行前先完成(而且能够修改这个函数控制的数据)。这点与C语言不同。例如,C语言中当一个程序在一个线程中运行时,它可以在任何点停止且可以在其它线程中运行其它代码。
这个模型的一个缺点在于当一个消息的完成耗时过长,网络应用无法处理用户的交互如点击或者滚动。浏览器用“程序需要过长时间运行”的对话框来缓解这个问题。一个比较好的解决方案是使消息处理变短且如果可能的话,将一个消息拆分成几个消息。
添加消息
在浏览器里,当一个事件出现且有一个事件监听器被绑定时,消息会被随时添加。如果没有事件监听器,事件会丢失。所以点击一个附带点击事件处理函数的元素会添加一个消息。其它事件亦然。
调用 setTimeout 函数会在一个时间段过去后在队列中添加一个消息。这个时间段作为函数的第二个参数被传入。如果队列中没有其它消息,消息会被马上处理。但是,如果有其它消息,setTimeout消息必须等待其它消息处理完。因此第二个参数仅仅表示最少的时间 而非保证的时间。
几个运行时(Runtime)互相通信
一个web worker或者一个跨域的iframe都有它们自己的栈,堆和消息队列。两个不同的运行时只有通过postMessage
方法进行通信。这个方法会给另一个运行时添加一个消息如果后者监听了message事件。
绝不阻塞
一个很有趣的事件循环(event loop)模型特性在于,Javascript跟其它语言不同,它永不阻塞。处理I/O (input/output)通常由事件或者回调函数进行实现。所以当一个应用正等待IndexedDB的查询的返回或者一个XHR(译者注:用于Ajax)的请求返回时,它仍然可以处理其它事情例如用户输入。
例外是存在的,如alert或者同步XHR,但避免它们被认为是最佳实践。注意的是,例外的例外也是存在的(但通常是实现错误而非其它原因)。