Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let's talk about Event Loop #17

Open
FE-Sadhu opened this issue May 9, 2019 · 0 comments
Open

Let's talk about Event Loop #17

FE-Sadhu opened this issue May 9, 2019 · 0 comments
Labels
运行机制 Further information is requested

Comments

@FE-Sadhu
Copy link
Owner

FE-Sadhu commented May 9, 2019

照例思维导图。

单线程的js

我们通常说 JavaScript 是单线程的,实际上是指在 JS 引擎中负责解释和执行 JS 代码的线程只有一个,一般成为主线程,在这种前提下,为了让用户的操作不存在阻塞感,前端 APP 的运行需要依赖于大量的异步过程,所以当然浏览器中还存在一些其他的线程,比如处理 http 请求的线程、处理 DOM 事件的线程、定时器线程、处理文件读写的 I/O 线程等等。

异步过程

一个异步过程通常是这样的:

  1. 主线程发起一个异步请求,相应的工作线程接收请求并告知主线程已收到;
  2. 主线程可以继续执行后面的代码,同时工作线程执行异步任务;
  3. 工作线程完成工作后通知主线程,主线程收到通知后调用回调函数。

消息队列和事件循环

异步过程中,工作线程在异步操作完成后需要通知主线程,那么这个通知机制是怎样实现的呢?

答案是利用消息队列和事件循环。消息队列是一个先进先出的队列,里面存放着各种消息,我们可以简单的理解为消息就是注册异步任务时添加的回调函数;事件循环是指主线程重复从消息队列中获取消息、执行回调的过程,之所以称为事件循环,就是因为它经常被用类似如下方式来实现:

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

消息队列是一个存储着待执行任务的队列,其中的任务严格按照时间先后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。消息队列每次仅执行一个任务,在该任务执行完毕之后,再执行下一个任务。执行栈则是一个类似于函数调用栈的运行容器,当执行栈为空时 JS 引擎便检查消息队列,如果不为空消息队列便将第一个任务压入执行栈中运行。

下面我们来看下述代码来验证我们的想法:

setTimeout(function() {
    console.log(4)
}, 0);

new Promise(function(resolve) {
    console.log(1)

    for (var i = 0; i < 10000; i++) {
        i == 9999 && resolve()
    }

    console.log(2)
}).then(function() {
    console.log(5)
});

console.log(3);

比较诡异的事情出现了,为什么结果是“1, 2, 3, 5, 4”,而不是“1, 2, 3, 4, 5”呢?!

按道理来说,执行 setTimeout 时因为延迟为0,所以 console.log(4) 直接插入至消息队列;创建 Promise 实例时同步执行其函数体内的代码,先打印 1,再循环10000次后执行 resolve 将 then 中的回调函数 console.log(5) 插入至消息队列,然后打印 2;最后执行 console.log(3) 打印 3;在主线程执行完成后读取消息队列,依次打印 4 和 5 。

上面的想法当然是比较天真的,实际上浏览器中仅有一个事件循环,然后消息队列是可以有多个的。

macro-queue: script (整体代码), setTimeout, setInterval, setImmediate, I/O, UI Rendering

micro-queue: process.nextTick, Promise, Object.observe, MutationObserver

并且 micro-queue 的任务优先级高于 macro-queue 的任务优先级,这两个任务队列执行顺序如下:取1个 macro-task 执行之,然后把所有 micro-task 顺序执行完,再取 macro-task 中的下一个任务,以此类推依次进行。

优先级:process.nextTick > promise.then > setTimeout > setImmediate

Tip:process.nextTick 永远大于 promise.then 原因其实很简单:在 NodeJS 中,_tickCallback 在每一次执行完 TaskQueue 中的一个任务后被调用,而在这个_tickCallback 中实质上干了两件事:

  1. 执行掉 nextTickQueue 中所有任务
  2. 第一步执行完后,执行 _runMicrotasks 函数(执行 micro-task 中的部分,即 promise.then 注册的回调)

总结

浏览器环境一般只能有一个事件循环(实际上有两类:browsing contexts 和 web workers),而一个事件循环可以多个任务队列,每个任务都有一个任务源。相同任务源的任务,只能放到一个任务队列中;不同任务源的任务,可以放到不同任务队列中。举个栗子,客户端可能实现一个包含鼠标键盘事件的任务队列,还有其他的任务队列,而给鼠标键盘事件的任务队列更高优先级,例如75%的可能性执行它,这样就能保证流畅的交互性,而且别的任务也能执行到。

至此,再返回去看之前的代码就不难分析出:代码执行开始把 setTimeout 的回调插入至 macro-queue 中,而打印完1后把 promise.then 的回调函数插入至 micro-queue 中,整体代码执行完后,按照消息队列的优先级,先执行 micro-task 即打印5,最后执行 macro-task 即打印4。


参考: Event Loop 那些事儿

@FE-Sadhu FE-Sadhu added the 运行机制 Further information is requested label May 9, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
运行机制 Further information is requested
Projects
None yet
Development

No branches or pull requests

1 participant