JS中EventLoop、宏任务与微任务的个人理解
为什么要EventLoop?
JS 作为浏览器脚本语言,为了避免复杂的同步问题(例如用户操作事件以及操作DOM),这就决定了被设计成单线程语言,而且也将会一直保持是单线程的。而在单线程中若是遇到了耗时的操作(IO,定时器,网络请求)将会一直等待,CPU利用率将会大打折扣,时间大量浪费。所以需要设计一种方案让一些耗时的操作放在一边等待,让后面的函数先执行,于是有了EventLoop的设计。
将任务分为两种:
- 同步任务
- 异步任务
- 定时器都是异步操作
- 事件绑定都是异步操作
- AJAX中一般采取的异步操作(虽然也可以同步)
- 回调函数(不严谨的异步)
阮一峰老师《JavaScript 运行机制详解:再谈Event Loop》
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
任务都会按顺序进入调用栈(call stack),即图1-1的stack,然后按栈的顺序依次执行。若全是同步任务,就会正常地顺序执行。当遇到异步任务时(其实就是执行到了一个耗时的任务,它发起后,需要它的回调函数等待拿到结果之后才继续进行)将会放到WebAPIs中(图1-1),等待这个耗时操作返回结果,也有网友把这个 WebAPIs 称之为 Event Table。如果异步任务在WebAPIs中等待有了结果(比如setTimeout的时间截止了,xhr得到响应结果了,用户click事件发生了),就会将这个结果作为一个事件置于任务队列中。 【或者称之为:注册回调函数】
那么任务队列又是什么?个人认为就是图中的callback queue,或称之为 Event Queue 。就是存放了各种耗时操作最后响应结果的各个事件(说白了,就是已经拿到结果的,就会从WebAPIs放到任务队列里来)
图 1-1 转自Philip Roberts的演讲《Help, I'm stuck in an event-loop》
搞懂上面两段话后,就可以谈EventLoop的作用了:
- 在调用栈和任务队列之间进行“轮询”
- 但轮询的规则是:只有每当调用栈为空,才能去“询问”任务队列中是否有事件需要处理
- 若任务队列存在事件,则会将该事件相应的回调函数(异步操作)结束等待,置于调用栈中开始执行
- 如果调用栈一直不为空,那就一直不会“询问”任务队列
以上过程是不断循环的,js引擎中,存在一个叫monitoring process的进程,这个进程会不断的检查主线程的执行情况,一旦为空,就会去任务队列检查有哪些待执行的函数。这里的整个过程可以参考 一个工具 loupe 对整个调用过程进行查看。
图 1-2 loupe, 也是从其他地方发现的这个东西,很直观
针对call stack调用栈多说一句:通俗地讲,将调用栈比喻为程序员,各个任务比喻为需求,任务队列比喻为总监。当总监提需求时,程序员就要交接需求过来,然后完成它。如果没有需求,就一直等待总监给需求。给了就做,不给就等。
搞懂同步任务与异步任务的具体执行流程后,再谈谈为什么要设计宏任务和微任务。
为什么有宏任务、微任务?
页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么若只有一种类型的任务就不合适了,所以引入了微任务队列。
至此,任务队列已被分为:
- 宏任务队列,即上文说的任务队列,callback queue,用于存放宏任务
- 微任务队列,再开辟一个队列,用于存放微任务
图 2-1 微任务Microtask Queue的加入
首先列举一下哪些是宏任务、哪些是微任务
宏任务:
- script(主代码)
- setTimeout()
- setInterval()
- postMessage
- I/O
- UI交互事件
- setImmediate(Node.js)
- requestAnimationFrame(浏览器)
微任务:
- new Promise().then(回调)
- MutationObserver(html5 新特性)
- process.nextTick(Node.js)在当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前
紧接着第一节里说的EventLoop,当时没有考虑什么宏任务微任务,现在再加入微任务的概念再来考虑整个流程:
- 依旧是在调用栈和任务队列中轮询。(此时的任务队列指的是宏任务队列)
- 调用栈为空后,优先检查微任务队列,如果微任务队列中存在事件,则加入到调用栈中进行执行(为什么先询问的是微任务队列而不是宏任务队列,在后面解释)
- 注:如果在执行微任务队列中的函数时,产生了新的微任务(比如then函数嵌套),则会继续在本次执行中执行(就是说如果期间一直有微任务产生,那就会永远卡在微任务队列执行)
- 如果微任务队列为空,那就取宏任务队列中的事件加入到调用栈中进行执行
- 若在执行宏任务的时候,产生了新的微任务,就会将该微任务加入到微任务队列,该微任务队列将会在下一次宏任务执行之前执行,如图2-2。
- 循环。
依旧是:两个任务队列(宏、微)只有有任务,那么主进程的调用栈就会调过去执行,没有任务的话,主进程就一直等着,直到又有任务。
图 2-2 宏任务与微任务的执行顺序
注意的是,图2-2看起来是宏任务先执行,微任务后执行,这仅仅是宏任务与微任务的先后次序,但不代表宏任务优先级比微任务高。事实是微任务的优先级是高于宏任务的。因为微任务其实是产生于宏任务的,不可能凭空产生微任务,也就不可能一开始就出现几个微任务。在本次宏任务产生微任务后,将会在下次宏任务执行之前,优先执行这些微任务。自然也就映证了设计微任务的初衷:为了让某些任务尽快执行。
总结完整的EventLoop流程:
- 执行一个宏任务(调用栈中没有就从宏、微任务队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前微任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
微任务在本次宏任务之后执行,在本次渲染之前执行,在下次宏任务之前执行。(宏任务 -> 微任务 -> 渲染 -> 宏任务)
包含宏任务、微任务的异步代码分析:
// 知乎作者:Miku
// 链接:https://zhuanlan.zhihu.com/p/257069622
// 注意:代码中的process.netxTick 函数存在于Node.js中
console.log('1'); setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5');
});
});
process.nextTick(function() {
console.log('6');
});
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8');
}); setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
});
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12');
});
});
第一轮循环:
1)、首先打印 1
2)、接下来是setTimeout是异步任务且是宏任务,加入宏任务暂且记为 setTimeout1
3)、接下来是 process 微任务 加入微任务队列 记为 process1
4)、接下来是 new Promise 里面直接 resolve(7) 所以打印 7 后面的then是微任务 记为 then1
5)、setTimeout 宏任务 记为 setTimeout2 第一轮循环打印出的是 1 7
当前宏任务队列:setTimeout1, setTimeout2
当前微任务队列:process1, then1,第二轮循环:
1)、执行所有微任务
2)、执行process1,打印出 6
3)、执行then1 打印出8
4)、微任务都执行结束了,开始执行第一个宏任务
5)、执行 setTimeout1 也就是 第 3 - 14 行
6)、首先打印出 2
7)、遇到 process 微任务 记为 process2
8)、new Promise中resolve 打印出 4
9)、then 微任务 记为 then2 第二轮循环结束,当前打印出来的是 1 7 6 8 2 4
当前宏任务队列:setTimeout2
当前微任务队列:process2, then2第三轮循环:
1)、执行所有的微任务
2)、执行 process2 打印出 3
3)、执行 then2 打印出 5
4)、执行第一个宏任务,也就是执行 setTimeout2 对应代码中的 25 - 36 行
5)、首先打印出 9
6)、process 微任务 记为 process3
7)、new Promise执行resolve 打印出 11
8)、then 微任务 记为 then3 第三轮循环结束,当前打印顺序为:1 7 6 8 2 4 3 5 9 11
当前宏任务队列为空
当前微任务队列:process3,then3第四轮循环:
1)、执行所有的微任务
2)、执行process3 打印出 10
3)、执行then3 打印出 12 代码执行结束:
最终打印顺序为:1 7 6 8 2 4 3 5 9 11 10 12
参考
- 宏任务与微任务 https://zhuanlan.zhihu.com/p/92460508
- js中的宏任务与微任务 https://zhuanlan.zhihu.com/p/78113300
- 对微任务和宏任务的执行顺序的个人理解 https://zhuanlan.zhihu.com/p/257069622
- loupe http://latentflip.com/loupe/
- JavaScript 运行机制详解:再谈Event Loop http://www.ruanyifeng.com/blog/2014/10/event-loop.html
- 译文:JS事件循环机制(event loop)之宏任务、微任务 https://segmentfault.com/a/1190000014940904
- JavaScript中的Event Loop(事件循环)机制 https://segmentfault.com/a/1190000022805523
JS中EventLoop、宏任务与微任务的个人理解的更多相关文章
- js中的宏任务与微任务
如果你已经知道了js中存在宏任务和微任务,那么你一定已经了解过promise了.因为在js中promise是微任务的一个入口. 先来看一道题: setTimeout(function(){ conso ...
- javascript中的宏任务和微任务(一)
一.宏任务和微任务有哪些 宏任务:setTimeout,setInterval,ajax,dom,宏任务是由浏览器提供的 微任务:promise,async/await,微任务是由es6提供的 二.微 ...
- Js中关于构造函数,原型,原型链深入理解
在 ES6之前,在Javascript不存在类(Class)的概念,javascript中不是基于类的,而是通过构造函数(constructor)和原型链(prototype chains)实现的.但 ...
- javascript中的宏任务和微任务(二)
js事件轮询执行顺序总结: 1)所有的同步任务都在主线程上执行,行成一个执行栈. 2)除了主线程之外,还存在一个任务列队,只要异步任务有了运行结果,就在任务列队中植入一个时间标记. 3)主线程完成所有 ...
- JS异步之宏队列与微队列
1. 原理图 2. 说明 JS 中用来存储待执行回调函数的队列包含 2 个不同特定的列队 宏列队:用来保存待执行的宏任务(回调),比如:定时器回调.DOM 事件回调.ajax 回调 微列队:用来保存待 ...
- 关于JS中变量提升的规则和原理的一点理解
关于变量提升,以前在一些教程和书籍上都听到过,平时开发中也知道有这个规律,但是今天突然在一个公开课中听到时,第一反应时一脸懵逼,然后一百度,瞬间觉得好熟悉啊,差点被这个概念给唬住了,不信我给你 ...
- 关于JS中变量提升的规则和原理的一点理解(二)
上篇文章中讲到变量提升和函数提升的先后顺序时蒙了,后来去查了一下资料,特别整理一下. 在<你不知道的JavaScript(上卷)>一书的第40页中写到:函数会首先被提升,然后才是变量. 书 ...
- JS中some(),every(),fiflter(),map()各种循环的区别理解
1.some():返回一个Boolean,判断是否有元素符合func条件const arr = [1,2,3,4]; arr.some((item)=>{return item>1}) 打 ...
- JavaScript同步模式,异步模式及宏任务,微任务队列
首先JavaScript是单线程的语言,也就是说JS执行环境中,负责执行代码的线程只有一个.一次只能执行一个任务,如果有多个任务的话, 就要排队,然后依次执行,优点就是更安全,更简单.缺点就是遇到耗时 ...
随机推荐
- 2015 - 2020 最新 Linux 命令大全
# 2015 - 2020 最新 Linux 命令大全 ## VIM 命令模式(Command mode):vi 插入模式(Insert mode):i底线命令模式(Last line mode):e ...
- 中英文混排网站排版指南 All In One
中英文混排网站排版指南 All In One 排版 数字与单位 正确 5G 的下载速度可以达到 1Gbps,4G 为100Mbps 1Gbps === 1000Mbps 错误 5G的下载速度可以达到1 ...
- node.js & read argv
node.js & read argv https://nodejs.org/docs/latest/api/process.html https://flaviocopes.com/node ...
- Linux & SIGUSER1
Linux & SIGUSER1 https://stackoverflow.com/questions/10824886/how-to-signal-an-application-witho ...
- nasm astrrchr函数 x86
xxx.asm %define p1 ebp+8 %define p2 ebp+12 %define p3 ebp+16 section .text global dllmain export ast ...
- 【C#】反射的用法及效率对比
反射实例化类 public class Person { public string Name { get; set; } public Person(string name) { this.Name ...
- 加州金融专访NGK,就NGK DeFi+展开讨论
近日,加利福尼亚金融日报联合数家知名媒体就DeFi+行业专访了NGK团队代表特德惠斯基. 首先,加利福尼亚金融日报专栏记者迈尔斯表示,目前区块链领域,去中心化金融(DeFi+)的概念是目前市场上面最火 ...
- Java 动态调试技术原理及实践
本文转载自Java 动态调试技术原理及实践 导语 断点调试是我们最常使用的调试手段,它可以获取到方法执行过程中的变量信息,并可以观察到方法的执行路径.但断点调试会在断点位置停顿,使得整个应用停止响应. ...
- Vue学习笔记-django-cors-headers安装解决跨域问题
一 使用环境: windows 7 64位操作系统 二 jango-cors-headers安装解决跨域问题(后端解决方案) 跨域,指的是浏览器不能执行其他网站的脚本.它是由浏览器的同源策略造成的 ...
- SpringBoot(九):SpringBoot集成Mybatis
(1)新建一个SpringBoot工程,在pom.xml中配置相关jar依赖 贴代码: <!--加载mybatis整合springboot--> <dependency> &l ...