浅析JS异步、事件循环&任务队列
站在前人肩膀上可以看得更远,也不一定,至少比你自己看得远。
一、带着问题去想
问题:JS为什么是单线程? 为什么需要异步?单线程又是如何实现异步的?
第一个问题:javaScript最初被设计用在浏览器中,思考一下,如果浏览器中的JS是多线程,会什么样?
虚拟场景:
现有2个进程,process1 & process2 ,由于是多进程的JS,它们对同一个dom同时进行操作,
process1 要删除该dom,process2编辑了该dom元素,同时执行2个命令,试想,浏览器究竟要如何执行?
结论:JS为什么被设计成单线程有木有理解了呢?
第二个问题: JS为什么需要异步?
虚拟场景:
试想,如果JS不存在异步,只能自上而下执行,如果上一行解析时间很长,下面要执行的代码就会被阻塞。
对于用户而言,阻塞意味着卡死,浏览器无响应,等等。。 导致用户体验很差,估计研发要和(产品、UI设计、视觉设计等一票人)干一仗了。
结论:证明JS中是一定要存在异步执行操作。
第三个问题:试想单线程要如何实现异步操作?
结论:通过事件循环(Event Loop)理解event loop机制,同样就理解了JS的执行机制
二、概念
再讲之前,我们先熟悉了解一下,3个概念?
- 事件循环?
- 任务队列(事件队列)?
- 主线程?
事件循环:将任务栈中的事件放到执行栈中
事件循环过程被称为"tick"; (每次Tick会查看任务队列中是否有需要执行的任务)
每次Tick的过程就是查看是否有待处理的事件,如果有则取出相关事件及回调函数放 入执行栈中由主线程执行。
任务队列(事件队列):存储异步操作添加的相关事件回调
异步操作会将相关回调添加到任务队列中。不同的异步操作添加到任务队列的时机也不同。
异步操作是由浏览器内核的 webcore 来执行的,webcore 包含3种 webAPI,分别是 DOM Binding、network、timer模块
{
1.setTimeout 会由浏览器内核的 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。
2.onclick 由浏览器内核的 DOM Binding 模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。
3.ajax(XMLHttpRequest) 则会由浏览器内核的 network 模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中。
}
主线程:
1. 主线程中的执行栈放的同步代码,先执行
2. 主线程唯一(只有一个主线程)主线程将执行栈里的代码执行完毕。事件循环才开始执行
3. 主线程中执行栈为空,才会进行事件循环来观察要执行的事件回调。当事件循环检测到任务队列中有事件就取出相关回调放入执行栈中由主线程执行
此图来源网络,再次引用作为学习用途,感谢原图作者绘图!
具体只一个任务队列来说明:
ok
三、Event Loop 运行机制
先看一个例子:输出的循序是什么?
console.log(1);
setTimeout(() => {
console.log(2)
}, 1000)
console.log(3);
分析:setTimeout中的函数并没有立即执行,二是延迟了一段时间,满足一定条件后,才执行,通常我们所谓的异步操作。
注:setTimeout函数,通常理解为1秒后,执行里面的函数,这种理解并不是很准确,
而是,1秒后,setTimeout中函数会推入到event queue,而事件队列中的任务,只有在 主线程空闲时才会执行。
只有同时满足:1秒后且主线程空闲时,才会真的在1秒后执行该函数
实际场景,会遇到,超过1秒后,才执行该函数(因为主线程执行的内容很多,导致真正的延迟时间远远大于1秒)
特殊情况:setTimeout(fn,0)
只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行;
即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒
故此,知道JS中将任务分为同步任务和异步任务。
看图:
JS执行的机制是:
1.先判断JS是同步任务还是异步任务,同步任务进入主进程,异步任务进入event table
2.异步任务在event table中注册函数,当满足条件后,被推入event queue
3.同步任务进入主线程后一直执行,当主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主线程中(执行栈)
上述步骤循环执行,即event loop
注:event table就是注册站,调用栈让event table注册一个函数,并移到event queue。
event queue:理解为缓冲区域即可,事件队列中函数等着被调用并移到调用栈(执行栈)。
JS引擎会有一个monitoring process 持续不断地检查调用栈是否为空
回过头,分析上述例子:
console.log(1); // 同步任务,放入主线程
setTimeout() 是异步任务,被放入event table, 0秒之后被推入event queue里
console.log(3 是同步任务,放到主线程里
当 1、 3在控制条被打印后,主线程去event queue(事件队列)里查看是否有可执行的函数,执行setTimeout里的函数
继续往下看:
setTimeout(function(){
console.log('定时器开始啦')
});
new Promise(function(resolve){
console.log('马上执行for循环啦');
for(var i = 0; i < 10; i++){
i == 9 && resolve();
}
}).then(function(){
console.log('执行then函数啦')
});
console.log('代码执行结束');
按照上述JS执行机制分析,最终输出结果
1.setTimeout 是异步任务,被放到event table
2.new Promise 是同步任务,被放到主进程里,直接执行打印 console.log('马上执行for循环啦')
3..then里的函数是 异步任务,被放到event table
4.console.log('代码执行结束')是同步代码,被放到主进程里,直接执行
结论:马上执行for循环啦 — 代码执行结束 — 定时器开始啦 — 执行then函数啦(对么)
按照异步和同步的划分方式,并不准确
划分方式:任务队列分两种类型(宏任务、微任务)
注:ES6 的 promise[ECMAScript标准]产生的任务队列为microtask queue
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise.then(),process.nextTick, MutationObserver
按照此分类,JS执行的机制是:
- 执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里
- 当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完
此时,再次分析上述例子:
- 首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的’队列’里
- 遇到 new Promise直接执行(立即执行函数), 打印"马上执行for循环啦"
- 遇到then方法,是微任务,将其放到微任务的队列里
- 打印 "代码执行结束"
- 本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印"执行then函数啦"
- 到此,本轮的event loop 全部完成
- 下一轮的循环里,先执行一个宏任务,发现宏任务的队列里有一个 setTimeout里的函数,执行打印"定时器开始啦"
结论:马上执行for循环啦 — 代码执行结束 — 执行then函数啦 — 定时器开始啦
最后,
总结:
永远记得js就是单线程执行
(扩展:h5中 webworker可实现多线程)主要解决计算,并不能操作dom
具体可参考:http://www.ruanyifeng.com/blog/2018/07/web-worker.html
1.了解事件循环的机制,了解了任务队列、js主线程、异步操作之间的相互协作;
2.了解两种任务队列:macrotask queue & microtask queue
microtask queue对应ECMAScript的promise属性(es6)DOM3的MutationObserver
3.通过JS的事件循环机制,可以清楚js代码的执行流,更好的控制代码
不够完善、不够详细、后续不断调整补充
看到一句总结关于事件队列的优先级的问题!
在JS中ES6 中新增的任务队列(promise)是在事件循环之上的,事件循环每次 tick 后会查看 ES6 的任务队列中是否有任务要执行,也就是 ES6 的任务队列比事件循环中的任务(事件)队列优先级更高。而 Promise 就使用了 ES6 的任务队列特性,也即在执行完任务栈后首先执行的是任务队列中的promise任务。其他异步操作加入队列的时间是没有相应优先级一说。
参考资料:
- Tasks, microtasks, queues and schedules . https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
- Understanding Javascript Function Executions — Call Stack, Event Loop , Tasks & more — Part 1 . https://medium.com/@gaurav.pandvia/understanding-javascript-function-executions-tasks-event-loop-call-stack-more-part-1-5683dea1f5ec
- How JavaScript works: an overview of the engine, the runtime, and the call stack . https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf
- 理解事件循环一(浅析) . https://github.com/ccforward/cc/issues/47
- 理解事件循环二(macrotask和microtask . https://github.com/ccforward/cc/issues/48
- 从Promise来看JavaScript中的Event Loop、Tasks和Microtasks . https://github.com/creeperyang/blog/issues/21
- 理解 Node.js 事件循环 . http://www.zcfy.cc/article/node-js-at-scale-understanding-the-node-js-event-loop-risingstack-1652.html
- Philip Roberts: What the heck is the event loop anyway? . https://2014.jsconf.eu/speakers/philip-roberts-what-the-heck-is-the-event-loop-anyway.html
- http://latentflip.com/loupe
- Promises/A+ . https://promisesaplus.com/
- 。。。当然也会查阅其他文章内容,这里就不一一感谢!