js 事件循环执行顺序(setTimeout,async,promise多层嵌套)

我们知道JS是单线程脚步语言,设计为单线程是有好处的,如果为多线程,当两个人同时操作了同一个资源,这样会造成同步问题,不知以谁为准。
同时,单线程也存在一些问题,比如:

for(var i = 0;i<1000;i++){
	console.log(1)	
}
console.log(2)
结果就是,2将会等待1全部输出完毕后在执行,浪费了大量的时间

我们希望在等待的时间去做别的事,所以,js诞生了异步。
js 的异步有很多,像事件绑定、ajax请求,promise、回调、订阅监听、async等等。
异步与同步不同,当JS解析执行时,会被js引擎分为两类任务,同步任务(synchronous) 和 异步任务(asynchronous)。

对于同步任务来说,会被推到执行栈按顺序去执行这些任务。
对于异步任务来说,当其可以被执行时,会被放到一个 任务队列(task queue) 里等待JS引擎去执行。

当执行栈中的所有同步任务完成后,JS引擎才会去任务队列里查看是否有任务存在,并将任务放到执行栈中去执行,执行完了又会去任务队列里查看是否有已经可以执行的任务。这种循环检查的机制,就叫做事件循环(Event Loop)。

对于任务队列,其实是有更细的分类。其被分为 微任务(microtask)队列 & 宏任务(macrotask)队列

宏任务: 整体js代码,setTimeout、setInterval等,会被放在宏任务(macrotask)队列。
微任务: Promise的then、Mutation Observer等,会被放在微任务(microtask)队列。
当js开始执行的时候,先执行主执行栈的代码,遇到异步任务,将任务放入异步任务队列里(微任务和宏任务分别放入各自的任务队列),
然后开始执行异步的任务,先执行微任务,后执行宏任务。

如下图所示
在这里插入图片描述
下图为一次Eventloop(事件循环)
在这里插入图片描述
下面一个简单的例子,看一下输出什么

console.log('script start');
setTimeout(function() {
  console.log('timeout1');
}, 10);

new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
    console.log('then1')
})

console.log('script end');

分析:

从上到下开始执行,主线程宏任务 输出 " script start" ,
遇到settimeout,放入宏任务队列Event Queue H["timeout1"],继续,
new promise,直接打印"promise1"["script start""promise1"],
然后promise.then,放入微任务队列Event Queue W["then1"]
settimeout,放入宏任务队列 H["timeout1""timeout2"],
然后console,直接打印"script end"["script start""promise1""script end"]
现在主栈执行完毕,开始执行异步,先执行微任务队列,结果["script start""promise1""script end""then1"],
至此,该次事件循环已经结束,开启下一次事件循环,从宏任务队列开始打印"timeout1",继续查找微任务队列,没有微任务,结束第二次事件循环
开启第三次执行宏任务"timeout2",结果就是["script start""promise1""script end""then1""timeout1""timeout2"]
tips:setimeout 定时w3c标准最小为4ms,即使设置时间为0,系统会默认为4ms

然后看一个复杂的例子,先自己分析一下输出什么,结果最后我会分析。

setTimeout(function() {
    console.log('timeout1');
},200)
async function async1(){
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}
async function async2(){
  console.log('async2')
}
async1();

new Promise(function (resolve) {
  console.log('111');         
  resolve();   
  new Promise(function(resolve){
  	console.log('222')
  	setTimeout(function(){
  	console.log('333')	
  	})
  	resolve()
  }).then(function(){
  	console.log('444')
  	setTimeout(function(){
  	console.log('555')	
  	})
  })                    
}).then(function (resolve) {       
  console.log('666')               
setTimeout(function(){
  	console.log('777')	
  	},200)
});

setTimeout(function(){
	console.log('timeout2')
},100)

结果分析

开始,从上到下,遇到settimeout,放入宏任务H["timeout1(200)"],注意时间是200ms,
然后遇到两个async函数,执行async1(),主栈输出["async1 start"],继续,执行await async2,注意这里,我们要分析await,
实际上async是promise的语法糖,我们要将其转换为promise,async会返回一个隐式的promise [async function MDN的解释](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function)
将async1和async2转换如下,这样就容易理解了
 function async1(){
  Promise.resolve(console.log('async1 start'))
  Promise.resolve(console.log('async2'))
}
 function async2(){
	Promise.resolve(console.log('async2'))
}
现在执行async2 Promise.resolve(console.log('async2')),主栈输出["async1 start""async2"],

将 console.log('async1 end')
放入微任务栈, 这里用P1代替["P1"],继续,输出“111”,主栈["async1 start""async2","111"],然后resolve,
将 console.log('666')               
setTimeout(function(){
  	console.log('777')	
  	},200),
  	放入微任务,用P2表示,W["P1","P2"]
我们发现在promise内部又有一个promise,继续执行,主栈["async1 start""async2","111""222"],
宏任务“333”,时间为0,放入第一位,H["333","timeout1(200)",],继续resolve,
将console.log('444')
  	setTimeout(function(){
  	console.log('555')	
  	})
放入微任务,用P3表示,因为在函数内部,先执行 W["P1","P3","P2"],
继续,遇到宏任务timeout2,时间为100ms,所以排第二H["333","timeout2(100)","timeout1(200)"]
OK ,主任务结束,目前,主栈["async1 start""async2","111""222"],微任务W["P1","P3","P2"],
宏任务H["333","timeout2(100)","timeout1(200)","777"],接下来执行微任务P1,
输出"async1 end",
目前W["P3","P2"],执行P3,P2,目前微任务W["444","666"],
宏任务H["333","555","timeout2(100)","timeout1(200)"],目前为止,所有任务队列已经理清楚,先执行微任务队列,
结束事件循环,继续宏任务的新的事件循环,注意:每一个宏任务都是一次新的事件循环
最终结果:
["async1 start""async2","111""222","async1 end","444","666","333","555","timeout2",
"timeout1","777"]

相信这应该是最详细的式例分析了,这篇文章花了我两天的时间,研究的同时,也发现了自己的很多不足。
异步的方式有很多,例子中仅仅使用了三种,实际上,js的异步是浏览器开起的不同线程决定的。比如事件,http,定时器线程等等,我将会在下一篇解释浏览器多线程的事情。

  • 4
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值