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,定時器線程等等,我將會在下一篇解釋瀏覽器多線程的事情。

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