JavaScript的运行机制及宏任务和微任务

原文链接

一篇文章搞懂浏览器Js事件循环机制(推荐阅读!)

前言

在初次入门学习和使用 JavaScript 的过程中,相信遇到过许多程序执行顺序及结果与预期不一致的问题,在查阅资料的过程中了解到原来是程序的执行有同步与异步之分;与此同时也会看到许多有关概念,例如回调函数、执行栈、任务队列、事件循环机制(Event Loop)、宏任务、微任务、Promise(ES6)等等。此时对于一个刚入门不久的小白来说,要理解消化这些概念真的不容易。对于入门不久的我来说也一样,所以写一篇博客记录一下,有关 JavaScript 的运行机制,以及上述的这些概念为什么会出现,又解决了什么问题。

一、JavaScript 是单线程

我们知道多线程是可以并行执行程序的,能提高程序运行效率。但是 JS 是一门单线程语言,同一时间内做一件事。

最初作为服务于浏览器的脚本语言,很多时候都是在与用户交互,这个过程涉及了许多 DOM 的操作,倘若使用多线程,那么就容易出现几个线程同时操作一个 DOM 的问题,那么浏览器此时要以哪一个线程为主呢?这样一来无疑增加了复杂性,所以 JS 成为了单线程。虽然说多线程处理起来也很高效,但对于当时直接服务于浏览器用户的 JS 来说,尽可能避免过度复杂,能更简单的处理相对好点吧。

二、异步任务及其回调函数

虽然单线程降低了复杂性,但是也有了新的问题。单线程是顺序执行程序,每一个任务要等待上一个任务执行完毕才执行,如果遇到执行时间太长或者出现了别的问题,那么就会一直卡在那,导致整个程序无法顺利执行完毕。为了解决问题,语言设计者希望在程序执行时,将一些耗时、有延迟的任务先挂起,让能快速执行完毕的任务先执行;按照这样的方式执行完整个程序后,在返回去执行那些被挂起的任务。因此有了同步任务与异步任务之分;在执行过程中,当前执行程序的线程称为主线程,同步任务直接在主线程立即执行,而那些异步任务,先给它挂在一边放着,等到主线程执行完了所有同步任务,再回来读取挂在一旁的异步任务,并且执行他们。

(1) 任务队列

任务队列是一系列事件组成的一个队列,也就是上面说到的异步任务挂起的地方。程序执行时会将定义的异步任务送入任务队列,或者用户点击鼠标触发的异步任务送入队列。等待主线程来执行它们。例如常见的各种事件(鼠标点击、键盘敲击、滚动等等)、又或者是 Ajax 那样等待响应的异步任务。

实际上,任务队列不止一种,因为处理的异步任务种类可能不同

(2) 回调函数 (callback)

回调函数往往就是异步任务所定义的代码。主线程执行完同步任务,就会回来开始读取任务队列中的异步任务并执行这些代码,同时也称为回调函数。

(3) 宏任务和微任务

异步任务又可以看为两种,通常由宿主环境(浏览器、node)提供的为宏任务,由语言标准提供的为微任务。 JavaScript 可能会在不同的宿主环境下运行,所以宏任务来自于宿主环境,而微任务作为语言标准,在任何环境下都可以使用。

常见宏任务

  • setTimeout
  • setInterval
  • setImmediate (仅 node 提供)
  • requestAnimationFrame (仅浏览器提供)
  • 各种交互 (鼠标点击、滚动等等)
  • I/O

常见微任务

  • Promise.then catch finally
  • MutationObserver (仅浏览器提供)
  • process.nextTick (仅 node 提供)

三、事件循环机制 (Event Loop)

主线程执行程序时会将定义的异步任务放入任务队列中,宏任务会放在宏任务队列,微任务放在微任务队列,当触发 UI 事件时,也会把相应任务放入队列。为了确保事件处理正常进行,主线程不阻塞。所以有了解决方案 Event Loop,事件循环线程是独立于主线程的,并且一直存在直到整个脚本环境被关闭。无论是主线程执行时添加的异步任务,还是 UI 交互触发后添加的异步任务,事件循环机制都会按一定规则循环读取并且执行。

那么该循环机制如何运行呢?

  • (1) 打开某个宿主环境时,主线程执行同步任务的所有代码,形成一个执行栈;把遇到的异步任务放入相应的队列里;同时一个独立于主线程的事件循环线程也被创建并一直存在。

  • (2) 当主线程执行完同步任务,会将该执行过程中添加的微任务全部执行完,之后由事件循环机制协调。

  • (3) 事件循环读取当前宏任务队列的一个宏任务,并放入执行栈中执行

  • (4) 在执行过程中遇到宏任务和微任务,按照相同的方式放入相应队列

  • (5) 该宏任务执行完毕后立即执行此次宏任务中所添加的所有微任务

  • (6) 回到第 (3) 步开始重复后面步骤。

  • 说那么多,看个例子

console.log('1-1');

Promise.resolve().then(() => console.log('微任务 1-1'));

new Promise((resolve) => {
  console.log('1-2');
  resolve();
}).then(() => {
  console.log('微任务 1-2')
});

setTimeout(() => console.log('宏任务 1-1'), 100);

