前端干貨:JS的執(zhí)行順序

JS的運(yùn)行機(jī)制

先來一個(gè)今日頭條的面試題

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');


/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/

1. 單線程的JavaScript

js是單線程的,基于事件循環(huán),非阻塞IO的。
特點(diǎn): 處理I/O型的應(yīng)用,不適合CPU運(yùn)算密集型的應(yīng)用。
說明: 事件循環(huán)中使用一個(gè)事件隊(duì)列,在每個(gè)時(shí)間點(diǎn)上,系統(tǒng)只會處理一個(gè)事件,即使電腦有多個(gè)CPU核心,也無法同時(shí)并行的處理多個(gè)事件。因此,node.js在I/O型的應(yīng)用中,給每一個(gè)輸入輸出定義一個(gè)回調(diào)函數(shù),node.js會自動將其加入到事件輪詢的處理隊(duì)列里,當(dāng)I/O操作完成后,這個(gè)回調(diào)函數(shù)會被觸發(fā),系統(tǒng)會繼續(xù)處理其他的請求。

  • 然而單線程不應(yīng)該是自上而下按照順序執(zhí)行的嗎?
  • 下面的代碼輸出順序就被打亂了
function fn(){
    console.log('start');
    setTimeout(()=>{
        console.log('setTimeout');
    },0);
    console.log('end');
}

fn() // 輸出 start end setTimeout

2. JavaScript中的同步異步

js的同步異步是如何實(shí)現(xiàn)的?
js中包含諸多創(chuàng)建異步的函數(shù)如:
seTimeout,setInterval,dom事件,ajax,Promise,process.nextTick等函數(shù)

timg.jpeg
  1. 因?yàn)閱尉€程,所以代碼自上而下執(zhí)行,所有代碼被放到執(zhí)行棧中執(zhí)行;
  2. 遇到異步函數(shù)將回調(diào)函數(shù)添加到一個(gè)任務(wù)隊(duì)列里面;
  3. 當(dāng)執(zhí)行棧中的代碼執(zhí)行完以后,會去循環(huán)任務(wù)隊(duì)列里的函數(shù);
  4. 任務(wù)隊(duì)列里的函數(shù)放到執(zhí)行棧中執(zhí)行;
  5. 如此往復(fù),稱為事件循環(huán);
    [圖片上傳失敗...(image-5babc5-1559665370997)]
  • 這樣分析,上一段的代碼就得到了合理的解釋;
  • 再來看一下這段代碼;
function fn() {
    setTimeout(()=>{
        console.log('a');
    },0);
    new Promise((resolve)=>{
        console.log('b');
        resolve();
    }).then(()=>{
        console.log('c')
    });
}
fn() // b c a
Promise和async中的立即執(zhí)行

我們知道Promise中的異步體現(xiàn)在then和catch中,所以寫在Promise中的代碼是被當(dāng)做同步任務(wù)立即執(zhí)行的。而在async/await中,在出現(xiàn)await出現(xiàn)之前,其中的代碼也是立即執(zhí)行的。那么出現(xiàn)了await時(shí)候發(fā)生了什么呢?

await做了什么

從字面意思上看await就是等待,await 等待的是一個(gè)表達(dá)式,這個(gè)表達(dá)式的返回值可以是一個(gè)promise對象也可以是其他值。

很多人以為await會一直等待之后的表達(dá)式執(zhí)行完之后才會繼續(xù)執(zhí)行后面的代碼,實(shí)際上await是一個(gè)讓出線程的標(biāo)志。await后面的表達(dá)式會先執(zhí)行一遍,將await后面的代碼加入到microtask中,然后就會跳出整個(gè)async函數(shù)來執(zhí)行后面的代碼。
由于因?yàn)閍sync await 本身就是promise+generator的語法糖。所以await后面的代碼是microtask。所以對于開始面試題中的

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

等價(jià)于

async function async1() {
    console.log('async1 start');
    Promise.resolve(async2()).then(() => {
                console.log('async1 end');
        })
}

3. 宏任務(wù)和微任務(wù)

