這個前端學習筆記是學習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
由上分析得知,每次主線程執行棧爲空的時候,引擎會優先處理微任務隊列,處理完微任務隊列裏的所有任務,再去處理宏任務。