📝使用 Event Loop

JavaScript 的 runtime model(运行时模型)是基于 event loop(事件循环)的 。event loop 负责 execute 代码、collect 和 process 事件、execute queued sub-tasks。

1. JavaScript 的单线程

2. runtime 的相关概念

关于 runtime 的理论模型,如下:

2.1 heap

对象在堆中分配,堆只是一个名称,表示一个大的(大部分是非结构化的)内存区域。

2.2 stack

function calls(函数调用)形成一个 frames stack。

2.3 queue

JavaScript runtime 会使用一个 message queue 来表示要处理的消息列表,每个 message 都有一个关联的 function 来处理它。

在 event loop 期间的某个时刻,runtime 会开始处理 queue 里的 message,从队头(queue,先进先出)取一个。即把那个 message 从 queue 中移除,同时将 message 作为输入参数调用它所对应的 function。为了执行 function,就会给它创建一个新的 stack frame。

直到 stack 再次空了,对 function 的处理才会停止。接着,event loop 会处理 queue 里的下一个 message(如果有的话)。

3. Event loop

之所以叫 event loop 就是因为它是一个处理 event 的 loop,其实现通常类似于:

// 同步等待 message 的到来(??? 莫非这里就是指的 JS 的主线程)
while (queue.waitForMessage()) {
    // 取出一个,来执行
    queue.processNextMessage()
}

"Run-to-completion"

每个 message 被处理完之后,才会处理下一个 message。

无论一个 function 什么时候运行,它都不能被抢占,在它运行完之前任何其它代码都不能被执行。 这一点和 C 语言不同,在 C 语言中,当一个 function 正在一个 thread 中运行着,它可能随时被 runtime system 停止以运行另一个 thread 中的其它代码。(嗯~ JS 语言是单线程的原因)

这种 model 的缺点是,如果一个 message 需要执行很长时间,要么 web application 将无法处理 user interactions(比如 click, scoll)。 此时,浏览器会弹个对话框以提示用户“a script is taking too long to run”。 好的做法是:缩短 message 的处理时间,(如果可能)也可以把它拆成多个 messages。

Adding messages

在 web browser 中,只要 event 发生,就会添加 message,并且会为其附加一个 event listener。 因此,click 一个具有 click event handler 的元素就会添加一个 message,其它 event 也类似。

函数 setTimeout 有两个参数:要添加到 queue 里的 message 和一个 time value(可选,默认是 0)。 time value 表示该 message 被 push 到 queue 里的(minimum)delay。 如果 queue 里没有其它 message 并且 stack 是空,那么 message 就会在 delay 之后被立即处理。 然而,如果 queue 里还有其它 message,那么 setTimeout 的 message 将不得不等待它前面的 messages 都被处理完了。 正因为这样,第二个参数 time value 表示的是 minimum time(最短时间)而不是 guaranteed time(保证时间)。

const now = new Date().getTime() / 1000;

// 设置的是在 500 毫秒之后执行,但真正执行的时候是在 2+ 秒之后
setTimeout(() => {
    // 会输出:在 2 秒之后运行
    console.log(`在 ${new Date().getTime() / 1000 - now} 秒之后运行`);
}, 500);

while (true) {
    // 模拟一个需要 2 秒的操作
    if (new Date().getTime() / 1000 - now >= 2) {
        break
    }
}

Zero delays

0 delay 并不意味着回调将在 0 毫秒之后触发。 以 0 毫秒的 delay 调用 setTimeout 不会在给定的 interval(时间间隔)之后执行 callback function。 执行取决于 queue 里等待 tasks 的数量。

在下面的示例中,消息“这只是一条消息”将在回调中的消息得到处理之前写入控制台,因为延迟是运行时处理请求所需的最短时间(不是保证时间) .

setTimeout 需要等待排队消息的所有代码完成,即使您为 setTimeout 指定了特定的时间限制。

(() => {
    console.log('start')

    setTimeout(() => {
        console.log('this is a message from call back 1')
    }) // time value 默认是 0

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

    setTimeout(() => {
        console.log('this is a message from call back 2')
    }, 0)

    console.log('end')
})()

// start
// this is just a message
// end
// this is a message from call back 1
// this is a message from call back 2

Several runtimes communicating together

多个 runtime 彼此通信

web worker 和 cross-origin iframe 都是独立的 runtime,它们有自己的 stack, heap 和 message queue。 两个不同的 runtimes 要通信,只能通过 postMessage 方法互发 messages。 postMessage 方法会向其它 runtime 添加一个 message,如果后者监听了 message event。

4. Never blocking

JavaScript 中的 event loop model 一个非常有趣的特性是:never blocks(它从不阻塞)。 它处理 I/O 通常是通过 events 和 callbacks 执行的,所以当 application 正在等待 IndexedDB 查询的结果或是 XHR 请求的返回时, 它依然能处理其它事情,比如 user input。

不过还是存在一些历史异常情况,比如 alert 和同步 XHR,所以尽量避免使用它们。

Last updated