JS執行機制詳解,定時器時間間隔的真正含義

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

回顧下我學習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

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

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

嗯????不是先註冊的異步先執行?爲啥這裏先輸出1,promise學習下來,成功讓本身懵逼。瀏覽器

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

介紹js執行機制的文章挺多了,這裏只是作個我的思路的整理,那麼開始。網絡

1、JavaScript中的同步異步dom

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角度這個過程是什麼樣的,咱們接着說。

 2、執行棧與任務隊列

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

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

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

那麼這種水果是?

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

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

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

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

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

 

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

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

 3、事件循環 (Event Loop)

 

圖片來源

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

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

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

4、宏任務與微任務

咱們先對宏任務微任務作個大概分類:

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的定時器晚執行?那這裏就得聊聊定時器時間的具體意義了。

5、有趣的定時器

按期器分爲一次性定時器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(事件循環)機制

相關文章
相關標籤/搜索