1. 程式人生 > >JS執行機制詳解

JS執行機制詳解

你會 成了 分類 event col 這也 obs play 改變

通過結果倒推過程是我們常用的思考模式,我在上一篇學習promise筆記中,有少量關於promise執行順序的例子,通過倒推,我成功讓自己對於js執行機制的理解一塌糊塗,js事件機制,事件循環是面試常考的點,弄懂它們是賊有必要的。

回顧下我學習promise的心理歷程:

let p = Promise.resolve(1);
p.then(resp => console.log(resp));
console.log(2);
//2
//1

哦,原來如此,同步代碼會先執行,先輸出1,所以then回調是異步。

let p1 = Promise.resolve(1);
p1.then(resp 
=> console.log(resp)); let p2 = Promise.resolve(2); p2.then(resp => console.log(resp)); //1 //2

哦!多個異步,先註冊的回調先執行,原來如此。

setTimeout(() => console.log(2),0);
let p1 = Promise.resolve(1);
p1.then(resp => console.log(resp));
//1
//2

嗯????不是先註冊的異步先執行?為啥這裏先輸出1,promise學習下來,成功讓自己懵逼。

技術分享圖片

理解JS執行機制是很重要的,它會讓你的代碼調試更符合自己的預期,其次對於面試也非常有幫助。

介紹js執行機制的文章挺多了,這裏只是做個個人思路的整理,那麽開始。

一、JavaScript中的同步異步

JavaScript是一門單線程非阻塞語言,在同一時間只能專心做一件事,如果前面的事情沒做,後面的事情就得耐心的等著,這就是所謂的同步。

你會想,為什麽要同步?

JavaScript本身是一門瀏覽器腳本語言,更多負責用戶的交互,dom操作之類;假設JS並非單線程,我讓兩個行為同時操作一個dom對象,那豈不是亂套了。想想我們排隊取餐吃飯,如果不排隊,往往容易引發爭吵,編程也是現實行為的抽象。

也許你會說,不是有web worker嗎,但web worker屬於瀏覽器的解決方法,並非JavaScript;瀏覽器雖然可以開多個線程,但每個線程仍然是單線程,而且也不被允許操作dom,這依舊沒改變JS是單線程語言的事實。

let funA = () => {
    let NUM = 10000;
    while (NUM) {
        NUM--;
    };
    console.log(1);
};

let funB = () => console.log(2);

funA(); //1
funB(); //2

在上述代碼中,讓10000進行自減如果讓我們腦補這個過程是很費時的,但是對於強大的js引擎來說並不是事,也要了太多時間;

可是偏偏存在xhr一類網絡請求,發起請求網絡可能存在延遲,服務器查數據也不知道要多久,反饋結果可能受多個不確定因素影響,那可不成啊,我後面的程序不可能就這麽一直等著。

於是異步誕生了,對於不確定的網絡請求,定時器之類,你不是耗時嗎,那咱先備註不急著處理,就接著去忙同步的事情了,等手頭上同步忙完了,再來處理先前備註的異步事件。

想想我們排隊取餐吃飯,前面的哥們大聲說道,牛肉面不要面只要牛肉,多蔥多蒜少辣不吃香菜半小時後來取,老板也不會等他半小時把面取了再做後面顧客的生意,那真要這樣,店子早倒閉了。

技術分享圖片

那麽說完同步異步,我們大概有了個抽象的概念,js會先執行同步,萬一遇到異步,就先備註下有這個異步,等同步跑完了,咱再來處理異步的後續操作,那麽站在js角度這個過程是什麽樣的,我們接著說。

二、執行棧與任務隊列

我們都知道,當一個方法被調用時,JavaScript會生成一個屬於此方法的執行環境,也叫執行上下文,這個上下文中存放著方法依賴的參數,變量以及作用域等等,怎麽理解這個執行上下文呢,舉個例子:

情景一:媽媽去水果店買了很多蘋果。
我最愛吃這種水果了

情景二:媽媽去水果店買了很多橘子。
我最愛吃這種水果了

那麽這種水果是?

同樣一句話放在不同情境下表達的意思不同,同一個方法放在不同執行環境下執行,結果也可能不同,差不多這麽個意思。

技術分享圖片

什麽是執行棧呢?當調用一個方法A時,這個方法可能也會調用另一個方法B,B還可能調用方法C,而JS只能同時一件事,所以BC沒執行完之前,方法A也不能被釋放,那總得找個地方把這些方法按順序存一存吧,存放的地方就是執行棧。

執行棧是存放同步方法調用的地方,遵從先進後出的規則:

let A = () => {
    B()
    console.log(1);
};
let B = () => {
    C()
    console.log(2);
};
let C = () => {
    console.log(3);
};
A();//3 2 1

上述代碼站在執行機制角度來看,是這樣的,你應該也能理解遞歸處理不好陷入死循環後爆棧是個什麽情況了:

技術分享圖片

