EventLoop 事件循環機制

1. EventLoop 概念

Event Loop即事件循環,是指瀏覽器或Node的一種解決javaScript單線程運行時不會阻塞的一種機制,也就是我們經常使用異步的原理。

因爲JavaScript就是單線程,也就是說,同一個時間只能做一件事。單線程就意味着,所有任務需要排隊,前一個任務結束,纔會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等着。

所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。

2.同步任務和異步任務

在講EventLoop之前,先說一下什麼是 同步任務異步任務

同步任務:函數返回—>拿到預期結果
異步任務:函數返回—>拿不到預期結果,還要通過一定的手段獲得結果

簡單點說 :
同步任務:會立即執行的任務
異步任務:不會立即執行的任務(異步任務又分爲宏任務與微任務)

在異步任務中,任務被分爲兩種,一種宏任務(MacroTask)也叫Task,一種叫微任務(MicroTask)

  • 宏任務:由宿主對象發起的任務(setTimeout)

    宏任務包括 scriptsetTimeoutsetIntervalsetImmediate(Node.js)I/OpostMessageMessageChannelUI rendering

  • 微任務:由js引擎發起的任務(promise)

    微任務包括 process.nextTick(Node.js)promise.then()promise.catch()MutationObserver

宏任務與微任務的概念不必強記,理解不了可先記住宏任務和微任務包含哪些情況。

由上述可知,在代碼執行的過程中,同步任務會立即執行,異步任務會通過一些手段和過程纔會拿到結果。所以在異步任務等待結果的同時,可先執行其後的同步任務。當異步任務有結果的時候,在回過頭來執行異步任務。

所以 EventLoop 的執行機制如下:

(1)所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。

