EventLoop和任务队列(微任务和宏任务)

Eventloop

要说Eventloop,就不得不提浏览器进程JavaScript单线程的三两事。

浏览器的工作原理,这里不提了,细抠的话很复杂,这里只说浏览器进程的组成。

浏览器进程

简单了解下浏览器进程,但这不是本文重点,本文重点是: 浏览器渲染进程(内核),也叫 浏览器内核进程

  • Browser进程: 浏览器的主进程,负责协调、主控,只有一个
  • 第三方插件进程: 每种类型的插件对应一个进程,仅当使用该插件时才创建
  • GPU进程:最多一个,用于3D绘制
  • 浏览器渲染进程(内核):默认每个Tab页面一个进程,互不影响,控制页面渲染,脚本执行,事件处理等(有时候会优化,如多个空白tab会合并成一个进程)

以下是本文重点


浏览器内核进程中主要有5大常驻线程,他们各自有分工:

  • GUI渲染线程:负责页面渲染
  • JS引擎线程:主线程,执行JavaScript脚本
  • 事件触发线程: 顾名思义
  • 定时器线程:顾名思义
  • 异步http请求线程: 顾名思义

大家都知道JavaScript是单线程脚本语言,为啥是单线程,有一定的历史原因,也有专业性的考量,感兴趣的话推荐阅读 阮一峰 老师的 JavaScript 运行机制详解:再谈Event Loop

上面列出来的5大常驻线程,其中GUI渲染线程JS引擎线程是“相爱相杀”的,JS引擎线程在执行过程中会阻塞GUI渲染线程进行页面渲染,导致页面卡顿。

如果你说感觉不到,那只能说卡顿时间长短的问题,可以试试这个,先在浏览器打开看看,再把while(true) {}注释掉刷新试试。

test.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script>
    while(true) {}
  </script>
</head>
<body>
  这是测试GUI线程渲染结果显示的内容,很简单,就一行文字而已。。。
</body>
</html>

这就是为什么在聊到页面优化的时候,有一项是: 把没有上下文依赖的外部脚本引入放到</body>上一行的原因。

除了GUI渲染线程JS引擎线程之外,剩下的3个线程:事件触发线程、定时器线程、异步http请求线程,都是用来处理异步逻辑的。

任务队列(消息队列)

除了这些线程之外,还有一个消息队列(任务队列),任务对列不是线程,而是一个存储结构。

下面说的内容可能会有偏差,因为我查询到的有各个版本的说法,难以总结,大致有个概念就行

任务对列分为2种类型:

  • 宏任务队列: Task Queue(MacroTask Queue),可以有多个
  • 微任务队列: MicroTask Queue

异步代码会根据类型分别丢到 宏任务队列 和 微任务队列 中处理,下面是一些划分(左边是高优先级):

  • 宏任务: setTimeout => setInterval => setImmediate(nodeJS) => I/O => UI Rendering
  • 微任务: process.nextTick(nodeJS) => Promise(Promise.then) => mutationObserver

EventLoop(事件循环)

当JavaScrip会创建一个类似于while(true)的循环,不停地执行,每当JS主线程执行完栈中同步代码后,就会去任务队列中读取,被读取到主线程栈中的代码一定是已经执行完成的异步代码,如果异步代码还没有完成,怎么继续等待,直到完成后才会被读取到主进程中执行,因为JavaScript单线程特性决定了它不可能等待不知道什么时候能完成的任务。


其实上面都是概念性的内容,其实本文的核心是想说一说 宏任务和微任务 的执行顺序。


案例

在面试题中经常会看到类似于这样的代码,来判断输出的顺序,这里就借这个案例来抠一抠:

先别慌思考和运行,看看示例代码块下面的规则,然后再去思考,写出顺序。
如果你都觉得这是小case,那么浏览器Tab右上角有个“叉号”, 请点一点;
如果你错了,那么看看规则下方的分析吧。。。

console.log('1');

setTimeout(function() {
  console.log('2');
  Promise.resolve().then(function() {
    console.log('3');
  })
  new Promise(function(resolve) {
    console.log('4');
    resolve();
  }).then(function() {
    console.log('5')
    Promise.resolve().then(function() {
      console.log('14');
    })
  })
}, 100)

Promise.resolve().then(function() {
  console.log('6');
})