兩任務(wù)在同步異步中處于什么地位?
兩個(gè)任務(wù)分別處于任務(wù)隊(duì)列中的宏隊(duì)列微隊(duì)列中;
宏隊(duì)列微隊(duì)列組成了任務(wù)隊(duì)列;
任務(wù)隊(duì)列將任務(wù)放入執(zhí)行棧中執(zhí)行

宏任務(wù):

宏隊(duì)列,macrotask,也叫tasks。
異步任務(wù)的回調(diào)會依次進(jìn)入macro task queue,等待后續(xù)被調(diào)用,
這些異步任務(wù)包括:

  • setTimeout
  • setInterval
  • setImmediate (Node獨(dú)有)
  • requestAnimationFrame (瀏覽器獨(dú)有)
  • I/O
  • UI rendering (瀏覽器獨(dú)有)

微任務(wù):

微隊(duì)列,microtask,也叫jobs。
異步任務(wù)的回調(diào)會依次進(jìn)入micro task queue,等待后續(xù)被調(diào)用,
這些異步任務(wù)包括:

  • process.nextTick (Node獨(dú)有)
  • Promise
  • Object.observe
  • MutationObserver
圖片
  1. 執(zhí)行全局Script同步代碼,這些同步代碼有一些是同步語句,有一些是異步語句(比如setTimeout等);
  2. 全局Script代碼執(zhí)行完畢后,執(zhí)行棧Stack會清空;
  3. 微隊(duì)列中取出位于隊(duì)首的回調(diào)任務(wù),放入執(zhí)行棧Stack中執(zhí)行,執(zhí)行完后微隊(duì)列長度減1;
  4. 繼續(xù)循環(huán)取出位于微隊(duì)列的任務(wù),放入執(zhí)行棧Stack中執(zhí)行,以此類推,直到直到把微任務(wù)執(zhí)行完畢。注意,如果在執(zhí)行微任務(wù)的過程中,又產(chǎn)生了微任務(wù),那么會加入到微隊(duì)列的末尾,也會在這個(gè)周期被調(diào)用執(zhí)行;
  5. 微隊(duì)列中的所有微任務(wù)都執(zhí)行完畢,此時(shí)微隊(duì)列為空隊(duì)列,執(zhí)行棧Stack也為空;
  6. 取出宏隊(duì)列中的任務(wù),放入執(zhí)行棧Stack中執(zhí)行;
  7. 執(zhí)行完畢后,執(zhí)行棧Stack為空;
  8. 重復(fù)第3-7個(gè)步驟;

以上才是一個(gè)完整的事件循環(huán)

回到面試題

1.首先,事件循環(huán)從宏任務(wù)(macrotask)隊(duì)列開始,這個(gè)時(shí)候,宏任務(wù)隊(duì)列中,只有一個(gè)script(整體代碼)任務(wù);當(dāng)遇到任務(wù)源(task source)時(shí),則會先分發(fā)任務(wù)到對應(yīng)的任務(wù)隊(duì)列中去。

2.然后我們看到首先定義了兩個(gè)async函數(shù),接著往下看,然后遇到了 console 語句,直接輸出 script start。輸出之后,script 任務(wù)繼續(xù)往下執(zhí)行,遇到 setTimeout,其作為一個(gè)宏任務(wù)源,則會先將其任務(wù)分發(fā)到對應(yīng)的隊(duì)列中

3.script 任務(wù)繼續(xù)往下執(zhí)行,執(zhí)行了async1()函數(shù),前面講過async函數(shù)中在await之前的代碼是立即執(zhí)行的,所以會立即輸出async1 start。
遇到了await時(shí),會將await后面的表達(dá)式執(zhí)行一遍,所以就緊接著輸出async2,然后將await后面的代碼也就是console.log('async1 end')加入到microtask中的Promise隊(duì)列中,接著跳出async1函數(shù)來執(zhí)行后面的代碼

4.script任務(wù)繼續(xù)往下執(zhí)行,遇到Promise實(shí)例。由于Promise中的函數(shù)是立即執(zhí)行的,而后續(xù)的 .then 則會被分發(fā)到 microtask 的 Promise 隊(duì)列中去。所以會先輸出 promise1,然后執(zhí)行 resolve,將 promise2 分配到對應(yīng)隊(duì)列

