Event Loop – 事件隊列

Event Loop

定義:

event - 事件 loop - 循環,既然叫事件循環,那麼循環的點在哪?

循環的是一個又一個的任務隊列,這些任務隊列由宏任務和微任務構成

兩條原則

  1. 一次處理一個任務
  2. 一個任務開始後直到完成,不會被其他任務中斷

事件處理之間的關係

一次事件處理中,最多處理一個宏任務,但是會處理所有的微任務,任務開始後,會將內部所有的宏觀函數加到宏觀隊列等待,會將所有的圍觀函數加到微觀隊列等待,當前宏任務處理完畢後,開始逐個處理微任務,當微任務執行處理完成,會檢查是否需要更新ui,如果是則重新渲染ui。之後再次檢查宏觀任務隊列中的下一個宏觀任務,取出來並且執行

執行規則

image

宏任務包含:

  1. 整體script(也叫js主進程)
  2. settimeout
  3. setinterval
  4. requestAnimationFrame

微任務包含:

  1. Promise.then catch finally
  2. Generator 函數
  3. async/await
  4. MutationObserver

異步任務都有哪些

  1. 回調函數
  2. Promise(注意new promise裏面的屬於同步任務)
  3. async
  4. Generator
  5. 事件監聽
  6. 發佈/訂閱
  7. 計時器
  8. requestAnimationFrame
  9. MutationObserver

題外話

瀏覽器的渲染,不同瀏覽器處理是不同的,但是大部分瀏覽器選擇一般是一秒鐘60幀率(也就是60fps),這意味着瀏覽器會在16ms左右渲染一幀,所以我們單個任務和該任務的所有附屬微任務都應該在16ms內完成,以達到顯示平滑流暢。

怎麼證明js事件隊列存在,就拿簡單的setTimeout來說

console.time("settimeout");
setTimeout(() => {
  console.log('settimeout執行')
  console.timeEnd("settimeout");
}, 1000);
for (let a = 0; a < 50000; a++) {
  console.log(a)
}

time會輸出多少呢?這種事件是怎麼觸發的?

代碼是在一秒鐘後才加入事件隊列,之後等待執行。怎麼證明,很簡單我們在拿出一個定時器

console.time("settimeout");
console.time("settimeout2");
setTimeout(() => {
  console.log('settimeout執行')
  console.timeEnd("settimeout");
}, 1000);
setTimeout(() => {
  console.log('settimeout2執行')
  console.timeEnd("settimeout2");
}, 1);
for (let a = 0; a < 50000; a++) {
  console.log(a)
}

明明後聲明的定時器2,卻先執行了,不知道有沒有發現,定時器1和定時器2執行時間,很接近!!!這很重要,為什麼會這樣,這就是今天所說的事件隊列,當1毫秒時候,定時器2被加入了事件隊列,當一秒鐘時候定時器1被加入事件隊列,然後for執行完後,隊列中下一個任務為定時器2,但是他只有一個console故而很快,所以拿出了定時器進行執行。再來看個額外例子,來鞏固下:

console.time("settimeout");
console.time("settimeout2");
setTimeout(() => {
  console.log('settimeout執行')
  console.timeEnd("settimeout");
}, 1000);

for (let a = 0; a < 50000; a++) {
  console.log(a)
}

setTimeout(() => {
  console.log('settimeout2執行')
  console.timeEnd("settimeout2");
}, 1);

枯燥無味的定義基本就這樣,我們來從實踐來做分析

從練習題來說

console.log(1)
setTimeout(function () {
  console.log(2)
  setTimeout(function () {
    console.log(3)
  })
})
setTimeout(function () {
console.log(4)
})
console.log(5)

第一次運行之後
image.png

這時當前主進程隊列已經結束,開始檢測微任務隊列是否還有未完成的任務,發現微任務隊列已經空了所以,當前宏任務隊列結束,開始下一組宏任務

image.png

settimeout2任務完結,檢查當前微任務隊列為空,開始下一組宏任務

image.png

所以最終答案為:
1,5,2,4,3

  • 注意:setTimeout是以其觸發事件為寫入隊列時間。如果這麼說不理解的話 可以將上面代碼改為如下:
console.log(1)
setTimeout(function () {
  console.log(2)
  setTimeout(function () {
    console.log(3)
  })
})
setTimeout(function () {
  console.log(4)
})
console.log(5)

這樣就會輸出1,5,2,3,4

tips: setTimeout寫的1000不等於他就是在上一次事件結束後的1000ms,而是以他聲明開始就進行計時的。不過這不是本篇文章的核心,我們不深究他的邏輯,繼續看第二個例子

