理解javascript事件迴圈

Meteor發表於2018-01-23

1背景

從敲下第一行“Hello World!”起,我們都無時無刻的使用並接觸著事件迴圈。

去年寫react的時候總會遇到設定setState,後面呼叫值不會改變的問題。會想到是非同步的原因,試著延時定時問題得到解決。

    this.setState({status: 1})
    console.log(this.state.status);   //0
    setTimeout(()=>{
        console.log(this.state.status);    //1
    },100)
複製程式碼

問題可以解決,所以延時定時解決了之後的很多問題。

後面看到javascript的事件迴圈,才真正理解了這之中的原理。

2單執行緒

從學習JavaScript開始,就一直看書和資料說JavaScript是一門單執行緒的語言。JavaScript到處存在著非同步(剛開始認為非同步是主執行緒下開的子執行緒)是依靠事件迴圈來實現的。

3同步非同步

單執行緒就意味著,所有任務需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等著。

如果計算量大,讀寫IO請求、Ajax開銷大。如果通過同步來獲取資料會造成程式阻塞。所以像這種開銷大的任務,我們使用非同步處理。

所有任務可以分成兩種,一種是同步任務(synchronous),另一種是非同步任務(asynchronous)。同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;非同步任務指的是,不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。

4事件迴圈

理解javascript事件迴圈
根據事件迴圈圖來理解這一段程式碼

//首先整段程式碼進入主執行緒執行
let a= 5;
//同步程式碼立即執行,輸出a=5
console.log(a);
//按鈕繫結點選事件,當使用者點選按鈕事件回撥函式進入任務佇列,待主執行緒執行完畢呼叫該函式
oBtn.onclick(function () {
    a = 10;
    console.log(a, 'click');
})
//當延時定時時間到的時候,回撥函式進入任務佇列,待主執行緒執行完畢呼叫任務佇列中的函式。
setTimeout(function () {
    console.log(a,'timeout');
},5000)
複製程式碼

5node中的事件迴圈

Node.js也是單執行緒的Event Loop,但是它的執行機制不同於瀏覽器環境。

理解javascript事件迴圈

  • V8引擎解析JavaScript指令碼。
  • 解析後的程式碼,呼叫Node API。
  • libuv庫負責Node API的執行。它將不同的任務分配給不同的執行緒,形成一個Event Loop(事件迴圈),以非同步的方式將任務的執行結果返回給V8引擎。
  • V8引擎再將結果返回給使用者。

除了setTimeout和setInterval這兩個方法,Node.js還提供了另外兩個與"任務佇列"有關的方法:process.nextTick和setImmediate。它們可以幫助我們加深對"任務佇列"的理解。

process.nextTick方法可以在當前"執行棧"的尾部----下一次Event Loop(主執行緒讀取"任務佇列")之前----觸發回撥函式。也就是說,它指定的任務總是發生在所有非同步任務之前。setImmediate方法則是在當前"任務佇列"的尾部新增事件,也就是說,它指定的任務總是在下一次Event Loop時執行,這與setTimeout(fn, 0)很像。

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED
複製程式碼

上面程式碼中,由於process.nextTick方法指定的回撥函式,總是在當前"執行棧"的尾部觸發,所以不僅函式A比setTimeout指定的回撥函式timeout先執行,而且函式B也比timeout先執行。這說明,如果有多個process.nextTick語句(不管它們是否巢狀),將全部在當前"執行棧"執行。

現在,再看setImmediate。

setImmediate(function A() {
  console.log(1);
  setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0);
複製程式碼

哪個回撥函式先執行呢?答案是不確定。執行結果可能是1--TIMEOUT FIRED--2,也可能是TIMEOUT FIRED--1--2。setImmediate與setTimeout不分伯仲。

但在遞迴中,setImmediate永遠優於setTimeout

setImmediate(function (){
  setImmediate(function A() {
    console.log(1);
    setImmediate(function B(){console.log(2);});
  });

  setTimeout(function timeout() {
    console.log('TIMEOUT FIRED');
  }, 0);
});
// 1
// TIMEOUT FIRED
// 2
複製程式碼

最後感謝阮一峰老師的文章和張仁陽老師的思路。

相關文章