likes
comments
collection
share

Javascript并发模型和事件循环

作者站长头像
站长
· 阅读数 1

JavaScript的并发模型基于“事件循环”

JavaScript运行时概念

可视化描述

Javascript并发模型和事件循环

函数调用形成一个栈帧

function foo(b) {
    var a = 10;
    return a + b + 11;
}

function bar(x) {
    var y = 3;
    return foo(x * y);
}

console.log(bar(7)); // 42
  1. 当调用bar时,创建了第一个帧,帧中包含了bar的参数和局部变量
  2. 当bar中调用foo时,第二个帧就被创建,并被压到第一个帧之上,帧中包含了foo的参数和局部变量
  3. 当foo返回时,最上层的帧就被弹出栈(剩下bar的调用栈),当bar返回的时候,栈就空了

对象被分配在一个堆中,即用以表示一大块非结构化的内存区域

队列

一个JavsScript运行时包含了一个待处理的消息队列;每一个消息都关联着一个用以处理这个消息的函数。

在 事件循环 期间的某个时刻,运行时从最先进入队列的消息开始处理队列中的消息。为此,这个队列消息会被移出队列,并作为输入参数调用与之关联的函数。正如前面所提到,调用一个函数总是会为其创造一个新的栈帧。

函数的处理会一直进行到执行栈再次为空未知;然后事件循环将会处理队列中下一个消息(如果还有的话)

事件循环

之所以称之为事件循环,是因为它经常按照类似如下的方式来被实现:

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

如果当前没有任何消息,queue.waitForMessage()会同步等待信息到达

"执行至完成"

每一个消息完整地执行后,其他消息才会被执行

这为程序分析提供了一些优秀的特性,包括,一个函数执行时,它永远不会被抢占,并且在其他代码运行之前完全运行(且可以修改此函数操作的数据)

这个模型的缺点在于当一个消息需要太长时间才能处理完毕时,web应用就无法处理用户的交互,例如点击或滚动。 一个很好的做法就是缩短消息处理,并在可能的情况下将一个消息裁剪成多个消息。

添加消息

在浏览器里,当一个事件发生且有一个事件监听器绑定在该事件上时,消息会被随时添加进队列。如果没有事件监听器,事件就会丢失。所以点击一个附带点击事件处理函数的元素会添加一个消息,其他事件类似。

<div id="box"></div>

var box = document.getElementById('box');
box.onClick = function() { console.log('click') }  // 绑定了事件监听器
box.onClick = null    // 没有事件监听器,事件就会丢失

setTimeout

函数setTimeout接受两个参数: 待加入队列的消息和一个延迟(可选,默认为0)。 这个延迟代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其他消息,在这段延迟时间过去之后,消息会被马上处理。但是,如果有其他消息,setTimeout消息必须等待其他消息处理完。

setTimeout(function() {
    console.log("zhixing")
}, 200}

因此,第二个参数(200)仅仅表示最少延迟时间,而非确切的等待时间。

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

setTimeout(function() {
  // 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行
  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;
  }
}

零延迟

零延迟并不意味着回调会立即执行。以0为第二个参数用setTimeout并不表示0毫秒后立即调用回调函数。

其等待的时间取决于队列里待处理的消息数量。

基本上setTimeout需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间。

(function() {

  console.log('这是开始');

  setTimeout(function cb() {
    console.log('这是来自第一个回调的消息');
  });

  console.log('这是一条消息');

  setTimeout(function cb1() {
    console.log('这是来自第二个回调的消息');
  }, 0);

  console.log('这是结束');

})();

// "这是开始"
// "这是一条消息"
// "这是结束"
// "这是来自第一个回调的消息"
// "这是来自第二个回调的消息"

多个运行时互相通信

一个web worker或则一个跨域的iframe都有自己的栈,堆和消息队列。两个不同的运行时只能通过postMessage方法进行通信。如果另一个运行时侦听message事件,则此方法会向该运行时添加消息。

永不阻塞

事件循环模型的一个非常有趣的特性是,与许多其他语言不同,JavaScript永不阻塞。处理I/O通常通过事件和回调来执行,所以当一个应用正等待一个IndexedDB查询返回或则一个XHR请求返回时,它仍然可以处理其它事情,比如用户输入。

遗留的意外也是存在的,如alert或者通过XHR,但应该尽量避免使用它们

我只是MDN搬运工 (MDN地址)developer.mozilla.org/zh-CN/docs/…