前端學習筆記——異步下

這個前端學習筆記是學習gitchat上的一個課程,這個課程的質量非常好,價格也不貴,非常時候前端入門的小夥伴們進階。
在這裏插入圖片描述
筆記不會涉及很多,主要是提取一些知識點,詳細的大家最好去過一遍教程,相信你一定會有很大的收穫


在秋招的時候,經常會遇到問異步輸出順序,這裏我整理了一些題目來認識下 JavaScript 和瀏覽器引擎交織的異步行爲,包括 eventloop、宏任務、微任務等。

setTimeout

setTimeout(() => {
    console.log('setTimeout block')
}, 100)

while (true) {

}

console.log('end here')

將不會有任何輸出。

原因很簡單,因爲 while 循環會一直循環代碼塊,因此主線程將會被佔用。

setTimeout(() => {
    while (true) {

    }
}, 0)

console.log('end here')

會打印出:end here。這段代碼執行後,如果我們再執行任何語句,都不會再得到響應。

由此可以延伸出:JavaScript 中所有任務分爲同步任務和異步任務

  • 同步任務是指:當前主線程將要消化執行的任務,這些任務一起形成執行棧(execution context stack)
  • 異步任務是指:不進入主線程,而是進入任務隊列(task queue),即不會馬上進行的任務。

setTimeout是一個異步任務,裏面的函數會放置在異步任務去,而不會阻塞線程。

主線程執行完之後,纔會執行異步任務。

計時器真的守時嗎?

如果稍做更改:

const t1 = new Date()
setTimeout(() => {
    const t3 = new Date()
    console.log('setTimeout block')
    console.log('t3 - t1 =', t3 - t1)
}, 100)


let t2 = new Date()

while (t2 - t1 < 200) {// 0.2秒後停止死循環
    t2 = new Date()
}

console.log('end here')
// end here
// setTimeout block
// t3 - t1 = 200

可以看出,雖然定時器設置的是0.1秒後執行,但是由於主線程的阻塞導致計時器不能及時地執行

多個計時器的情況

setTimeout(() => {
    console.log('here 100')
}, 100)

setTimeout(() => {
    console.log('here 2')
}, 0)

這個如我們所願,輸出情況如下

//here 2
//herr 100

但是瀏覽器並不是那麼地敏感

遇到下面的情況就出現不一樣的結果

setTimeout(() => {
    console.log('here 100')
}, 2)

setTimeout(() => {
    console.log('here 2')
}, 0)
//here 100
//herr 2

事實上針對這兩個setTimeout,誰先進入任務隊列,誰先執行並不會嚴格按照 1 毫秒和 0 毫秒的區分。

瀏覽器和人一樣,有一個最小延遲時間的極限,最小延遲時間是 1 毫秒,在 1 毫秒以內的定時,都以最小延遲時間處理。此時,在代碼順序上誰靠前,誰就先會在主線程空閒時優先被執行。

值得一提的是,MDN 上給出的最小延時概念是 4 毫秒。

異步任務的區別

異步任務其實還有細分,有微任務和宏任務的區別。這2個任務分別在不同的任務隊列。

異步任務的執行過程:

主線程所有任務執行完之後,判斷微任務是否有異步任務,如果有則添加到主線程執行,執行完之後繼續判斷微任務隊列,如果沒有則判斷宏任務是否有異步任務,如果有則添加到主線程,執行結束後判斷微任務……一直循環。

微任務的事件:

  • Promise.then
  • MutationObserver
  • process.nextTick (Node.js)

宏任務的事件:

  • setTimeout
  • setInterval
  • I/O
  • 事件
  • postMessage(跨域)
  • requestAnimationFrame
  • UI 渲染

我們從例子來看看微任務和宏任務的執行順序:

console.log('start here')

const foo = () => (new Promise((resolve, reject) => {
    console.log('first promise constructor')

    let promise1 = new Promise((resolve, reject) => {
        console.log('second promise constructor')

        setTimeout(() => {
            console.log('setTimeout here')
            resolve()
        }, 0)

        resolve('promise1')
    })

    resolve('promise0')

    promise1.then(arg => {
        console.log(arg)
    })
}))

foo().then(arg => {
    console.log(arg)
})

console.log('end here')
// start here
// first promise constructor
// second promise constructor
// end here
// promise1
// promise0
// setTimeout here
  • 首先輸出同步內容:start here,執行 foo 函數,同步輸出 first promise constructor,
  • 繼續執行 foo 函數,遇見 promise1,執行 promise1 構造函數,同步輸出 second promise constructor,以及 end here。同時按照順序:setTimeout 回調進入任務隊列(宏任務),promise1 的完成處理函數(第 18 行)進入任務隊列(微任務),第一個(匿名) promise 的完成處理函數(第 23 行)進入任務隊列(微任務)
  • 雖然 setTimeout 回調率先進入任務隊列,但是優先執行微任務,按照微任務順序,先輸出 promise1(promise1 結果),再輸出 promise0(第一個匿名 promise 結果)
  • 此時所有微任務都處理完畢,執行宏任務,輸出 setTimeout 回調內容 setTimeout here

由上分析得知,每次主線程執行棧爲空的時候,引擎會優先處理微任務隊列,處理完微任務隊列裏的所有任務,再去處理宏任務。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章