簡(jiǎn)單而面試中又常見的知識(shí)點(diǎn):JS執(zhí)行機(jī)制

在開始講解之前,我們先來(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ù)文章,我們一起加油吧!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容