5.script任務(wù)繼續(xù)往下執(zhí)行,最后只有一句輸出了 script end,至此,全局任務(wù)就執(zhí)行完畢了。
根據(jù)上述,每次執(zhí)行完一個(gè)宏任務(wù)之后,會去檢查是否存在 Microtasks;如果有,則執(zhí)行 Microtasks 直至清空 Microtask Queue。
因而在script任務(wù)執(zhí)行完畢之后,開始查找清空微任務(wù)隊(duì)列。此時(shí),微任務(wù)中, Promise 隊(duì)列有的兩個(gè)任務(wù)async1 end和promise2,因此按先后順序輸出 async1 end,promise2。當(dāng)所有的 Microtasks 執(zhí)行完畢之后,表示第一輪的循環(huán)就結(jié)束了

6.第二輪循環(huán)依舊從宏任務(wù)隊(duì)列開始。此時(shí)宏任務(wù)中只有一個(gè) setTimeout,取出直接輸出即可,至此整個(gè)流程結(jié)束

  • 來一個(gè)稍微復(fù)雜點(diǎn)的代碼
function fn(){
    console.log(1);
    
    setTimeout(() => {
        console.log(2);
        Promise.resolve().then(() => {
            console.log(3);
        });
    },0);
    
    new Promise((resolve, reject) => {
        console.log(4);
        resolve(5);
    }).then(data => {
        console.log(data);
    });
    
    setTimeout(() => {
        console.log(6);
    },0);
    
    console.log(7);
}
fn(); //

流程重現(xiàn)

  1. 執(zhí)行函數(shù)同步語句;
  • step1

    console.log(1); 
    

    執(zhí)行棧: [ console ]
    宏任務(wù): []
    微任務(wù): []

    打印結(jié)果:
    1

  • step2

    setTimeout(() => {
        // 這個(gè)回調(diào)函數(shù)叫做callback1,setTimeout屬于宏任務(wù),所以放到宏隊(duì)列中
        console.log(2);
        Promise.resolve().then(() => {
            console.log(3)
        });
    });
    

    執(zhí)行棧: [ setTimeout ]
    宏任務(wù): [ callback1 ]
    微任務(wù): []

    打印結(jié)果:
    1

  • step3

    new Promise((resolve, reject) => {
        // 注意,這里是同步執(zhí)行的
        console.log(4);
        resolve(5)
    }).then((data) => {
        // 這個(gè)回調(diào)函數(shù)叫做callback2,promise屬于微任務(wù),所以放到微隊(duì)列中
        console.log(data);
    });
    

    執(zhí)行棧: [ promise ]
    宏任務(wù): [ callback1 ]
    微任務(wù): [ callback2 ]

    打印結(jié)果:
    1
    4

  • step4

    setTimeout(() => {
        // 這個(gè)回調(diào)函數(shù)叫做callback3,setTimeout屬于宏任務(wù),所以放到宏隊(duì)列中
        console.log(6);
    })
    

    執(zhí)行棧: [ setTimeout ]
    宏任務(wù): [ callback1 , callback3 ]
    微任務(wù): [ callback2 ]

    打印結(jié)果:
    1
    4

  • step5

    console.log(7)
    

    執(zhí)行棧: [ console ]
    宏任務(wù): [ callback1 , callback3 ]
    微任務(wù): [ callback2 ]

    打印結(jié)果:
    1
    4
    7

  1. 同步語句執(zhí)行完畢,從微隊(duì)列中依次取出任務(wù)執(zhí)行,直到微隊(duì)列為空
  • step6

    console.log(data)       // 這里data是Promise的成功參數(shù)為5
    

    執(zhí)行棧: [ callback2 ]
    宏任務(wù): [ callback1 , callback3 ]
    微任務(wù): []

    打印結(jié)果:
    1
    4
    7
    5

  1. 這里微隊(duì)列中只有一個(gè)任務(wù),執(zhí)行完后開始從宏隊(duì)列中取任務(wù)執(zhí)行
  • step7

    console.log(2);
    

    執(zhí)行棧: [ callback1 ]
    宏任務(wù): [ callback3 ]
    微任務(wù): []

    打印結(jié)果:
    1
    4
    7
    5
    2

    但是執(zhí)行callback1的時(shí)候遇到另一個(gè)Promise,Promise異步執(zhí)行完畢以后在微隊(duì)列中又注冊了一個(gè)callback4函數(shù)

  • step8

    Promise.resolve().then(() => {
        // 這個(gè)回調(diào)函數(shù)叫做callback4,promise屬于微任務(wù),所以放到微隊(duì)列中
        console.log(3);
    });
    

    執(zhí)行棧: [ Promise ]
    宏任務(wù): [ callback3 ]
    微任務(wù): [ callback4 ]

    打印結(jié)果:
    1
    4
    7
    5
    2

  1. 取出一個(gè)宏任務(wù)macrotask執(zhí)行完畢,然后再去微任務(wù)隊(duì)列microtask queue中依次取出執(zhí)行
  • step9

    console.log(3)
    

    執(zhí)行棧: [ callback4 ]
    宏任務(wù): [ callback3 ]
    微任務(wù): []

    打印結(jié)果:
    1
    4
    7
    5
    2
    3

  1. 微隊(duì)列全部執(zhí)行完,再去宏隊(duì)列中取第一個(gè)任務(wù)執(zhí)行
  • step10

    console.log(3)
    

    執(zhí)行棧: [ callback3 ]
    宏任務(wù): []
    微任務(wù): []

    打印結(jié)果:
    1
    4
    7
    5
    2
    3
    6

  1. 以上全部執(zhí)行完畢,執(zhí)行棧,宏隊(duì)列微隊(duì)列均為空

    執(zhí)行棧: []
    宏任務(wù): []
    微任務(wù): []

    打印結(jié)果:
    1
    4
    7
    5
    2
    3
    6

  • 再來一段復(fù)雜代碼
