JavaScript Concurrency model and Event Loop 并发模型和事件循环机制

原文地址:https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop

JavaScript 有一个基于 event loop 的并发模型,这个模型和其他如 Java 和 C 语言的模型是不同的。

Runtime concepts 运行时概念

下面是一个可视化的模型展示,现代 JavaScript 引擎实现并着重优化了这个模型。

JavaScript Concurrency model and Event Loop 并发模型和事件循环机制

Stack 栈

函数从一个调用栈中执行,这个栈由多个调用栈帧组成

function foo(b) {
var a = ;
return a + b + ;
} function bar(x) {
var y = ;
return foo(x * y);
} console.log(bar()); //returns 42

调用 bar 时,第一个调用帧被创建,这个帧包含 bar 的参数和局部变量,在 bar 内部调用 foo,第二个调用帧被创建,然后入栈,放在第一个调用帧的上面。

当 foo 执行完成以后,栈顶的帧就会出栈。当 bar 也执行完成之后,栈为空。

Heap 堆

对象被分配到一个堆中,堆这个名字就象征着一大块没有结构的内存区域。

Queue 队列

一个 JavaScript 运行时有一个消息队列(message queue),这是一个待处理消息列表,每个消息有相应的函数会被调用来处理这个消息。

在 event loop 的某个时刻,运行时开始处理队列中的消息,从最早的那一个开始。这个消息将被移出队列,对应的函数会被调用,并将这个消息作为参数。

同样和前面一样,这个函数调用也会给这个函数产生一个调用栈帧。

函数执行的过程会一直持续到调用栈再次为空,然后 event loop 会继续处理消息队列中的下一个消息 (如果有的话)

Event loop 事件循环

事件循环的实现原理类似于下面的伪代码:

while (queue.waitForMessage()) {
queue.processNextMessage();
}

如果当前没有事件消息,queue.waitForMessage() 会同步地等待

Run-to-completion 执行到完成

在其它消息被处理之前,当前的消息会完全处理结束。这在你分析自己的代码时有非常好的特性,比如当一个函数执行时,它不会被提前阻止,在其它代码运行之前会完整地执行(这些代码可能会改变此函数操作的数据)。这个 C 语言不同,例如,当一个线程中的一个函数运行,它可能会被中止,因为运行时要去处理其它线程中的另外一些代码。

这个模型的一个缺点就是,如果一个消息需要过长的处理时间,web 应用将无法处理其它如点击或者鼠标滚动事件。浏览器会弹出对话框“程序需要过长事件无法运行”的对话框来处理这种消息。一个好的实践是将消息处理时长减少,如果可以,将一个消息分成多个消息。

Adding message 添加消息

在 web 浏览器中,只要一个事件发生并且有一个事件监听绑定到这个事件,就会添加消息到队列中。如果没有监听到,事件就丢失了。

函数 setTimeout 有两个参数,一个将添加到队列中的消息,一个代表延迟的时间值(毫秒,默认为0)。这个时间代表添加到队列前的最少等待时间。

如果队列中没有其它消息,则这个消息将在延迟之后立即被处理。然而,如果有其它消息,setTimeout 的消息还要等待其它消息处理。所以第二个参数仅仅代表最少时间,并不保证确切的时间长度。

下面是一个例子

const s = new Date().getSeconds();

setTimeout(function() {
// prints out "2", meaning that the callback is not called immediately after 500 milliseconds.
console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500); while(true) {
if(new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}

Zero delays 零延时

延时设置为 0 并不表示回调函数会立即执行,是否执行取决于队列中等待的任务的数量。

在下面的例子中,cb1 是最后执行的,因为它要等待队列前面的任务执行完毕。

(function() {

  console.log('this is the start');

  setTimeout(function cb() {
console.log('this is a msg from call back');
}); console.log('this is just a message'); setTimeout(function cb1() {
console.log('this is a msg from call back1');
}, 0); console.log('this is the end'); })(); // "this is the start"
// "this is just a message"
// "this is the end"
// note that function return, which is undefined, happens here
// "this is a msg from call back"
// "this is a msg from call back1"

几个运行时之间通信

一个 web worker 或者 一个跨域的 iframe 都要自己的栈、堆和消息队列。

两个不同的运行时只能通过 postMessage 方法进行通信。

另外一个运行时如果监听 message 时间,就可以添加消息到自己的队列。

Never blocking 非阻塞

时间循环机制的一个非常有趣的特性的就是永远不会阻塞。

处理 I/O 事件通常都通过事件和回调函数执行。当应用在等待一个数据库查询或者 XHR 请求的结果时,可以同时处理其它事情比如用户输入。

遗留问题也是存在的,比如 alert 或者 同步的XHR,不过好的实践是避免使用它们。

上一篇:js算法初窥04(算法模式01-递归)


下一篇:git命令行获取某分支代码