切換語言為:簡體

透過例項理解 JavaScript 事件迴圈機制(Event Loop)

  • 爱糖宝
  • 2024-06-14
  • 2198
  • 0
  • 0

前言

相信大家都聽過一句話,js是單執行緒的語言,那麼這句話如何理解呢?今天就來討論一下js中的事件迴圈,理解完事件迴圈機制,你就會對這句話有更深刻的理解。

事件迴圈(event loop)

同步與非同步程式碼

let a = 1
console.log(a);

setTimeout(function () {
    a++
}, 1000)

console.log(a);


問:a列印多少? 答:1,1。因為定時器是一個耗時的程式碼,同步程式碼是不需要耗時執行的,非同步程式碼是需要耗時執行的,JSV8引擎會先執行同步程式碼再執行非同步程式碼

到這,你已經瞭解了什麼是同步程式碼什麼是非同步程式碼。

  • 同步程式碼:不需要耗時執行的程式碼。

  • 非同步程式碼:需要耗時執行的程式碼。

  • JSV8引擎會先執行同步程式碼再執行非同步程式碼。

程序與執行緒

大家大學階段應該都學習過一門課程叫做《作業系統》,這門課程對程序與執行緒都有深刻的講解。

  • 程序:是作業系統中程式的一次執行過程,是資源分配的基本單位。它擁有獨立的地址空間、記憶體、檔案等資源。

  • 執行緒:是程序中的一個執行單元,是作業系統能夠進行運算排程的最小單位。執行緒共享所屬程序的資源,但也有自己少量的私有資源,如程式計數器、棧等。

  • 多個執行緒可以在一個程序內併發執行,共同完成程序的任務,提高系統的併發性和效率。

    • 例如:一個瀏覽器的tab頁面,就會有渲染執行緒、js執行緒、http執行緒,這些執行緒通力合作,共同完成程序任務。

    • 做一個假設,我們有一份html程式碼,有一個p標籤hello,但是在p標籤之前引入了一份js程式碼。這個hello想要展示出來一定是會等到js執行完了才輪到它執行,例如我們自己寫了一份js程式碼,這份js程式碼中用到了引用的js程式碼,如果不是前面的js程式碼先載入完的話,那我們這一份js程式碼就報錯了。因此:js的載入會阻塞頁面的渲染,即:在我們的例子中,渲染執行緒與js執行緒是互斥的(不能同時工作),這就從側面說明了一個問題:js是單執行緒的

js的單執行緒

jd的v8引擎在執行js的過程中,只有一個執行緒會工作。

let a = 1
console.log(a);

setTimeout(function () {
    a++
}, 1000)

console.log(a);


還是剛纔的例子,為什麼js中會有同步程式碼和非同步程式碼,如果第二行程式碼列印完了之後,到了第四行,第四行如果要你等一天時間,才能執行後面的程式碼,就不完蛋了嗎,因為js騰不出手,所以纔有了同步與非同步這個機制,先把非同步的比如這個定時器給他掛起來,騰出手來先去執行下面的同步程式碼列印a,然後全部執行完了,再執行非同步,這樣效率就提高了許多。

不論一個語言是單執行緒的還是多執行緒的,都各有各自的優勢,例如多執行緒的語言,毋庸置疑,執行效率高。 那麼單執行緒語言有什麼優勢呢?

js單執行緒的優勢

js打造之初就是想作為一個瀏覽器的指令碼語言,因此如果它佔用使用者過多的效能,這門語言會被嫌棄。

  1. 節約效能:不需要過多考慮多執行緒環境下資源競爭、死鎖等複雜情況,程式碼邏輯相對簡單清晰,降低了開發難度和出錯機率。

  2. 節約上下文切換的時間:例如有一個迴圈語句要執行1s,然後對a變數做一個操作,一個定時器也要執行1s,也對a變數做一個操作,多執行緒語言就會對其中一個上鎖,先執行完另一個然後切換回這個解鎖。這就存在上下文切換的耗時。

  3. 便於與瀏覽器互動:能更好地與瀏覽器的單執行緒模型相契合,確保頁面渲染和使用者互動的穩定性,不會因為多執行緒競爭而導致頁面顯示異常等問題。

微任務與宏任務

let count = 0;

function a(){
    setTimeout(()=>{
        count++;
    },1000)
}

function b(){
    console.log(count);
}

a();
b();


以上面程式碼為例子,還是一樣,執行到13行,呼叫a,發現a是一個非同步程式碼,因此先掛起了,執行b的呼叫,然後發現count還是0,因此列印0,然後過1s執行count++。倘若我們這個地方寫的不是一個定時器隔1s執行,而是一個http請求,但是這個請求耗時是說不準的,機器效能好,速度就快,效能差就慢。因此就會存在一些場景,你認為這個程式碼是耗時的但是它又不耗時,你說它耗時,但是它又幾乎不耗時。因此僅僅有非同步程式碼這個概念就沒辦法解決所有的應用場景了,那麼官方就在非同步下又區分了一個微任務與宏任務。

  • 微任務與宏任務,都是非同步程式碼

  • 微任務:promise.then(),proces.nextTick(),mutationObserver()

  • 宏任務:script,setTimeout,setInterval,setImmediate,I/O,UI rendering