function fn(){
    console.log(1);
    
    setTimeout(() => {
        console.log(2);
        Promise.resolve().then(() => {
            console.log(3)
        });
    });
    
    new Promise((resolve, reject) => {
        console.log(4)
        resolve(5)
    }).then((data) => {
        console.log(data);
        
        Promise.resolve().then(() => {
            console.log(6)
        }).then(() => {
            console.log(7)
            
            setTimeout(() => {
                console.log(8)
            }, 0);
        });
    })
    
    setTimeout(() => {
        console.log(9);
    })
    
    console.log(10);
}
fn();

4. NodeJS中的事件循環(huán)

圖片

NodeJS中的宏任務(wù)微任務(wù)

NodeJS的Event Loop中,執(zhí)行宏隊(duì)列的回調(diào)任務(wù)有6個(gè)階段,如下圖:

圖片

各個(gè)階段執(zhí)行的任務(wù)如下:

  • timers階段:這個(gè)階段執(zhí)行setTimeout和setInterval預(yù)定的callback
  • I/O callback階段:執(zhí)行除了close事件的callbacks、被timers設(shè)定的callbacks、setImmediate()設(shè)定的callbacks這些之外的callbacks
    idle, prepare階段:僅node內(nèi)部使用
  • poll階段:獲取新的I/O事件,適當(dāng)?shù)臈l件下node將阻塞在這里
  • check階段:執(zhí)行setImmediate()設(shè)定的callbacks
  • close callbacks階段:執(zhí)行socket.on('close', ....)這些callbacks

NodeJS的宏隊(duì)列:

  • Timers Queue
  • IO Callbacks Queue
  • Check Queue
  • Close Callbacks Queue

這4個(gè)都屬于宏隊(duì)列,但是在瀏覽器中,可以認(rèn)為只有一個(gè)宏隊(duì)列,所有的宏任務(wù)都會被加到這一個(gè)宏隊(duì)列中,但是在NodeJS中,不同的宏任務(wù)會被放置在不同的宏隊(duì)列

NodeJS的微隊(duì)列:

  • Next Tick Queue:是放置process.nextTick(callback)的回調(diào)任務(wù)的
  • Other Micro Queue:放置其他微任務(wù),比如Promise等

瀏覽器中,也可以認(rèn)為只有一個(gè)微隊(duì)列,所有的微任務(wù)都會被加到這一個(gè)微隊(duì)列中,但是在NodeJS中,不同的微任務(wù)會被放置在不同的微隊(duì)列

圖片