console.log('1-3');
//1-1
//1-2
//1-3
//微任务 1-1
//微任务 1-2
//宏任务 1-1
  • 主线程开始执行,形成一个执行栈

  • 碰到第一个 console.log('1-1'),并打印 -> 1-1

  • 碰到第一个 Promise,已为成功状态,将其 then() 加到微任务中

  • 碰到第二个 Promise,先执行其中的 console.log('1-2'),打印 -> 1-2,并将其 then() 放入微任务队列

  • 碰到第一个宏任务,放入宏任务队列

  • 碰到 console.log('1-3'),打印 -> 1-3

  • 主线程执行完所有同步任务,开始执行本次添加的所有微任务

  • 读取微任务队列

  • 遇到先进去的第一个 then() ,打印 -> 微任务 1-1

  • 遇到后进去的 then() 打印 -> 微任务 1-2

  • 本次主线程任务完成,下面由事件循环机制来协调。开始读取宏任务队列

  • 遇到第一个放入的宏任务 setTimeout(),将其丢到执行栈延时 100ms 执行,打印 -> 宏任务 1-1

  • 第一次宏任务执行完毕,读取微任务队列,发现没有微任务。进入第二次循环

  • 读取宏任务队列,发现没有宏任务。JS 执行栈开始摸鱼...

到这里其实会发现,微任务都会紧跟在当前执行栈执行同步任务后执行,而存好的宏任务被放在下次执行,好似重新开始一样。

按个人总结来就是(不一定对),主线程的执行栈是专门用来执行代码的;当事件循环线程读取到一个宏任务时,将其放入执行栈执行,主线程会执行其中定义的同步任务,将遇到的宏任务和微任务存起来,在本次同步任务执行完之后立即执行微任务。而此次存好的宏任务又会按照相同的方式在下一次循环中进行。因为事件循环机制一次循环只读取执行一个宏任务。

由此看来其实整个程序也可以看成是一个宏任务,而首次添加的宏任务和微任务是按照上面的方式一层层刨开,按照一次执行一个宏任务和里面所有微任务的规则进行

  • 再看个例子说明宏任务是一次循环读取一次,并且会执行宏任务下所有微任务
console.log('开始执行主线程');
console.log('0-1');

Promise.resolve().then(() => console.log('微任务 0-1\n-----'));

setTimeout(() => {//宏任务 1
  console.log('第一个宏任务');
  console.log('宏任务 1-1');
  Promise.resolve().then(() => console.log('微任务 1-1'));
  Promise.resolve().then(() => console.log('微任务 1-2\n-----'));

  setTimeout(() => {//宏任务3
    console.log('第三个宏任务');
    console.log('宏任务 3-1')
    Promise.resolve().then(() => console.log('微任务 3-1\n-----'))
  },10);

},100);

setTimeout(() => {//宏任务2
  console.log('第二个宏任务');
  console.log('宏任务2-1');
  Promise.resolve().then(() => console.log('微任务 2-1\n-----'));
},100);

console.log('0-2');

***************************

执行结果

开始执行主线程
0-1
0-2
微任务 0-1
-----
第一个宏任务
宏任务 1-1
微任务 1-1
微任务 1-2
-----
第二个宏任务
宏任务 2-1
微任务 2-1
-----
第三个宏任务
宏任务 3-1
微任务 3-1
-----
  • 开始执行主线程后,将 微任务 0-1 、 宏任务1 、 宏任务2 存入队列,并先打印其同步任务代码,又打印微任务代码
  • 开始第一次事件循环,读取宏任务1(第一个定时),将 微任务 1-1 、微任务 1-2、和宏任务3 存入队列。打印方式如上一条。
  • 开始第二次事件循环,读取宏任务2(第二个定时),将 微任务 2-1 存入队列,打印方式如上。
  • 开始第三次事件循环,读取宏任务队列中最后一个进去的宏任务3(宏任务1中定义的定时器),将 微任务 3-1 存入队列,打印方式如上。

大概流程图

流程图

提示,虽然说是一次循环只读取一个宏任务,但是他没说要等当前宏任务执行完才进行下一次循环哦!!,事件循环读取到队列中的任务并且让它开始执行后,就可以开始下次循环,不需要等待

  • 下面改动的例子,留给自己做练习吧
console.log(1);

Promise.resolve().then(() => console.log(2));

setTimeout(() => {
  console.log(3);
  Promise.resolve().then(() => console.log(4));

  setTimeout(() => {
    console.log(5);
  },10);

},200);

setTimeout(() => {
  console.log(6);
  Promise.resolve().then(() => console.log(7));
  setTimeout(() => {
    console.log(8)
  }, 300);
},100);

console.log(9);

自己在纸上写了一下,将代码在浏览器上运行之后对比,发现完全正确。你也可以自己写一下哦。

2020/9/22 更新

有一种情况,那就是 then() 之后接着 then() ,那么此时的顺序呢?

console.log('1-1');

Promise.resolve().then(() => console.log('微任务 1-1')).then(() => console.log('微任务 1-3'));

new Promise((resolve) => {
  console.log('1-2');
  resolve();
}).then(() => {
  console.log('微任务 1-2')
}).then(() => console.log('微任务 1-4'));

setTimeout(() => console.log('宏任务 1-1'), 100);

//1-1
//1-2
//1-3
//微任务 1-1
//微任务 1-2
//微任务 1-3
//微任务 1-4
//宏任务 1-1

可以看到他的运行顺序,说明在 then() 执行之后,如果后面还接着 then() 那么按照同样的方式添加到微任务队列,等到之前添加的第一层 then() 都执行完后,在到微任务队列里面读取后面添加的 then(),运行方式如上。并且只有当微任务队列为空时,事件循环机制才会进行到下一轮并读取新的宏任务。

参考链接

阮一峰的网络日志
JavaScript 运行机制详解:再谈Event Loop

知乎作者:tigerHee
js中的宏任务与微任务

博客园作者:daisy,gogogo
JavaScipt 中的事件循环 event loop,以及微任务和宏任务的概念

国外作者写的一篇文章
Tasks, microtasks, queues and schedules

未完,后续文章记录异步编程的学习笔记...

第一次学习理解JS运行机制、同步异步的问题。如果文章中有理解错误的地方欢迎提出,我会及时改正,谢谢!

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