console.log(1)
setTimeout(() => {
  console.log('2')
  Promise.resolve(4).then((res) => console.log(res))
}, 0);
setTimeout(() => {
  console.log('3')
}, 0);

image.png

主進程執行完畢,檢查微任務隊列為空,當前宏任務結束,開啟下一組宏任務

image.png

settimeout宏任務執行完畢,檢查宏任務隊列,拿出settimeout3的宏任務,將它拿出來執行。這個比較簡單咱們就不畫圖了

所以本題答案為1,2,4,3

let promise = new Promise(function(resolve, reject) {
  console.log('1');
  resolve();
});

promise.then(function() {
  console.log('2');
});
console.log(3)

image.png

檢查宏任務隊列發現為空,所以本次代碼結束

答案為:1,3,2

console.log(1);
setTimeout(function(){
  console.log(2);
}, 0);
Promise.resolve().then(function(){
  console.log(3);
}).then(function(){
  console.log(4);
});

image.png

當前宏任務已經結束,查看宏任務隊列中發現還有settimeout沒有執行,將它取出來執行,
輸出2.

所以本題答案為:1,3,4,2

setTimeout(()=>{
  console.log('1');
},0);
var obj={
  func:function () {
      setTimeout(function () {
          console.log('2')
      },0);
      return new Promise(function (resolve) {
          console.log('3');
          resolve();
      })
  }
};
obj.func().then(function () {
  console.log('4')
});
console.log('5');

image.png

主線進程及其微觀進程執行完畢,會拿出下一組settimeout1執行,執行後會進行檢測微觀隊列,如果沒有則會繼續往下取出settimeout2執行。至此程序結束。

所以本題答案為:3,5,4,1,2

console.log(1)
const p = new Promise(function(resolve, reject) {
  console.log(2)
  resolve()
}).then(() => {
  console.log(3)
  throw(new Error('錯誤'))
}).catch(() => {
  console.log(4)
})
console.log(5)
setTimeout(() => {
  console.log(6)
}, 0);
console.log(7)
p.then(() => {
  console.log(8)
  throw(new Error('錯誤2'))
})

image.png

至此,當前宏任務結束。檢查宏任務隊列。取出下一組宏任務,settimeout6,並執行

所以答案為:1,2,5,7,3,4,8,Error,6

最後留兩道題給大家做學習用。

如果不能一眼看出。可以像我一樣畫一個圖。進行梳理。本文中為了代碼整齊,settimeout都是直接簡化為儘快執行。其實settimeout應該是在到達他聲明的時間時候,才進入宏觀隊列排隊的。

根據以下規則,你將不會在遇到事件隊列的問題

  1. 一次處理一個任務
  2. 宏任務+當前所有微任務為一組
  3. 同步直接執行、異步會加入事件隊列
  4. 所有異步的加入隊列時間均以他們觸發時間為準
console.log(1)
const p = new Promise(function(resolve, reject) {
  console.log(2)
  setTimeout(() => {
    console.log(9)
  }, 0);
  resolve()
}).then(() => {
  console.log(3)
  throw(new Error('錯誤'))
}).catch(() => {
  console.log(4)
})
console.log(5)
setTimeout(() => {
  console.log(6)
}, 0);
console.log(7)
p.then(() => {
  console.log(8)
  throw(new Error('錯誤2'))
})
console.log(1)
const p = new Promise(function(resolve, reject) {
  console.log(2)
  setTimeout(() => {
    console.log(9)
  }, 0);
  resolve()
}).then(() => {
  console.log(3)
  throw(new Error('錯誤'))
}).catch(() => {
  console.log(4)
})
console.log(5)
setTimeout(() => {
  console.log(6)
}, 0);
console.log(7)
p.then(() => {
  console.log(8)
  throw(new Error('錯誤2'))
})
requestAnimationFrame(function() {
  console.log(10)
})

宏觀微觀

宏觀微觀不是嵌套!!!!!即使代碼嵌套了在隊列中也不是嵌套的!!!!

怎麼利用這些,在代碼中優化自己的代碼,舉個例子來說

for (let i = 0 ; i<50000;i++) {
  const div = document.createElement('div')
  div.innerText = i
  document.body.appendChild(div)
}


function slice(startSplitNumber, total, sliceNumber, cb) {
  const oneNumber = total/sliceNumber
  const start = startSplitNumber * oneNumber
  const end = (startSplitNumber + 1) * oneNumber
  if (start >= total) return
  setTimeout(() => {
    for(let i = start; i < end;i++) {
      cb(i)
    }
    slice(startSplitNumber + 1, total, sliceNumber, cb)
  }, 0)
}

slice(0, 50000, 5000, function (current) {
  const div = document.createElement('div')
  div.innerText = current
  document.body.appendChild(div)
})