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)队列

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

如下图所示
在这里插入图片描述
在这里插入图片描述
下面一个简单的例子,看一下输出什么

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,放入宏任务H["timeout1"],继续,
遇到promise,输出["script start""promise1"],
然后resolve,放入微任务W["then1"],
继续settimeout,放入宏任务H["timeout1""timeout2"],
最后主栈输出["script start""promise1""script end"],OK,
现在主栈执行完毕,开始执行异步,先执行微任务,结果["script start""promise1""script end""then1"],
再执行宏任务,结果就是["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(){
  console.log('async1 start')
  const p = async2()
   return new Promise(resolve=>{
   resolve()
  }).then(()=>{
  		p.then(()=>{
   		console.log('async1 end')
   	})
  })
}
 function async2(){
	console.log('async2')
	return new Promise(resolve=>{
		resolve()
	})
}
现在执行async2,主栈输出["async1 start""async2"],程序中多了一个嵌套的promise,new Promise(resolve=>{resolve()}).then(()=>{
   		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,P1是promise,执行then,"async1 end",放入微任务,
目前W["P3","P2","async1 end"],执行P3,P2,"async1 end",目前微任务W["444","666","async1 end"],
宏任务H["333","555","timeout2(100)","timeout1(200)"],目前为止,所有任务队列已经理清楚,先微后宏,
最终结果:
["async1 start""async2","111""222","444","666","async1 end","333","555","timeout2",
"timeout1","777"]

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章