憑直覺來想,異步任務不可能直接在執行棧中執行,不然絕對存在堵塞的問題,那先存放在哪呢?當然是任務隊列了。

那麽到這裏我們又有了一個模糊的概念,同步任務與異步任務存放的地方不同,有個問題,JavaScript怎麽知道什麽時候去執行異步任務呢?那就不得不說事件循環。

三、事件循環 (Event Loop)

技術分享圖片

圖片來源

當一個任務被執行,js會判斷是否為同步任務,如果是同步,壓入主線程立即執行;但如果是異步任務,請移步異步處理模塊(Task Table),當異步任務有了結果,就將異步任務的回調函數註入到任務隊列中等待。

當主線程的同步任務執行完畢,此時執行棧為空,js引擎就會讀取任務隊列中的第一個任務加入到執行棧執行,當此任務完成,繼續重復此類操作,這也就是事件循環了。

那麽到這裏,我們知道js引擎會利用事情循環機制來處理同步異步問題;那麽問題又來了,還記得文章開頭第三個例子嗎,定時器和promise都是異步,為什麽後面的promise反而比前面的定時器先執行,難道異步任務也有自己的先後順序?這裏就得引出宏任務與微任務了。

四、宏任務與微任務

我們先對宏任務異步任務做個大概分類:

macro-task(宏任務):setTimeout、setInterval、I/O、事件、postMessage、 MessageChannel、setImmediate (Node.js)

micro-task(微任務):Promise,process.nextTick,MutaionObserver

很多面孔沒見過,沒關系,好歹我們知道了定時器是宏任務,new Promise是微任務。我把上面的例子搬下來:

setTimeout(() => console.log(‘我第一‘), 1000);
let p1 = Promise.resolve(‘我第二‘);
p1.then(resp => console.log(resp));
//我第二
//我第一

明明是定時器先進的異步處理模塊,結果promise.then還要早於定時器先執行,為什麽呢?

這是因為,異步任務又分為宏任務與微任務兩種,當執行棧為空,JS引擎會優先處理微任務隊列的任務,等到微任務隊列處理完成,才會處理宏任務隊列的任務。

setTimeout(() => console.log(‘我第一‘), 2000);
let p1 = Promise.resolve(‘我第二‘);
p1.then(resp => console.log(resp));
setTimeout(() => console.log(‘我第三‘), 1000);
let p2 = Promise.resolve(‘我第四‘);
p2.then(resp => console.log(resp));
//我第二
//我第四
//我第三
//我第一

上述代碼中,不管你異步是怎麽個執行順序,最終在執行棧中,總是先處理微任務,最後處理宏任務。

那麽我在這裏說,對於任務隊列,是先進先出的順序,你肯定要噴我了,睜眼說瞎話,要是先進先出,怎麽等待2000ms的定時器比等待1000ms的定時器晚執行?那這裏就得聊聊定時器時間的具體意義了。

五、有趣的定時器

定期器分為一次性定時器setTimeout與周期性定時器setInterval,前者是等待N秒之後執行回調一次沒了,後者是每隔N秒執行回調一次。

有這麽一個定時器:

setTimeout(() => console.log(‘我第一‘), 3000);

站在宏觀思想上理解,這行代碼的意思是這個定時器將在三秒後觸發,但站在微觀的角度上,3000ms並不代表執行時間,而是將回調函數加入任務隊列的時間,這也是為何存在定時器執行與所設置等待時間不符的問題所在。

setTimeout(() => console.log(‘我第一‘), 3000);
setTimeout(() => console.log(‘我第二‘), 3000);

你猜這兩個定時器怎麽執行?先等三秒打印“我第一”,再等三秒打印“我第二”嗎?其實不是,真正執行是是等待三秒後幾乎無間隔的同時打印2個結果。

我們可以腦補下執行順序,首先遇到第一個定時器,告訴異步處理模塊,等待三秒後將回調加入任務隊列,然後又調用了第二個定時器,同樣是3秒後將回調加入任務隊列。

等到執行棧為空,去任務隊列拿任務,執行第一個console,這要不了多久,於是幾乎無時差的又去任務隊列拿第二個任務,這也導致了為什麽2次輸出幾乎在同時進行。

兩個定時器等待時間相同,但第一個定時器回調還是先進入任務隊列,所以先觸發,這也印證了任務隊列先進先出的規則。

所以當我們使用周期定時器setInterval時,也會遇到執行間隔與所設時間不符的情況,比如前面有個賊復雜的操作,導致周期定時器按時間不停給任務隊列加入回調,等到前面任務跑完,這時你會發現前面所積累的回調像憋久了一樣一下全部一起執行了。

那麽到這裏這篇文章大概記錄完成了。

參考資料:

最後一次搞懂 Event Loop

這一次,徹底弄懂 JavaScript 執行機制

詳解JavaScript中的Event Loop(事件循環)機制

JS執行機制詳解