NodeJS中的事件循環(huán)過程

  1. 執(zhí)行全局的同步代碼;
  2. 執(zhí)行微任務(wù)先執(zhí)行next tick queue所有任務(wù),再執(zhí)行other micro tasks queue中的所有任務(wù);
  3. 開始執(zhí)行宏任務(wù),共6個(gè)階段,從第1個(gè)階段開始執(zhí)行相應(yīng)每一個(gè)階段宏隊(duì)列中的所有任務(wù),
    注意,這里是所有每個(gè)階段宏任務(wù)隊(duì)列的所有任務(wù),在瀏覽器的Event Loop中是只取宏隊(duì)列的第一個(gè)任務(wù)出來執(zhí)行,
    每一個(gè)階段的宏任務(wù)執(zhí)行完畢后,開始執(zhí)行微任務(wù),回到步驟2;

Timers Queue -> 步驟2 ->
I/O Queue -> 步驟2 ->
Check Queue -> 步驟2 ->
Close Callback Queue -> 步驟2 ->
Timers Queue

  • 再看兩張圖
    圖片

圖片
  • 代碼又來了
function fn(){

    console.log('start');

    setTimeout(() => {              // callback1
        console.log(111);
        
        setTimeout(() => {          // callback2
            console.log(222);
        }, 0);
        
        setImmediate(() => {        // callback3
            console.log(333);
        });
        
        process.nextTick(() => {    // callback4
            console.log(444);     
        });
        
    }, 0);
    
    setImmediate(() => {            // callback5
        console.log(555);
        
        process.nextTick(() => {    // callback6
           console.log(666); 
        });
    });
    
    setTimeout(() => {              // callback7
        console.log(777);
        
        process.nextTick(() => {    // callback8
            console.log(888);
        });
    }, 0);
    
    process.nextTick(() => {        // callback9
        console.log(999);
    });
    
    console.log('end');
}
fn();   

//  before version 11.0.0  start end 999 111 777 444 888 555 333 666 222
//  after  version 11.0.0  start end 999 111 444 777 888 555 666 333 222

PS:版本不同導(dǎo)致運(yùn)行結(jié)果不同

總結(jié):

  • 瀏覽器的Event Loop和NodeJS的Event Loop是不同的,實(shí)現(xiàn)機(jī)制也不一樣,不要混為一談。
  • NodeJS可以理解成有4個(gè)宏任務(wù)隊(duì)列和2個(gè)微任務(wù)隊(duì)列,但是執(zhí)行宏任務(wù)時(shí)有6個(gè)階段。先執(zhí)行全局Script代碼,執(zhí)行完同步代碼調(diào)用棧清空后,先從微任務(wù)隊(duì)列Next Tick Queue中依次取出所有的任務(wù)放入調(diào)用棧中執(zhí)行,再從微任務(wù)隊(duì)列Other Microtask Queue中依次取出所有的任務(wù)放入調(diào)用棧中執(zhí)行。然后開始宏任務(wù)的6個(gè)階段,每個(gè)階段都將該宏任務(wù)隊(duì)列中的所有任務(wù)都取出來執(zhí)行(注意,這里和瀏覽器不一樣,瀏覽器只取一個(gè)),每個(gè)宏任務(wù)階段執(zhí)行完畢后,開始執(zhí)行微任務(wù),再開始執(zhí)行下一階段宏任務(wù),以此構(gòu)成事件循環(huán)。
  • MacroTask包括: setTimeout、setInterval、 setImmediate(Node)、requestAnimation(瀏覽器)、IO、UI rendering。
  • Microtask包括: process.nextTick(Node)、Promise、Object.observe、MutationObserver。
  • v11以前 是上面說的那樣;v11以后將Node環(huán)境的事件循環(huán)和瀏覽器的統(tǒng)一了。
  • process.nextTick 上限是1000?
  • 寫一個(gè)休眠函數(shù) 達(dá)到阻塞目的

附圖

瀏覽器中的EventLoop

d1ca0d6b13501044a5f74c99becbcd3d4043.gif

NodeJS中的EventLoop (v11以前,v11以后和瀏覽器一致)


963090bd3b681de3313b4466b234f4f02474.gif

歲月不饒人,我們亦未曾繞過歲月。

贈人玫瑰,手有余香。感謝點(diǎn)贊的你。


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

相關(guān)閱讀更多精彩內(nèi)容

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