最近在看JavaScript异步部分的内容,发现事件循环中的任务执行顺序问题在面试中经常出现,自己以前从来没注意过,所以就在网上找了一些教程,整理成笔记分享给大家。
JavaScript里的事件循环是JavaScript的一大特色,最近在看JavaScript忍者秘籍的时候刚好看到了异步函数相关的章节,于是从网上找各种资料学习,并整理成笔记分享给大家。
从JavaScript在内存中的数据结构看,有函数执行上下文栈,还有一个存储数据的堆,以及一个存放任务的任务队列。
因为JavaScript里有很多异步操作(大部分都是),又因为JavaScript是单线程的,所以我们需要一个任务队列来对那些异步的任务进行排序处理。
实际上有两个任务队列,宏任务和微任务队列。
事件循环的实现很简单,就是没有任务的时候就监听是否有事件到来,如果一下子来了很多个事件就按序排入任务队列里,然后再以先进先出的方式执行之。
while (queue.waitForMessage()) {
queue.processNextMessage();
}
这里有几个点要注意:
- 当JavaScript的一个函数执行时,不会被抢占,只有完整执行完后其他任务才会被执行。缺点是一些大任务可能会导致长时间无响应,影响交互。
- setTimeout()的第二个参数是指被加入到队列的事件,不是执行的事件,也就是说第一个参数的回调最早会在这个时间被执行。
- 因为这个特性,JavaScript可以在等待一个事件的时候先做其他事件(通过回调函数)
宏任务队列
宏任务产生的方式有:
1.当外部脚本 <script src="..."> 加载完成时,任务就是执行它。
2.当用户移动鼠标时,任务就是派生出 mousemove 事件和执行处理程序。
3.当安排的(scheduled)setTimeout 时间到达时,任务就是执行其回调。
……诸如此类。
两个细节
- 引擎执行任务时永远不会进行渲染(render)。如果任务执行需要很长一段时间也没关系。仅在任务完成后才会绘制对 DOM 的更改。
- 如果一项任务执行花费的时间过长,浏览器将无法执行其他任务,例如处理用户事件。因此,在一定时间后,浏览器会抛出一个如“页面未响应”之类的警报,建议你终止这个任务。这种情况常发生在有大量复杂的计算或导致死循环的程序错误时。
用例1:拆分大任务:如果一个任务过大,可以拆成几个小任务,利用setTimeout()在每个小任务执行完成后把下一个小任务添加到宏任务队列里,防止一次占用太多资源导致无响应
用例2:进度条:拆成几个小任务后,可以渲染一个显示任务完成程度的进度条,提供更好的交互
用例3:在任务后面执行新任务
微任务队列
微任务产生的方式有:
1.Promise回调函数执行到resolve()或reject(),会把then()的第一个回调和第二个回调或者catch()(分别对应resolve()和reject())加入到微任务队列
2.queueMicrotask(func)把func加入到微任务队列中
一个宏任务执行完成后,所有队列里的微任务都会被执行,然后再进行渲染。
.then这些只是注册了任务,把任务加进队列的是resolve被调用。
then不能调用resolve,里面有promise的话,里面的promise调用了resolve算是
完成任务,返回一个新的promise。
then()中只接受两个函数:handleFulfilled
,handleRejected
, 并且这两个函数中的参数只有一个, 就是上一个Promise中resolve出来的或reject出来的.
Promise的链式调用
先把setTimeout里的回调加入到宏任务队列里,这个时候队列里没有其他任务,开始执行,输出1。resolve()调用后,开始执行promise后面then注册的回调函数。输出2。resolve()执行后,then里的回调开始执行,输出3。因为最外层的then里的promise已经被resolve了,所以这里then也变为resolved,把最外层最下面的then注册的回调加入到任务队列里,输出5。最后又回到里面的最后一个then,输出4。
回调是异步的
1.当执行 then 方法时,如果前面的 promise 已经是 resolved 状态,则直接将回调放入微任务队列中(但是执行回调还是要等到所有同步任务都结束后)
2.当一个 promise 被 resolve 时,会遍历之前通过 then 给这个 promise 注册的所有回调,将它们依次放入微任务队列中
3.resolve()它的作用除了将当前的 promise 由 pending 变为 resolved,还会遍历之前通过 then 给这个 promise 注册的所有回调,将它们依次放入微任务队列中,很多人以为是由 then 方法来触发它保存回调,而事实上 then 方法即不会触发回调,也不会将它放到微任务,then 只负责注册回调,由 resolve 将注册的回调放入微任务队列,由事件循环将其取出并执行。
4.对于 then 方法返回的 promise 它是没有 resolve 函数的,取而代之只要 then 中回调的代码执行完毕并获得同步返回值,这个 then 返回的 promise 就算被 resolve
同步返回值
的意思换句话说,如果 then 中的回调返回了一个 promise,那么 then 返回的 promise 会等待这个 promise 被 resolve 后再 resolve。执行完3, then 中要到下一个循环才能执行。
https://blog.csdn.net/ywCSD/article/details/108462164
async/await语法下的任务队列
async返回一个promise,await等待一个promise执行完成。
这里其实可以把await语句下面的所有语句理解成then里的回调。
因为then里的回调也是等待promise完成后加入队列。
然后就一样排序就行了。
async function async1() {
console.log( 'async1 start' )
await async2()
console.log( 'async1 end' )
}
async function async2() {
console.log( 'async2' )
}
async1()
console.log( 'script start' )
//输出结果
async1 start
async2
script start
async1 end
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout')
},0)
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
}).then(function(){
console.log('promise2')
})
console.log('script end')
//输出结果
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
async function testSometing() {
console.log("执行testSometing");
return "testSometing";
}
async function testAsync() {
console.log("执行testAsync");
return Promise.resolve("hello async");
}
async function test() {
console.log("test start...");
const v1 = await testSometing();
console.log(v1);
const v2 = await testAsync();
console.log(v2);
console.log(v1, v2);
}
test();
var promise = new Promise((resolve) => {
console.log("promise start..");
resolve("promise");
}); //3
promise.then((val) => console.log(val));
setTimeout(()=>{console.log("setTime1")},3000);
console.log("test end...")
//输出结果
test start...
执行testSometing
promise start..
test end...
testSometing
执行testAsync
promise
setTime1
hello async
testSometing hello async
setImmediate()等等
这些都是nodejs才有的函数,因为还没具体的学习nodejs,先不看了,等后面学到在看,先贴个链接
https://blog.csdn.net/dennis_jiang/article/details/105044361
上述提到setTimeout()可以把任务分成几个小任务,从而降低大任务占用资源的时间。但是这样做也会使这个大任务的处理时间延长。下面看一种新的解决方法。
setImmediate(handler) 并不像 setTimeout(handler, 0) 由event loop检测系统时间是否到点然后向事件队列插入一个事件,然后调用事件的回调方法handler。而是监控UI线程的调用栈,一旦调用栈为空则将handler压栈。