new Promise(function(resolve) {
  console.log('7');
  resolve();
}).then(function() {
  console.log('8')
  Promise.resolve().then(function() {
    console.log('15');
  })
})
Promise.resolve().then(function() {
  console.log('100');
})
setTimeout(function() {
  console.log('9');
  Promise.resolve().then(function() {
    console.log('10');
  })
  new Promise(function(resolve) {
    console.log('11');
    resolve();
  }).then(function() {
    console.log('12')
  })
}, 0)

console.log('13')

有以下几个规则:

  • JavaScript在执行时,遇到同步代码就直接执行,遇到异步代码会分类:
    • 宏任务丢到宏任务队列;
    • 微任务丢到微任务队列
  • 宏任务优先于微任务执行(你可能会对这个规则有疑惑,不急,往下看)
    每个宏任务执行完后执行渲染(这是扩展的,放这里纯粹是防止忘记)
  • 继续执行下一个宏任务
  • 重点:每个宏任务执行完后,不会立即进入渲染或进入下一个宏任务,而是把在当前宏任务执行过程中产生的所有微任务执行完后才会进入下一步

执行结果为:

1 // console.log('1')
7
13 // console.log('13')
6
8
100
15
// 以下为第二个setTimeout
9
11
10
12
// 以下为第一个setTimeout
2
4
3
5
14

分析

  1. console.log('1')console.log('13') 是同步代码,输出:1 => 13 很好理解,但是7为什么会在13之前输出?

原因: 虽然 console.log('7') 是 new Promise的回调函数内的,但这里并不意味着它就是异步的,可以想象一下这样一个过程: new Promise(callback),而Promise的构造函数是这样的:

function Promise (cb) {
  let resolve = function (data) { xxx }
  typeof cb === 'function' && cb(resolve)
}

那么new Promise内部就是同步执行的,起码按照目前的表现是可以这么猜测和解释的。
所以 输出:1 => 7 => 13 就可以理解了。

  1. 接下来是 6 => 8 => 100 的输出的分析
    疑惑: 规则里不是说了先执行宏任务吗?为什么不是先执行setTimeout里的代码?而是执行微任务里的Promise.then?
    其实这问题我也疑惑了挺久的,弄得一直搞不清到底谁先谁后,后来想到了原因。
    原因: 因为代码执行的上下文就是被当做一个最外层的宏任务,而 Promise.resolve().then(function() {console.log('6');})Promise.xxxx.then(function() {console.log('8');})以及Promise.resolve().then(function() {console.log('100');}) 外部没有被属于宏任务的代码包裹,就说明属于顶层宏任务的微任务,所以会先执行。

  2. 再看看 15 和 8 都在一个Promise.then的代码块里,为什么会在 100 之后输出?

原因:

  • 实际上 console.log('8')Promise.resolve()确实是同步执行的
  • 但遇到了 .then,发现这是一个微任务,所以又把 console.log('15') 丢到了微任务队列中
  • 虽然这是一个异步,但根据规则同一个宏任务中的微任务会被等待到执行完,所以 console.log('15')是一定会被在下一个宏任务之前被执行的。

但我们不要忘记了,队列的数据结构是 先进先出 的,console.log('8') 和 console.log('100')由于被先丢到微任务队列中,所以会先执行,console.log('15') 就会后执行。

new Promise(function(resolve) {
  console.log('7');
  resolve();
}).then(function() {
  console.log('8')
  Promise.resolve().then(function() {
    console.log('15');
  })
})
  1. 综合以上分析那么下面两个定时器分别按顺序输出:
    9 => 11 =>10 =>12
    2 => 4 =>3 => 5 =>14
    就顺理成章了

  2. 还有个问题,不是 2 => 4 =>3 => 5 =>14 应该被先输出吗,为什么反了?

原因: 示例代码里其实有一个坑。。。两个定时器的延迟时间不同,第一个定时器是 100ms,而第二个定时器是 0ms,是立即执行的。

新的疑惑: 虽然两个定时器的延迟时间不同,但是第一个定时器被先添加到宏任务队列中,按照队列先进先出规则,岂不是自己打脸了?

原因: 这些异步代码是通过EventLoop不停地轮询队列,然后添加到主线程中执行的。
但是有一个前提,这个前提在上面介绍EventLoop已经加粗显示了:

被读取到主线程栈中的代码一定是已经执行完成的异步代码

注意已经执行完成这6个字,因为第二个定时器先执行完,所以EventLoop先发现了它,所以它被先取到主线程中执行。

这样疑问就解决了。。。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容