FrankKai / FrankKai.github.io

FE blog
https://frankkai.github.io/
362 stars 39 forks source link

[译]如何理解并发模型(concurrency model)和事件循环(event loop)? #216

Open FrankKai opened 4 years ago

FrankKai commented 4 years ago
FrankKai commented 4 years ago

初识并发模型和事件循环

JavaScript拥有基于event loop的并发模型。 它和C和Java的语言的model非常不同。

基于event loop的并发模型是什么?

运行时概念

下面的这些部分解释了理论模型。 现代的JavaScript引擎实现并且极大程度的去优化了这些实现。

可视化呈现

image

栈(stack)

从frames stack中调用函数。

function foo(){
    let a = 10;
    return a + b + 11
}
function bar(x){
    let y = 3;
    return foo(x*y);
}
console.log(bar(7)); // 返回42

过程拆解:

可视化拆解:

image

image

image

堆(Heap)

对象被分配在heap中,之所以称之为堆是因为它会占用大量的内存(很多都是非结构化的)。

队列(Queue)

JavaScript运行时使用的是message queue,它的意思是有一个消息列表去处理。 为了处理消息,每个消息都有一个与之相关的函数去调用。

在事件循环的一些点,runtime开始处理队列中的消息,从最老的一个开始。为了做到这样,消息会从队列中移除,并且使用消息作为输入参数调用相应的函数。通常来讲,调用一个函数会在栈中生成一个新的frame。

函数的处理一直会进行,知道stack被清空。然后event loop去处理下一个在队列中的消息。

如何理解”使用消息作为输入参数调用相应的函数“?

上面的例子中,foo执行完之后会从call stack中出栈,也就是说这个foo的消息从消息队列中移除。 来思考一下,仅仅是移除了就完事了吗? 当然不是。 foo执行完毕之后,会将返回值作为输入值传到bar中,调用return foo(x*y)。 使用foo这个消息作为输入参数去调用bar这个函数。 也就是我们说的”使用消息作为输入参数调用相应的函数。“

事件循环

初识事件循环

event loop之所以叫这个名字,是因为它的实现方式,可以看成下面的伪代码:

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

queue.waitForMessage()同步等待消息到达(如果有一个消息是可用并且是等待处理的。)

”执行到完成“

每个消息都会在下一个消息处理完成前完成。

这为你的程序带来了很大的好处,其中包括:无论何时调用一个函数,它都不能被预清空而且会在任何的代码运行前完全运行(而且可以修改函数操作的数据)。这和C不一样,例如:如果一个函数运行在一个线程中,它也许会在任何时间被运行系统停止,然后去其它的线程去运行其他的代码。

这种模型有一个缺点:如果一个消息花了很长时间才能完成,web应用不能到达去处理用户的click或者scroll事件。浏览器可以通过一个”script占用过长的时间去执行“的对话框告知用户。一个很好的实践是使得message变短,并且尽可能将一个消息拆解成多个消息。

增加消息

在web浏览器中,消息可以在时间发生的任意时间被添加,并且会有一个事件listener附加在其上。如果没有listener,event会丢失。所以当一个click事件发生在元素上时,click事件的处理器会添加一个message。其他任何的事件都是这样的。

setTimeout添加message到queue的过程

setTimeout有两个参数:第一callback是被增加到queue的message,第二个是最小时间(默认为0)。time的值代表着message被推入到queue的最小时间。如果没有其他的消息在queue中,并且这个栈是空的,消息会在delay的时间后被处理。然而,如果有消息,setTimeout的消息需要等待其他的消息处理完成后再执行。 因为这个原因,第二个参数代表着最小时间,并不是保证时间。

“setTimeout不能准时执行”可以看下面这个例子去理解:

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

setTimeout(function() {
  // 打印出了2,意味着并没有准时在500ms后执行
  console.log("在(new Date().getSeconds() - s)秒后打印。");
}, 500)

while (true) {
  if (new Date().getSeconds() - s >= 2) {
    console.log("循环了2秒")
    break;
  }
}

零延迟(Zero delay)

零延迟的意思是函数不能在0ms后立即执行。 比如setTimeout传入了一个0毫秒的时间延迟,但是它并不会在0毫秒后执行回调函数。

执行需要依赖队列中等待的任务数量。在下面的例子中,消息this is just a message会在回调中的消息被处理之前被写入console,因为delay是最小的时间并不是精准的保证时间。

基本上,setTimeout需要等待队列消息中的所有代码执行完成,即使你为setTimout指定了一个精准的时间。

(function() {

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

  setTimeout(function cb() {
    console.log('Callback 1: this is a msg from call back');
  }); // has a default time value of 0

  console.log('this is just a message');

  setTimeout(function cb1() {
    console.log('Callback 2: this is a msg from call back');
  }, 0);

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

})();

// "this is the start"
// "this is just a message"
// "this is the end"
// "Callback 1: this is a msg from call back"
// "Callback 2: this is a msg from call back"

多个运行时互相通信

一个web worker或者是跨域的iframe都有自己的stack,heap和消息队列。 两个截然不同的runtime(运行时)可以通过postMessage方法去互相传递消息。 这个方法会把消息增加到另一个运行时中(如果后者有一个message事件的话)。

从不阻塞

JavaScript的事件循环模型有一个非常有趣的属性,它不像其他的语言,js不会阻塞。 通过events和callbacks处理I/O,所以当应用等待IndexedDB的查询结果时,或者是等待XHR请求返回时,它能够处理其他类似用户输入这些事情。 有一些遗留的异常类似alert或者同步XHR,但是最好是避免使用它们。 异常的异常是存在的,用遗留的异常只能带来bug,带不来别的东西。

参考资料:https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop