JavaScript 是一門(mén)單線程的語(yǔ)言晌砾?
首先對(duì)于一個(gè)學(xué)前端的人來(lái)說(shuō)凛捏,肯定是聽(tīng)過(guò)這樣一句話(huà)担忧, JavaScript 是一門(mén)單線程的語(yǔ)言 。一些人聽(tīng)到這里的時(shí)候就會(huì)想坯癣,啊瓶盛,JS這語(yǔ)言不太行啊,只能單線程示罗,太垃圾了惩猫。有這種想法人,只能說(shuō)他還不了解JS蚜点。說(shuō)JS是一門(mén)單線程的語(yǔ)言轧房,并不是說(shuō)它本身有什么缺陷,只能和單線程扯上聯(lián)系绍绘,多線程這種能大大提高效率的事奶镶,和它沒(méi)什么關(guān)系。那你就錯(cuò)了陪拘。如今有了node厂镇,js不僅能在瀏覽器上執(zhí)行了,在后端用js寫(xiě)個(gè)多線程還不是簡(jiǎn)簡(jiǎn)單單的事情么左刽。那你可能就要問(wèn)了捺信,那為什么JS在瀏覽器中就非要單線程執(zhí)行呢? 其實(shí)JavaScript的單線程悠反,是與它的功能相關(guān)的残黑。作為瀏覽器腳本語(yǔ)言馍佑,JavaScript的主要用途是頁(yè)面與用戶(hù)的交互,操作DOM元素梨水。這就決定了它只能是單線程的拭荤,否則會(huì)帶來(lái)很復(fù)雜的問(wèn)題。比如疫诽,JavaScript如果說(shuō)是多線程的舅世,一個(gè)線程在某個(gè)DOM節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn)奇徒,那么瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)呢雏亚? 可是,單線程就意味著同一個(gè)時(shí)間只能做一件事摩钙,如果都是同步執(zhí)行下去的話(huà)罢低,就會(huì)出現(xiàn)阻塞的現(xiàn)象,比如發(fā)送一個(gè)ajax請(qǐng)求胖笛,在等待服務(wù)器返回?cái)?shù)據(jù)的過(guò)程中网持,其它什么事情都執(zhí)行不了,這種情況想想也是災(zāi)難性的长踊。所以功舀,在JS中,定時(shí)器身弊,ajax請(qǐng)求辟汰,事件綁定等操作都是異步執(zhí)行的。至于單線程中怎么進(jìn)行異步操作阱佛,這就要好好說(shuō)一說(shuō)我們今天的主角帖汞,JavaScript的執(zhí)行機(jī)制以及其中的事件循環(huán)(Event Loop)了。
初步了解JavaScript的執(zhí)行機(jī)制
在瀏覽器開(kāi)始解析js文件的時(shí)候瘫絮,會(huì)有一個(gè)主執(zhí)行棧涨冀,js代碼從上到下在主執(zhí)行棧中執(zhí)行(之前會(huì)有變量提升,函數(shù)聲明提升一系列操作)麦萤,遇到函數(shù)調(diào)用的時(shí)候,會(huì)將函數(shù)放到調(diào)用棧(call stack)中執(zhí)行(調(diào)用棧還是在主線程壮莹,執(zhí)行順序中和全局上下文執(zhí)行順序基本相同姻檀,至于函數(shù)執(zhí)行上下文命满,函數(shù)調(diào)用完后彈出調(diào)用椥灏妫或繼續(xù)保留形成閉包就不在這里詳細(xì)闡述了)胶台,在沒(méi)有遇到異步任務(wù)之前歼疮,js就是這樣一步步向下執(zhí)行的诈唬。當(dāng)遇到異步函數(shù)的時(shí)候,并不會(huì)將這個(gè)任務(wù)放到直接放到主線程中執(zhí)行铸磅,而是先把它放到任務(wù)隊(duì)列( task queue )中赡矢,等到主線程的所有任務(wù)執(zhí)行完后,就會(huì)從任務(wù)隊(duì)列中拿出一個(gè)任務(wù)阅仔,將其代碼放到主線程中執(zhí)行吹散,至于代碼里面又存在的異步任務(wù)八酒,依舊是先放到任務(wù)隊(duì)列中去,就是這樣從任務(wù)隊(duì)列中拿任務(wù)界轩,在主線程中執(zhí)行闭树,遇到異步任務(wù)再放到任務(wù)隊(duì)列中這樣的一個(gè)循環(huán)過(guò)程,就成為事件循環(huán)(Event Loop)报辱。聽(tīng)起來(lái)是不是還挺簡(jiǎn)單的,別著急幅疼,里面還有一些更加細(xì)節(jié)的東西要說(shuō)的昼接。
異步任務(wù)
首先我們要先明確異步任務(wù)都有哪些:
1、定時(shí)器都是異步操作
2逐工、事件綁定都是異步操作
3漂辐、AJAX中的操作
4、回調(diào)函數(shù)可以理解為異步(不是嚴(yán)謹(jǐn)?shù)漠惒讲僮鳎?/p>
其中髓涯,為了防止回調(diào)地獄的形成,es6又提供了promise和async/await解決方案(用的最多的兩種方案)蚓再,對(duì)于promise成功后的then()和await等待結(jié)果的任務(wù)的操作,也是要進(jìn)入任務(wù)隊(duì)列( task queue )摘仅。這時(shí)就又要提到任務(wù)隊(duì)列中又細(xì)分的宏任務(wù)(macrotask)和微任務(wù)(microtask)了
詳細(xì)認(rèn)知JavaScript的執(zhí)行機(jī)制
對(duì)于我們熟知的異步任務(wù),是放入宏任務(wù)隊(duì)列中的实檀,promise狀態(tài)為resolve后的then()和await等待結(jié)果的任務(wù)是放到微任務(wù)隊(duì)列的膳犹。每次主線程的代碼執(zhí)行完以后,是會(huì)先把微任務(wù)隊(duì)列中的任務(wù)給執(zhí)行完然后才會(huì)拿宏任務(wù)隊(duì)列中的任務(wù)放到主線程中執(zhí)行的须床。再結(jié)合上面說(shuō)的執(zhí)行順序,js代碼的執(zhí)行機(jī)制就很明確了钠惩。
為了能使你更好更快的理解族阅,我將用提煉的語(yǔ)言給你再總解一遍,首先愧沟,你要先知道我提到的幾個(gè)術(shù)語(yǔ)的含義:
1.解決全局任務(wù):是指執(zhí)行整個(gè)js文件鲤遥,其中同步任務(wù)放到主線程中執(zhí)行,異步任務(wù)放到對(duì)應(yīng)的任務(wù)隊(duì)列中混坞。當(dāng)主線程中將js文件中的的同步任務(wù)執(zhí)行完后钢坦,就算解決了解決全局任務(wù)。
2.解決一個(gè)微任務(wù):是指執(zhí)行一個(gè)微任務(wù)中的代碼爹凹,將這個(gè)微任務(wù)代碼中的同步任務(wù)放到主線程中執(zhí)行逛万,異步任務(wù)放到對(duì)應(yīng)的任務(wù)隊(duì)列中。當(dāng)主線程中將這個(gè)微任務(wù)中的同步任務(wù)執(zhí)行完后宇植,就算解決了一個(gè)微任務(wù)。
3.解決一個(gè)宏任務(wù):將一個(gè)宏任務(wù)放在主線程中執(zhí)行( 宏任務(wù)肯定是指定了回調(diào)函數(shù)的忙上,主線程執(zhí)行異步任務(wù)闲坎,其實(shí)就是執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù))對(duì)于這個(gè)宏任務(wù)中新形成的異步任務(wù)放在對(duì)應(yīng)的任務(wù)隊(duì)列中。當(dāng)主線程將這個(gè)宏任務(wù)執(zhí)行完以后梗逮,就算解決了一個(gè)宏任務(wù)绣溜。
總結(jié)一下就是:
1.解決全局任務(wù)(要明確,只要執(zhí)行js文件底哗,就會(huì)而且只會(huì)解決一次全局任務(wù))
2.依次解決微任務(wù)(解決一個(gè)微任務(wù)后锚沸,再解決下一個(gè)微任務(wù),直到微任務(wù)隊(duì)列中的所有微任務(wù)都解決完)
3.解決一個(gè)宏任務(wù)(可能會(huì)在任務(wù)隊(duì)列中添加了新的微任務(wù)和宏任務(wù))
4.重復(fù)2.3步操作前标,知道解決所有的任務(wù)
5.結(jié)束
其中恬叹,要注意的是,promise和async本身并不是直接放到微任務(wù)隊(duì)列的唯鸭。
//promise
new Promise((resolve,reject)=>{
console.log(1); //在new Promies的時(shí)候硅确,會(huì)直接執(zhí)行里面的代碼的
resolve('成功的數(shù)據(jù)') //then中的任務(wù)是在執(zhí)行resolve的時(shí)候才放到微任務(wù)隊(duì)列的
console.log(2)
}).then(res=>{
console.log(3)
})
console.log(4)
//運(yùn)行的結(jié)果是:1 2 4 3
//async
async function aaa(){
console.log(1) //不是說(shuō)整個(gè)async都會(huì)放到微任務(wù)隊(duì)列中,等待前面的部分是同步執(zhí)行的
let n = await async1() //在await獲取到值后缭付,將后面的代碼放到微任務(wù)隊(duì)列中循未,即使await等待的函數(shù)里面并沒(méi)有異步操作
console.log(2)
}
function async1(){
console.log(3)
}
aaa() //運(yùn)行結(jié)果是:1 3 2
看完整個(gè)上面的文字描述,現(xiàn)在對(duì)js的整個(gè)執(zhí)行機(jī)制你心里面應(yīng)該有一個(gè)自己的認(rèn)知了绣檬,那帶著你自己的想法,看一看下面的代碼娇未,通過(guò)代碼零抬,相信你會(huì)對(duì)這個(gè)過(guò)程的認(rèn)知更加清晰,甚至可能會(huì)打破你已有的認(rèn)知平夜,讓你對(duì)整個(gè)過(guò)程有一個(gè)更加準(zhǔn)確的理解。
代碼通關(guān)
答案在最后
1. console.log('1');
2. setTimeout(function() {
3. console.log('2');
4. new Promise(function(resolve) {
5. console.log('3');
6. resolve();
7. }).then(function() {
8. console.log('4')
9. })
10. },0)
11. new Promise(function(resolve) {
12. console.log('5');
13. resolve();
14. }).then(function() {
15. console.log('6')
16. })
17. setTimeout(function() {
18. console.log('7');
19. new Promise(function(resolve) {
20. console.log('8');
21. resolve();
22. }).then(function() {
23. console.log('9')
24. })
25. console.log('10')
26. },0)
27. console.log('11')
看起來(lái)是不是有點(diǎn)棘手嚼松,不要慌锰扶,跟著我一步步的把它給吃掉。
macrotask(宏任務(wù)隊(duì)列):[]; microtask(微任務(wù)隊(duì)列):[];console(控制臺(tái)輸出):[]
第1行坷牛,最先輸出1是毫無(wú)問(wèn)題的(console:[1])
第2行京闰,遇到定時(shí)器,直接放到宏任務(wù)隊(duì)列中蹂楣,然后跳到11行。(macrotask[第2行])
第11行肄扎,創(chuàng)建一個(gè)promise實(shí)例赁酝,會(huì)直接執(zhí)行傳進(jìn)來(lái)的函數(shù)
第12行,輸出5(console:[1衡载,5])
第13行隙袁,將這個(gè)promise實(shí)例的狀態(tài)設(shè)置為resolve弃榨,同時(shí)將其then里面的函數(shù)放到微任務(wù)隊(duì)列中(microtask:[第14行])
第17行猜揪,遇到定時(shí)器,直接放到宏任務(wù)隊(duì)列中,然后跳到27行划咐。(macrotask[第2行褐缠,第17行])
第21行,輸出11(console:[1队魏,5,11])
此時(shí)主線程任務(wù)執(zhí)行完畢(解決了全局任務(wù))官帘,要開(kāi)始依次解決微任務(wù)昧谊。跳到第14行
第14,15行 輸出6(console:[1涌哲,5尚镰,11,6]初烘,microtask:[])
此時(shí)主線程任務(wù)又執(zhí)行完畢(解決了一個(gè)微任務(wù))同時(shí)微任務(wù)隊(duì)列也被清空敞曹,開(kāi)始解決宏任務(wù)。跳到第2行
第2澳迫,3行 輸出2(console:[1橄登,5讥此,11谣妻,6,2])
第4行 創(chuàng)建一個(gè)promise實(shí)例他巨,會(huì)直接執(zhí)行傳進(jìn)來(lái)的函數(shù)
第5行 輸出3(console:[1减江,5,11辈灼,6巡莹,2,5降宅,3])
第6行 將這個(gè)promise實(shí)例的狀態(tài)設(shè)置為resolve钉鸯,同時(shí)將其then里面的函數(shù)放到微任務(wù)隊(duì)列中(microtask:[第7行],macrotask[第17行])
此時(shí)主線程再次實(shí)行完畢(解決了一個(gè)宏任務(wù))贸营,再執(zhí)行下一個(gè)宏任務(wù)之前岩睁,要先把微任務(wù)清空,所以跳到第7行捕儒,解決微任務(wù)
第7,8行:輸出4(console:[1阎毅,5点弯,11,6狼钮,2,5莲镣,3涎拉,4])
此時(shí)主線程再次執(zhí)行完畢(解決了一個(gè)微任務(wù)),同時(shí)微任務(wù)隊(duì)列也被清空区岗,開(kāi)始解決宏任務(wù)毁枯,跳到第17行
17叮称,18行輸出7(console:[1,5赂韵,11挠蛉,6,2质涛,5掰担,3,4毡代,7])
第19行 創(chuàng)建一個(gè)promise實(shí)例勺疼,會(huì)直接執(zhí)行傳進(jìn)來(lái)的函數(shù)
第20行 輸出8(console:[1,5酪耕,11耕肩,6问潭,2婚被,5,3灾茁,4谷炸,7旬陡,8])
第21行 將這個(gè)promise實(shí)例的狀態(tài)設(shè)置為resolve,同時(shí)將其then里面的函數(shù)放到微任務(wù)隊(duì)列中(microtask:[第22行])
第25行:輸出10(console:[1驶睦,5匿醒,11,6廉羔,2憋他,5,3举瑰,4此迅,7,8忍些,10])
此時(shí)主線程再次執(zhí)行完畢(解決了一個(gè)微任務(wù))坎怪,微任務(wù)隊(duì)列中又有了新任務(wù),繼續(xù)執(zhí)行嘁酿,跳到第22行
第22,23行:輸出9(console:[1闹司,5游桩,11,6盹憎,2铐刘,5,3奶稠,4膝蜈,7,8洽沟,10慷暂,9])
一步步下來(lái)是不是很清爽芹关,好像也就那么回事,相信現(xiàn)在你對(duì)整個(gè)流程已經(jīng)熟記在心了诗祸,接下來(lái)再做一道題轴总,展現(xiàn)一下自己的實(shí)力吧
1. async function async1() {
2. console.log('1');
3. await async2();
4. console.log('2');
5. }
6.
7. async function async2() {
8. console.log('3');
9. }
10. console.log('4');
11.
12. setTimeout(function() {
13. console.log('5');
14. }, 0)
15. async1();
16.
17. new Promise(function(resolve) {
18. console.log('6');
19. resolve();
20. }).then(function() {
21. console.log('7');
22. });
23. console.log('8');
macrotask(宏任務(wù)隊(duì)列):[]; microtask(微任務(wù)隊(duì)列):[];console(控制臺(tái)輸出):[]
1-9.定義了兩個(gè)異步函數(shù)怀樟,并沒(méi)有執(zhí)行,直接跳過(guò)
10.輸出4(console:[4])
12.定時(shí)器械荷,放到宏任務(wù)隊(duì)列中(macrotask[第12行])
15.執(zhí)行async1,跳到第1行
2.輸出1(console:[4痹兜,1])
3.執(zhí)行async2关拒,跳到第7行
7-8.輸出3(console:[4,1谐算,3])
回到第3行归露,將后面的代碼放到微任務(wù)隊(duì)列中(microtask:[第4行]),此時(shí)async1函數(shù)執(zhí)行完畢恐锦,回到調(diào)用函數(shù)的地方15行疆液,繼續(xù)執(zhí)行后面的代碼
//這一組邏輯大家應(yīng)該已經(jīng)很熟悉了
17.創(chuàng)建一個(gè)promise實(shí)例堕油,會(huì)直接執(zhí)行傳進(jìn)來(lái)的函數(shù)
18.輸出6(console:[4,1卜录,3眶明,6])
19.將這個(gè)promise實(shí)例的狀態(tài)設(shè)置為resolve丑瞧,同時(shí)將其then里面的函數(shù)放到微任務(wù)隊(duì)列中(microtask:[第4行犬辰,第20行])
23.輸出8(console:[4幌缝,1,3浴栽,6,8])
此時(shí)主線程任務(wù)執(zhí)行完畢(解決了全局任務(wù))被廓,要開(kāi)始依次解決微任務(wù)萝玷。跳到第4行
4.輸出2(console:[4,1蜓斧,3睁冬,6,8直奋,2])
時(shí)主線程再次執(zhí)行完畢(解決了一個(gè)微任務(wù))脚线,微任務(wù)隊(duì)列中還有任務(wù)弥搞,繼續(xù)執(zhí)行,跳到第20行
20-21 輸出7(console:[4,1肛度,3投慈,6,8加袋,2抱既,7])
此時(shí)主線程任務(wù)又執(zhí)行完畢(解決了一個(gè)微任務(wù))同時(shí)微任務(wù)隊(duì)列也被清空,開(kāi)始解決宏任務(wù)蚀之。跳到第12行
12-13 輸出5 (console:[4,1寿谴,3失受,6拂到,8,2惠桃,7辖试,5])
相信,將這兩題的過(guò)程一步步的搞明白罐孝,js的執(zhí)行過(guò)程對(duì)你來(lái)說(shuō)已經(jīng)不是什么問(wèn)題了呐馆。
答案:
第一題輸出順序:1 5 11 6 2 3 4 7 8 10 9
第二題輸出順序:4,1莲兢,3汹来,6,8改艇,2收班,7,5
答錯(cuò)了不要緊谒兄,相信我的答案分析一定能讓你搞明白到底是哪里出了問(wèn)題摔桦。答對(duì)了,那就恭喜你了承疲,你對(duì)js執(zhí)行的機(jī)制應(yīng)該掌握的是很不錯(cuò)了,但還是可以看一看答案分析哦燕鸽,也許你和我有不一樣的理解兄世,那你可一定要想辦法聯(lián)系我一下哦。