(2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。

(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。

(4)主線程不斷重複上面的第三步。

話不多說,直接上題

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(resolve => {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')
  })

console.log('script end')

這道題有些繁瑣,先放置一下,後面將會對這道題進行講解。

EvnentLoop執行的機制就是先執行同步代碼,接着是微任務,然後是宏任務

3. EventLoop 執行解析

遇到 Promise
在解答上題之前,先來兩道簡單點的,小試牛刀一下:
例題 1:

setTimeout(() => {
console.log(‘A’);
}, 0);
var obj = {
func: function() {
setTimeout(function() {
console.log(‘B’);
}, 0);
return new Promise(function(resolve) {
console.log(‘C’);
resolve();
});
},
};
obj.func().then(function() {
console.log(‘D’);
});
console.log(‘E’);

根據事件循環機制的執行順序,上述代碼執行步驟如下:

  • 執行 setTimeout ,由於是宏任務,將其放置進宏任務隊列,此時宏任務隊列爲 [ 'A ’ ]

  • 接着執行 obj.func() , 先執行 setTimeout,由於是宏任務,將其放置進宏任務隊列,此時宏任務隊列爲 [ 'A ','B ’ ]

  • 然後函數返回值是一個 Promise,因爲這是一個同步操作,所以先打印出 'C ’

  • 緊接着是 promise.then() , 由於是一個微任務,將其放置進微任務隊列,此時微任務隊列爲 [ 'D ’ ]

  • 接着就會執行同步任務,打印出 'E ’

  • 因爲微任務執行比宏任務早,所以打印 'D ’

  • 然後會執行宏任務,依次打印出來 'A ’ 和 'B ’

所以,這道題的打印順序就是

C -> E -> D -> A -> B

要注意的是 obj.func().then() 這裏,obj.func() 是普通函數/同步代碼,後面的 .then() 纔是微任務

例題2:

function go() {
console.log(5)
}
let p = new Promise(resolve => {
resolve(1);
Promise.resolve(go()).then(() => console.log(2));
console.log(4);
}).then(t => console.log(t));
console.log(3);

上述代碼執行步驟如下:

  • 在 new Promise的時候,就會執行內部的方法。首先就是執行 resolve(1) ,將 1 返回到 then 裏
  • 緊接着會執行內部的 Promise.resolve( go() ) ,因爲這是一個普通函數,是一個同步執行的代碼,所以會打印出 5
  • promise.then()是一個微任務,會將 console.log(2) 放置到微任務隊列,此時微任務隊列爲 [ 2 ]
  • 然後會順序執行同步任務 console.log(4),將 4打印出來
  • 然後會執行 new Promise 的 then() 方法,由於這也是一個微任務,所以會放置到微任務列表,此時微任務隊列爲 [ 2 ,1 ]
  • 順序執行同步任務 console.log(3),將 3打印出來
  • 最後,依次打印出微任務隊列的 2 和 1

so,上述代碼的打印結果爲

5 -> 4 -> 3 -> 2 -> 1

要注意的是 Promise.resolve(go()) 這裏也是普通函數/同步代碼,當執行到這一行的時候,會立即執行 go(),後面的 .then() 纔是微任務

以上兩道小題,是關於 Promise 的EventLoop分析。

遇到 async/await
根據定義,我們知道,async/await 僅僅是生成器的語法糖,所以不要怕,只要把它轉換成 Promise 的形式即可。

async function foo() {
console.log(1)
await bar();
console.log(2)
}
async function bar() {
console.log(3)
}
foo();

可以轉換成 Promise的形式,如下

 function foo(){
	 console.log(1)
	 Promise.resolve(bar()).then(()=>{
		 console.log(2)
	})
}
function bar(){
	 console.log(3)
}
foo()

執行步驟如下:

  • 執行 foo() ,打印同步代碼 內容 1
  • 順序執行 Promise.resolve() ,同步執行 bar() 方法,打印 3
  • Promise.thenr() 是一個微任務,將其放置到 微任務隊列,此時微任務隊列爲[ 2 ]
  • 執行微任務隊列,打印隊列內容 2

所以執行的順序是:

1 -> 3 -> 2

所以,開篇的那段代碼,可改寫成如下:

function async1() {
  console.log('async1 start'); // 2
 
  Promise.resolve(async2()).then(() => {
    console.log('async1 end'); // 6
  });
}
 
function async2() {
  console.log('async2'); // 3
}
 
console.log('script start'); // 1
 
setTimeout(function() {
  console.log('settimeout'); // 8
}, 0);
 
async1();
 
new Promise(function(resolve) {
  console.log('promise1'); // 4
  resolve();
}).then(function() {
  console.log('promise2'); // 7
});
console.log('script end'); // 5
  • 首先打印出 script start
  • 接着將 settimeout 添加到宏任務隊列,此時宏任務隊列爲 [‘settimeout’]
  • 然後執行函數 async1,先打印出 async1 start,又因爲 Promise.resolve(async2()) 是同步任務,所以打印出 async2,接着將 async1 end 添加到微任務隊列,,此時微任務隊列爲 [‘async1 end’]
  • 接着打印出 promise1,將 promise2 添加到微任務隊列,,此時微任務隊列爲 [‘async1 end’, promise2]
  • 打印出 script end
  • 因爲微任務優先級高於宏任務,所以先依次打印出 async1 end 和 promise2
  • 最後打印出宏任務 settimeout

其實,上面的代碼都弄清楚之後,對於EventLoop的執行機制就掌握的差不多了。

接下來是一道強化的題,有興趣的可以看一下

 console.log('1')
      setTimeout(()=>{
          console.log('2');
          new Promise(resolve=>{
            console.log('3')
            resolve();
          }).then(()=>{
            console.log('4')
          })
      },0)
      new Promise(resolve=>{
            console.log('5')
            resolve();
          }).then(()=>{
            console.log('6')
          }) 
      setTimeout(()=>{
          console.log('7');
      },0)   
      setTimeout(()=>{
          console.log('8');
          new Promise(resolve=>{
            console.log('9')
            resolve();
          }).then(()=>{
            console.log('10')
          })
      },0)
      new Promise(resolve=>{
            console.log('11')
            resolve();
          }).then(()=>{
            console.log('12')
          })
      console.log('13'); 

這裏只把運行的答案寫一下

1 -> 5 -> 11 -> 13 -> 6 -> 12 -> 2 -> 3 -> 4 -> 7 -> 8 -> 9 -> 10

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