事件迴圈機制

  1. 執行同步程式碼(這屬於是宏任務)

  2. 同步執行完畢後,檢查是否有非同步需要執行

  3. 如果有,則執行微任務

  4. 微任務執行完畢後,如果有需要就會渲染頁面

  5. 執行非同步宏任務,也是開啟下一次事件迴圈(因為宏任務中,也一樣會有同步程式碼、非同步程式碼...)

面試題實戰1
console.log(1);
new Promise((resolve, reject) => {
    console.log(2);
    resolve()
})
    .then(() => {
        console.log(3);
    })
    .then(() => {
        console.log(4);
    })
setTimeout(function () {
    console.log(5);
})
console.log(6);
// 1 2 6 3 4 5(宏裡面也有周期,同步->微任務->渲染->宏)


分析過程:

  1. 第一行程式碼,執行列印1(同步程式碼)

  2. 第二行程式碼promise的呼叫,同步程式碼,列印2

  3. 第六行程式碼是非同步程式碼裡面的微任務,因此,加入微任務佇列掛起(then1)。

  4. 第九行程式碼是非同步程式碼裡面的微任務,因此,加入微任務佇列掛起(then2)。

  5. 第十二行程式碼是非同步程式碼裡的宏任務,因此,加入宏任務佇列掛起(set1)。

  6. 第十五行程式碼是同步任務,列印6。

  7. 至此,第一次事件迴圈機制裡的第一步,同步任務全部執行完畢,開始尋找是否有非同步程式碼需要執行

  8. 如果有,執行微任務,因此then1出佇列,也就是列印3,然後then2出佇列,列印4。

  9. 微任務執行完畢後,沒有發現渲染操作,因此接下來執行非同步中的宏任務,也是開啟了下一次事件迴圈,set1出佇列,執行列印5.

  10. 最終結果:1,2,6,3,4,5。

透過例項理解 JavaScript 事件迴圈機制(Event Loop)

面試題實戰2
console.log(1);
new Promise((resolve, reject) => {
    console.log(2);
    resolve()
})
    .then(() => {
        console.log(3);
        setTimeout(() => {
            console.log(4);
        }, 0)
    })
setTimeout(() => {
    console.log(5);
    setTimeout(() => {
        console.log(6);
    }, 0)
}, 0)
console.log(7);


分析過程:

  1. 第一行執行,同步程式碼,列印1

  2. new了一個Promise,建構函式本身也不是非同步,同步程式碼,執行列印2

  3. 呼叫resolve,.then就能執行了,發現是一個非同步的微任務,因此入微任務佇列掛起then1(列印3掛起)

  4. 到達十二行,發現是一個定時器,非同步裡面的宏任務,入宏任務佇列set1

  5. 18行同步程式碼,列印7

  6. 至此,第一次事件迴圈的第一步同步程式碼執行結束

  7. 執行微任務,then1出佇列,then1中有同步和非同步,同步先執行,因此列印3,然後發現有個定時器,因此set2入宏任務佇列。微任務執行結束

  8. 開始執行宏任務,set1出佇列,宏任務開啟一次新的事件迴圈,同步任務先執行,列印5,然後第二次事件迴圈發現了一個定時器宏任務,set3入宏任務佇列,第二次事件迴圈的同步結束,然後去找微任務佇列,發現微任務佇列是空的,緊接著去宏任務佇列找宏任務,開啟第三次事件迴圈,set2出佇列,因此列印4,這也意味著第二次事件迴圈宏任務結束,第二次事件迴圈結束,列印4即是第二次時間迴圈的結束也是第三次事件迴圈的開始,緊接著去微任務佇列找,發現沒有,然後去宏任務佇列找,set3出佇列,列印6。

  9. 因此結果:1273546

面試題3實戰
console.log('script start');

async function async1() {
    await async2()
    console.log('async1 end');
}
async function async2() {
    console.log('async2 end');
}
async1()
setTimeout(function () {
    console.log('setTimeout');
}, 0)
new Promise(function (resolve, reject) {
    console.log('promise');
    resolve()
})
    .then(() => {
        console.log('then1');
    })
    .then(() => {
        console.log('then2');
    })
console.log('script end');


分析過程:注意:await會將後續程式碼阻塞進微任務佇列

  1. 第一行同步程式碼執行 列印script start

  2. 第10行帶了了async1的呼叫,async2前面有await,因此async2呼叫,async2裡面是同步,列印async2 end,然後輪到第五行程式碼執行,但是由於await將這行列印放到了微任務佇列,因此async1 end入微任務佇列

  3. 到達第十一行,是一個定時器,宏任務進宏任務佇列set

  4. 達到第十四行,同步任務列印promise

  5. 到達第十八行,進微任務佇列then1

  6. 達到21行,進微任務佇列then2(目前微任務佇列有3個,一個async1 end,then1,then2)

  7. 24行同步任務,列印script end

  8. 至此同步程式碼全部執行完畢,開始執行微任務,async1 end,then1,then2出佇列,列印async1 end,then1,then2

  9. 微任務執行結束,執行宏任務,set出佇列,列印setTimeout

小結

如果這三份面試實戰,你都能清楚明白什麼時候同步程式碼執行,什麼時候入微任務佇列什麼時候入宏任務佇列,什麼時候出微任務佇列,什麼時候出紅任務佇列,那麼事件迴圈機制就徹底搞明白了,明白了事件迴圈機制,你也就能夠更加理解js底層V8是如何執行的。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.