FrankKai.github.io
FrankKai.github.io copied to clipboard
[译]如何理解并发模型(concurrency model)和事件循环(event loop)?
- 初识并发模型和事件循环
- 运行时概念
- 可视化呈现
- 栈(stack)
- 堆(Heap)
- 队列(Queue)
- 事件循环
- 初识事件循环
- ”执行到完成“
- 增加消息
- 零延迟(Zero delay)
- 多个运行时互相通信
- 从不阻塞
初识并发模型和事件循环
JavaScript拥有基于event loop的并发模型。 它和C和Java的语言的model非常不同。
基于event loop的并发模型是什么?
- 负责代码的执行
- 收集和处理事件
- 执行队列子任务
运行时概念
下面的这些部分解释了理论模型。 现代的JavaScript引擎实现并且极大程度的去优化了这些实现。
可视化呈现
栈(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
过程拆解:
- 调用bar时,包含bar的arguments和本地变量的第一个frame创建了
- 当bar调用foo时,包含了foo的arguments和本地变量的第二个frame创建成功,并且将其推入顶部
- 当foo返回时,顶部的frame element会从stack中pop出来(留下bar在栈中)
- 当bar返回时,stack清空
可视化拆解:
堆(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与message queue的关系
- 为什么有些时候setTimeout的延迟时间会不准?
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