乐趣区

关于前端:再谈javaScript的事件循环模型

并发模型与事件循环

JavaScript 有一个基于 事件循环 的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子工作。这个模型与其它语言中的模型截然不同,比方 C 和 Java。

函数调用造成了一个由若干帧组成的栈。

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

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

console.log(bar(7)); // 返回 42

当调用 bar 时,第一个帧被创立并压入栈中,帧中蕴含了 bar 的参数和局部变量。当 bar 调用 foo 时,第二个帧被创立并被压入栈中,放在第一个帧之上,帧中蕴含 foo 的参数和局部变量。当 foo 执行结束而后返回时,第二个帧就被弹出栈(剩下 bar 函数的调用帧)。当 bar 也执行结束而后返回时,第一个帧也被弹出,栈就被清空了。

对象被调配在堆中,堆是一个用来示意一大块(通常是非结构化的)内存区域的计算机术语。

队列

工作 同步工作没有队列,异步工作才有队列

所有工作能够分成两种:

  1. 同步工作(synchronous)
    主线程上排队执行的工作,只有前一个工作执行结束,能力执行后一个工作
  2. 异步工作(asynchronous)
    不进入主线程、主线程主动读取工作队列,” 工作队列 ” 上第一位 (先进先出),则该工作(音讯) 关联的处理函数 (回调函数) 进入主线程执行

一个 JavaScript 运行时蕴含了一个待处理音讯的音讯队列。每一个音讯都关联着一个用以解决这个音讯的回调函数。

在 事件循环 期间的某个时刻,运行时会从最先进入队列的音讯开始解决队列中的音讯。被解决的音讯会被移出队列,并作为输出参数来调用与之关联的函数。正如后面所提到的,调用一个函数总是会为其发明一个新的栈帧。

函数的解决会始终进行到执行栈再次为空为止;而后事件循环将会解决队列中的下一个音讯(如果还有的话)。

事件循环

Event Loop(事件循环):主线程从 ” 工作队列 ” 中读取事件,整个过程循环不断

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

queue.waitForMessage() 

会同步地期待音讯达到 (如果以后没有任何音讯期待被解决)。

执行至实现

每一个音讯残缺地执行后,其它音讯才会被执行。这为程序的剖析提供了一些优良的个性,包含:当一个函数执行时,它不会被抢占,只有在它运行结束之后才会去运行任何其余的代码,能力批改这个函数操作的数据。这与 C 语言不同,例如,如果函数在线程中运行,它可能在任何地位被终止,而后在另一个线程中运行其余代码。

这个模型的一个毛病在于当一个音讯须要太长时间能力处理完毕时,Web 应用程序就无奈解决与用户的交互,例如点击或滚动。为了缓解这个问题,浏览器个别会弹出一个“这个脚本运行工夫过长”的对话框。一个良好的习惯是缩短单个音讯解决工夫,并在可能的状况下将一个音讯裁剪成多个音讯。

增加音讯

在浏览器里,每当一个事件产生并且有一个事件监听器绑定在该事件上时,一个音讯就会被增加进音讯队列。如果没有事件监听器,这个事件将会失落。所以当一个带有点击事件处理器的元素被点击时,就会像其余事件一样产生一个相似的音讯。

函数 setTimeout 承受两个参数:待退出队列的音讯和一个工夫值(可选,默认为 0)。这个工夫值代表了音讯被理论退出到队列的最小延迟时间。如果队列中没有其它音讯并且栈为空,在这段延迟时间过来之后,音讯会被马上解决。然而,如果有其它音讯,setTimeout 音讯必须期待其它音讯解决完。因而第二个参数仅仅示意起码延迟时间,而非确切的等待时间。

上面的例子演示了这个概念(setTimeout 并不会在计时器到期之后间接执行):

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,但应该尽量避免应用它们。留神,例外的例外也是存在的(但通常是实现谬误而非其它起因)

退出移动版