JS 事件循環 event loop,前端燒腦一刻

本文不保證能說明透徹,由於它原本就存在着混亂,但力求講到點子上。html

比較下面這幾個的執行順序node

  • setTimeout
  • setInterval
  • setImmediate (nodejs 支持,瀏覽器不適合使用
  • new Promise(cb)promise.then(cb)(promisePromiose 的實例)
  • process.nextTick(nodejs)

還有一些另外的(暫時不考慮)ios

分類

同步執行

new Promise(cb) cb 代碼會同步執行,其實不屬於考慮範疇了git

microTask 微任務

  • process.nextTick tickTask
  • promise.then(cb) microTask

microTask (在 nodejs)能夠進一步劃分爲 tickTask 和 microTask,都是微任務隊列,同類型的微任務先進先執行github

優先級是 tickTask > microTask算法

macroTask 宏任務

  • setTimeout
  • setInterval 優先級同 setTimeout,誰先被推到 timers 隊列誰就先執行
  • setImmediate

在不一樣類型宏任務切換的間隙,一旦微任務隊列有任務則會把微任務隊列先執行完,而後繼續執行下一個類型的宏任務隊列。(注意是切換的時候,若是已經進入執行階段是讓該類型的宏任務執行完而後檢查微任務隊列,若是宏任務執行時又加入了同類型的宏任務,則會在下一個循環裏面執行) 在同一個事件循環裏面,microTask 永遠比 macroTask 中的任務先執行,並且 microTask 在本次循環所有執行完。api

進入 timers 或者 check 或者其餘的宏任務隊列時,若是 microTask 任務隊列中沒有任務,則會在執行完優先執行的宏任務隊列以後再檢查 microTask 任務隊列(注意:若是某個 macroTask 執行時推入了新的 microTask,它依然會先把這個類型的宏任務隊列執行完,而後切換下一個類型宏任務隊列時先執行微任務),若是有則執行完 microTask,而後進入下一個類型的 macroTask 隊列(這個過程可能會比較陌生,能夠經過測試獲得結果) 一次只會從 macroTask 隊列裏面拿出一個執行,執行完就執行 microTask 去了。promise

這裏有一個非肯定狀況,setImmediatesetTimeout 的執行順序在 nodejs 中不是固定的(nodejs 開發者這麼說)。瀏覽器

For example, if we run the following script which is not within an I/O cycle (i.e. the main module), the order in which the two timers are executed is non-deterministic, as it is bound by the performance of the processbash

作個測試,下面的執行是不肯定的,沒有實際的意義

setTimeout(() => {
  console.log('1 setTimeout')
  setTimeout(() => {
    console.log('2 setTimeout')
  }, 0)
  setImmediate(() => {
    console.log('3 setImmediate')
  })
}, 0)
setImmediate(() => {
  console.log('4 setImmediate')
})
複製代碼

nodejs

可能
4 setImmediate
1 setTimeout
3 setImmediate
2 setTimeout

也可能
1 setTimeout
4 setImmediate
3 setImmediate
2 setTimeout
複製代碼

setTimeout 有一個隱形前提,它的第二個參數,也就是延遲執行的時間,最小是 4ms,即便指定的 0,另外注意它是 n ms 以後纔可能執行,並非 n ms 時就會執行,它的執行時間是不肯定的,只能知道在 n ms 以前它不會執行。

setInterval 回調內若是是一個 while 循環,即便時間設定的 0,它也不會推無限多個回調到 timers 隊列中,而是要等到這個執行完,纔會把下一個回調推入 timers 隊列,用 while 執行 2S 以後,清除掉 interval,發現回調只執行了一次,而不是執行不少次。

看下面的例子,這說明 setInterval 會推一個回調到 timers 隊列,而後執行,而後再推下一個回調。

let count = 0
setTimeout(() => {
  console.log('1 setTimeout')
}, 0)
const i = setInterval(() => {
  console.log('2 setInterval')
  count++
  if (count === 5) {
    clearInterval(i)
  }
  setTimeout(() => {
    console.log('3 setTimeout ', count)
  }, 0)
}, 0)
setTimeout(() => {
  console.log('4 setTimeout')
}, 0)

---

1 setTimeout
2 setInterval
4 setTimeout
3 setTimeout  1
2 setInterval
3 setTimeout  2
2 setInterval
3 setTimeout  3
2 setInterval
3 setTimeout  4
2 setInterval
3 setTimeout  5
複製代碼

nodejs 事件循環

┌───────────────────────────┐
┌─>│           timers          │ setTimeout setInterval setImmediate
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │ I/O 除了另外幾種以外的幾乎全部回調
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │ 內部使用(忽略)
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │ setImmediate
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │ 相似 socket.on('close', ...) 的 close 回調
   └───────────────────────────┘
複製代碼

上述基本都是 macroTask 宏任務

setTimeout(() => {
  console.log('1 setTimeout')
  setTimeout(() => {
    console.log('2 setTimeout')
  }, 0)
  setImmediate(() => {
    console.log('3 setImmediate')
  })
  setTimeout(() => {
    console.log('4 setTimeout')
  }, 0)
  setImmediate(() => {
    console.log('5 setImmediate')
  })
}, 0)
複製代碼

這個結果說明執行過程是整個隊列執行完再執行下一個隊列下面結果說明在同一事件循環內, check 隊列會執行完以後再去執行 timers 隊列。(check 未必比 timers 快)

1 setTimeout
3 setImmediate
5 setImmediate
2 setTimeout
4 setTimeout
複製代碼

若是在 check 隊列執行期間推入 microTask 任務,那就先讓當前 check 隊列執行完,而後再執行 microTask,再執行 timers 隊列。

setTimeout(() => {
  console.log('1 setTimeout')
  setTimeout(() => {
    console.log('2 setTimeout')
  }, 0)
  setImmediate(() => {
    console.log('3 setImmediate')
    new Promise(res => res()).then(() => {
      console.log('4 promise')
    })
    process.nextTick(() => {
      console.log('5 nextTick')
    })
  })
  setTimeout(() => {
    console.log('6 setTimeout')
  }, 0)
  setImmediate(() => {
    console.log('7 setImmediate')
  })
}, 0)
複製代碼

執行結果,兩個 setImmediate 被放到 check 隊列,check 隊列中的 setImmediate 要先所有執行完,而後再下一步,而下一步過程當中 microTask 就會執行。

1 setTimeout
3 setImmediate
7 setImmediate
5 nextTick
4 promise
2 setTimeout
6 setTimeout
複製代碼

注意下面代碼和上面代碼的區別

setTimeout(() => {
  console.log('1 setTimeout')
  setTimeout(() => {
    console.log('2 setTimeout')
  }, 0)
  setImmediate(() => {
    console.log('3 setImmediate')
  })
  new Promise(res => res()).then(() => {
    console.log('4 promise')
  })
  process.nextTick(() => {
    console.log('5 nextTick')
  })
  setTimeout(() => {
    console.log('6 setTimeout')
  }, 0)
  setImmediate(() => {
    console.log('7 setImmediate')
  })
}, 0)

---

1 setTimeout
5 nextTick
4 promise
3 setImmediate
7 setImmediate
2 setTimeout
6 setTimeout
複製代碼

題目

題目 1

setTimeout setInterval 是否是相同優先級?是否被推到同一個隊列

setTimeout(() => {
  console.log('1 timeout')
}, 0)
setInterval(() => {
  console.log('2 interval')
}, 0)
setTimeout(() => {
  console.log('3 timeout')
}, 0)
setInterval(() => {
  console.log('4 interval')
}, 0)
setTimeout(() => {
  console.log('5 timeout')
}, 0)
複製代碼

題目 2

setTimeout(() => {
  console.log('1 setTimeout')
  setTimeout(() => {
    console.log('2 setTimeout')
  }, 0)
  setImmediate(() => {
    console.log('3 setImmediate')
    setImmediate(() => {
      console.log('4 setImmediate')
    })
    process.nextTick(() => {
      console.log('5 nextTick')
    })
  })
  setImmediate(() => {
    console.log('7 setImmediate')
  })
  setTimeout(() => {
    console.log('8 setTimeout')
  }, 0)
}, 0)

複製代碼

題目解答前往 give-me-job issue #1

總結

微任務(microTask)是大爺,宏任務(macroTask)得讓着微任務,可是一旦讓一個類型的宏任務開始執行,那就得等這個類型的宏任務執行完,而後才能執行微任務!!!在宏任務中被推入隊列的宏任務得在下一輪才能開始執行,這一輪沒新宏任務的份。


歡迎你們關注個人掘金和公衆號,算法、TypeScript、React 及其生態源碼按期講解。

相關文章
相關標籤/搜索