资讯专栏INFORMATION COLUMN

Event Loop 那些事儿

tyheist / 2213人阅读

摘要:消息队列和事件循环异步过程中,工作线程在异步操作完成后需要通知主线程,那么这个通知机制是怎样实现的呢答案是利用消息队列和事件循环。

Event Loop 那些事儿

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

异步过程

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

主线程发起一个异步请求,相应的工作线程接收请求并告知主线程已收到;

主线程可以继续执行后面的代码,同时工作线程执行异步任务;

工作线程完成工作后通知主线程,主线程收到通知后调用回调函数。

消息队列和事件循环

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

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 中实质上干了两件事:

执行掉 nextTickQueue 中所有任务

第一步执行完后,执行 _runMicrotasks 函数(执行 micro-task 中的部分,即 promise.then 注册的回调)

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94334.html

相关文章

  • 关于Android消息机制的那些事儿

    摘要:关于中的消息机制就进行到这里,下一篇将讲讲中的内存泄漏问题以及处理方法。 这几天对handler有一些研究,写出来供大家看看。 一、消息机制详解 Android程序中,主要是通过消息机制来推动整个程序运作的。而消息机制中,完成主要功能的主要有以下几个类: 1、Looper 2、Message、MessageQueue 3、handler 这是最基础的几个消息机制类,下面探究一下这几个类...

    lushan 评论0 收藏0
  • 推送近期三波关于Vue.js的资讯

    摘要:原文来自集前端最近很火的框架资源定时更新,欢迎一下。推送自己整理近期三波关于的资讯这里就抛砖引玉了,望有更屌的资源送助攻。 原文来自:集web前端最近很火的vue2框架资源;定时更新,欢迎Star一下。 推送自己整理近期三波关于Vue.js的资讯; 这里就抛砖引玉了,望有更屌的资源送助攻。 showImg(https://segmentfault.com/img/bVVeiZ); 第...

    Anonymous1 评论0 收藏0
  • 【回顾九月份第二周】 前端你该知道的事儿

    摘要:顺便一说,这首歌的原唱是秋田,中岛当年嗓子坏了,才有这歌。中文是直接翻译来的,作曲是秋田。一部电影春夏秋冬又一春春夏秋冬又一春是由金基德执导,金英民吴英秀金基德主演的一部韩国电影。年月日于韩国上映。 原链接: http://bluezhan.me/weekly/#/9-2 1、web前端 Angular vs. React vs. Vue: A 2017 comparison 9 S...

    sixgo 评论0 收藏0

发表评论

0条评论

tyheist

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<