在開始講解之前,我們先來(lái)看一段代碼:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
});
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
});
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
});
各位小伙伴覺得上面的結(jié)果輸出會(huì)是多少呢?如果你沒有了解過javascript的執(zhí)行機(jī)制的話,上面的題目可能會(huì)讓你崩潰。
不過別著急,先往下看,我保證你看到最后,能輕輕松松寫出上面代碼的答案,并且完全了解其中的原理。
首先,希望大家記住一個(gè)要點(diǎn),javascript是單線程的語(yǔ)言。
因此,所有的javascript的異步特性都是基于單線程實(shí)現(xiàn)的,記住了這個(gè)特點(diǎn),我們?cè)偃ダ斫鈐avascript的很多機(jī)制就容易很多了。
我們先從簡(jiǎn)單的代碼說起,來(lái)引出今天的概念。
console.log('程序開始執(zhí)行~');
setTimeout(() => {
console.log('執(zhí)行setTimeout~');
}, 1000);
console.log('程序執(zhí)行結(jié)束~');
// 輸出結(jié)果:
// 程序開始執(zhí)行~
// 程序執(zhí)行結(jié)束~
// ...1s(這里表示等待時(shí)間)
// 執(zhí)行setTimeout~
我想小伙伴們對(duì)上面結(jié)果都不會(huì)有疑問,setTimeout是我們常用來(lái)做延遲執(zhí)行的全局函數(shù)。它接受兩個(gè)參數(shù),要執(zhí)行的函數(shù)a和等待的秒數(shù)x,函數(shù)a會(huì)在程序經(jīng)過x秒后執(zhí)行。
這里引出我們的第一個(gè)概念:同步函數(shù)和異步函數(shù)。上面的函數(shù)a就是異步函數(shù)了,它不是立刻執(zhí)行的函數(shù),而是要等待一段時(shí)間,或者說滿足一定的條件之后才執(zhí)行的函數(shù)。
不過,有時(shí)候我們明明設(shè)置了3秒的定時(shí),但是卻發(fā)現(xiàn)函數(shù)并沒有在3秒后執(zhí)行,有時(shí)候會(huì)更久,這又是為什么呢?
這要從javascript的執(zhí)行原理說起,js執(zhí)行的時(shí)候,有一個(gè)專門存放異步函數(shù)的地方,稱之為Event Table,而當(dāng)異步函數(shù)已經(jīng)滿足回調(diào)的執(zhí)行條件之后(比如時(shí)間過了x秒,異步請(qǐng)求返回了結(jié)果等等),原本放在Event Table的異步函數(shù)就會(huì)被放進(jìn)一個(gè)隊(duì)列中,這個(gè)隊(duì)列稱為Event Queue。
不要覺得這個(gè)隊(duì)列很深?yuàn)W,其實(shí)就是一個(gè)排隊(duì),里面放的都是回調(diào)函數(shù),它們正一個(gè)個(gè)等待著按順序執(zhí)行自身呢。來(lái)看下面的代碼:
?console.log('程序執(zhí)行開始~')
setTimeout(() => {
console.log('setTimeout執(zhí)行啦~')
}, 3000);
sleep(5000);
console.log('程序執(zhí)行結(jié)束~')
// 注:這里的sleep函數(shù)不是js的標(biāo)準(zhǔn)函數(shù),只是表示一個(gè)執(zhí)行需要5秒的函數(shù)。
// 輸出結(jié)果:
// 程序執(zhí)行開始~
// ...5s后
// 程序執(zhí)行結(jié)束~
// setTimeout執(zhí)行啦~
從上面結(jié)果我們可以看出,setTimeout并非是在setTimeout調(diào)用之后經(jīng)過3秒就馬上輸出結(jié)果"setTimeout執(zhí)行啦~",而是等待下方的sleep函數(shù)執(zhí)行完畢后才輸出的結(jié)果。
前面我已經(jīng)說過,要牢記javascript是單線程,那么它就一次只能運(yùn)行一個(gè)一段代碼。
因此,即使處于異步隊(duì)列的setTimeout函數(shù)已經(jīng)滿足執(zhí)行條件了,但是它還是得等待在Event Queue中,直到主線程執(zhí)行完畢才能執(zhí)行。
所以請(qǐng)記住,js會(huì)先執(zhí)行主線程的同步代碼,遇到setTimeout就將其回調(diào)函數(shù)注冊(cè)在Event Table中,然后當(dāng)異步函數(shù)滿足執(zhí)行條件之后,就會(huì)被放入Event Queue中,但是并不能馬上執(zhí)行,而是得等待主線程剩余代碼執(zhí)行完畢,隊(duì)列中的函數(shù)才能按順序執(zhí)行。
我想小伙伴們看到這里,已經(jīng)明白一點(diǎn)js的執(zhí)行機(jī)制了,那么我們一鼓作氣,繼續(xù)深入一下(其實(shí)也很簡(jiǎn)單),Promise和process又是怎樣的執(zhí)行機(jī)制呢?
在放代碼之前,我先介紹兩個(gè)基本概念:
- process.nextTick,我們知道瀏覽器環(huán)境下的setTimeout,那么process.nextTick就相當(dāng)于在node環(huán)境下執(zhí)行的setTimeout。
- 宏任務(wù)和微任務(wù),主線程一直在執(zhí)行script代碼,還有setTimeout、setInterval函數(shù)就是宏任務(wù),而Promise.then,process.nextTick則是微任務(wù)。
接下來(lái),我們看一段代碼:
console.log('程序執(zhí)行開始~')
setTimeout(() => {
console.log('setTimeout執(zhí)行啦~')
}, 3000);
new Promise((resolve) => {
console.log('promise開始執(zhí)行~');
resolve();
}).then(() => {
console.log('promise執(zhí)行結(jié)束~')
});
console.log('程序執(zhí)行結(jié)束~')
// 輸出結(jié)果:
// 程序執(zhí)行開始~
// promise開始執(zhí)行~
// 程序執(zhí)行結(jié)束~
// promise執(zhí)行結(jié)束~
// ...3s后
// setTimeout執(zhí)行啦~
emmm,這里的結(jié)果是不是就有點(diǎn)微妙了。
記得我們剛才說的宏任務(wù)和微任務(wù)嗎,js的執(zhí)行機(jī)制中,先是執(zhí)行完宏任務(wù)中的同步代碼,接著執(zhí)行微任務(wù),接著執(zhí)行宏任務(wù)的異步代碼。這樣說可能有點(diǎn)繞,我們結(jié)合上面的代碼來(lái)看
* 代碼一開始執(zhí)行,執(zhí)行的就是全局代碼,也就是宏任務(wù)的同步代碼;
* 遇到console.log,直接執(zhí)行,輸出"程序執(zhí)行開始~";
* 接著執(zhí)行,遇到setTimeout函數(shù),將其回調(diào)函數(shù)注冊(cè)進(jìn)宏任務(wù)的Event Queue(注意:宏任務(wù)和微任務(wù)分別有自己的Event Queue);
* 接著遇到new Promise,立刻執(zhí)行(new Promise里面的函數(shù)是立刻執(zhí)行的,只有.then函數(shù)里面才是放到微任務(wù)去執(zhí)行的,不要搞混咯),輸出"promise開始執(zhí)行";
* 接著遇到promise.then函數(shù),將其回調(diào)函數(shù)注冊(cè)到微任務(wù)的Event Queue;
* 接著繼續(xù)執(zhí)行,遇到console.log,直接輸出"程序執(zhí)行結(jié)束~"
* 到這里,宏任務(wù)的同步代碼就全部執(zhí)行完畢了,這時(shí)候,js引擎會(huì)去檢查微任務(wù)的Event Queue中是否存在回調(diào)函數(shù),這時(shí)微任務(wù)的Queue中還有一個(gè)函數(shù)未執(zhí)行,因此在這時(shí)候執(zhí)行,輸出"promise執(zhí)行結(jié)束~";
* 當(dāng)微任務(wù)的所有回調(diào)函數(shù)被執(zhí)行完了之后,一次事件循環(huán)就結(jié)束了。
* 這時(shí)候js引擎會(huì)檢查宏任務(wù)的Event Queue中是否還有未執(zhí)行的函數(shù),如果還有,將會(huì)開啟下一輪的事件循環(huán)。由于此時(shí)我們宏任務(wù)的Event Queue中還有未執(zhí)行的setTimeout,所以開啟下一輪事件循環(huán),執(zhí)行setTimeout回調(diào),輸出"setTimeout執(zhí)行啦~"
能堅(jiān)持到這里的小伙伴,相信你已經(jīng)學(xué)到了不少,給自己點(diǎn)個(gè)贊吧
接下來(lái),我們?cè)賮?lái)看一下加上process.nextTick之后的一個(gè)例子:
console.log('1')
setTimeout(() => {
console.log('2')
})
new Promise((resolve) => {
console.log('3')
resolve()
}).then(() => {
console.log('4')
})
process.nextTick(() => {
console.log('5')
})
new Promise((resolve) => {
console.log('6')
resolve()
}).then(() => {
console.log('7')
})
process.nextTick(() => {
console.log('8')
})
console.log('9')
// 輸出結(jié)果
// 1 3 6 9 5 8 4 7 2
是不是有一點(diǎn)一開始那塊代碼的味道了,上面的輸出結(jié)果也很容易理解:先是執(zhí)行了同步代碼,輸出:1 3 6 9,然后輸出微任務(wù)中的process.nextTick的回調(diào):5 8,然后輸出Promise.then中的回調(diào):4 7,最后輸出setTimeout的2,是不是一目了然。
上面唯一要注意點(diǎn)的就是:process.nextTick是要比Promise.then先執(zhí)行的(也許不同node版本環(huán)境下不同,這個(gè)要看具體執(zhí)行結(jié)果)。
好啦!終于這篇文章也要接近尾聲了,還在看的小伙伴再給自己點(diǎn)個(gè)贊吧,當(dāng)然也可以給我點(diǎn)個(gè)贊~你每一個(gè)小小的支持都是我堅(jiān)持下去的最大動(dòng)力。
接下來(lái)要進(jìn)入最后的重頭戲,按照我們前面所講的知識(shí),分析剛開始的代碼的執(zhí)行結(jié)果。這里再貼下一開始的代碼,最終結(jié)果我會(huì)在文章最后再貼出來(lái),所以小伙伴們也可以自己先看下,最后比對(duì)結(jié)果是否和文中的一致。
console.log('1');
setTimeout(() => {
console.log('2');
process.nextTick(() => {
console.log('3');
})
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5')
})
});
process.nextTick(() => {
console.log('6');
})
new Promise((resolve) => {
console.log('7');
resolve();
}).then(() => {
console.log('8')
});
setTimeout(() => {
console.log('9');
process.nextTick(() => {
console.log('10');
})
new Promise((resolve) => {
console.log('11');
resolve();
}).then(() => {
console.log('12')
})
});
接下來(lái)是分析過程:
程序開始,執(zhí)行宏任務(wù)同步代碼,遇到console.log,輸出:1;
遇到setTimeout1,將其放入宏任務(wù)Event Queue中;
遇到process.nextTick1,放入微任務(wù)Event Queue中;
遇到new Promise,直接執(zhí)行其中的代碼,輸出:7;
遇到Promise.then1函數(shù),將其放入微任務(wù)Event Queue;
繼續(xù)執(zhí)行,遇到setTimeout2,放入宏任務(wù)Event Queue;
-
此時(shí)任務(wù)隊(duì)列狀態(tài):
- 宏Queue: setTimeout1,setTimeout2;
- 微Queue: process.nextTick1、Promise.then1;
至此,宏任務(wù)同步代碼執(zhí)行完畢,檢測(cè)微任務(wù)隊(duì)列是否存在任務(wù),由于存在兩個(gè)微任務(wù),所以這時(shí)候執(zhí)行微任務(wù);
先執(zhí)行process.nextTick1,輸出:6;
接著執(zhí)行Promise.then1,輸出: 8;
微任務(wù)執(zhí)行完畢后,一次事件循環(huán)結(jié)束,js引擎持續(xù)檢測(cè)宏任務(wù)中是否存在任務(wù),存在的話開啟下一次事件循環(huán);由于存在兩個(gè)setTimeout,所以在滿足setTimeout執(zhí)行條件后,開啟下一次事件循環(huán),執(zhí)行回調(diào)函數(shù);
先執(zhí)行setTimeout1,遇到console.log,輸出:2;
接著遇到process.nextTick2,放入微任務(wù)Event Queue;
繼續(xù)執(zhí)行遇到new Promise,直接執(zhí)行,輸出:4;
然后遇到Promise.then2,放入微任務(wù)Event Queue;
-
至此setTimeout1執(zhí)行完畢,此時(shí)任務(wù)隊(duì)列狀態(tài):
- 宏Queue: setTimeout2;
- 微Queue: process.nextTick2、Promise.then2;
js引擎檢查微任務(wù)Event Queue中還存在兩個(gè)微任務(wù),因此執(zhí)行這兩個(gè)微任務(wù);
先執(zhí)行process.nextTick2,輸出:3;
接著執(zhí)行Promise.then2,輸出:5;
微任務(wù)執(zhí)行完畢,第二次事件循環(huán)結(jié)束;
js引擎持續(xù)檢查宏任務(wù)Event Queue中是否還有未執(zhí)行函數(shù),檢測(cè)到還有setTimeout2未執(zhí)行,因此開啟第三輪的事件循環(huán);
執(zhí)行setTimeout2,遇到console.log,輸出:9;
又遇到process.nextTick3,放入微任務(wù)隊(duì)列;
遇到new Promise,直接執(zhí)行,輸出:11;
遇到Promise.then3,放入微任務(wù)隊(duì)列;
-
至此,setTimeout2執(zhí)行完畢,此時(shí)任務(wù)隊(duì)列狀態(tài):
- 宏Queue: 無(wú);
- 微Queue: process.nextTick3、Promise.then3;
js引擎在檢測(cè)是否存在未執(zhí)行的微任務(wù),由于還有兩個(gè)微任務(wù)未執(zhí)行,因此將其執(zhí)行;
先執(zhí)行process.nextTick3,輸出:10;
接著執(zhí)行Promise.then3,輸出:12;
-
至此,微任務(wù)執(zhí)行完畢,事件循環(huán)結(jié)束;
最后程序輸出結(jié)果:1 7 6 8 2 4 3 5 9 11 10 12看到這里的小伙伴們,給自己點(diǎn)第三個(gè)贊吧。
怎么樣,是不是覺得已經(jīng)完全掌握了js的執(zhí)行機(jī)制,其實(shí)宏任務(wù)和微任務(wù)除了上文提到的那些,還有一些其他的,可以下來(lái)自己再去了解下~
最后,感謝大家的閱讀,如果覺得文章寫的還可以的話,可以給我點(diǎn)個(gè)贊、點(diǎn)個(gè)關(guān)注、或者直接關(guān)注本人,我會(huì)持續(xù)分享更多優(yōu)質(zhì)的技術(shù)文章,我們一起加油吧!