JavaScript事件循環


文章出自個人博客 https://knightyun.github.io/2019/06/20/js-event-loop,轉載請申明。


運行時(Runtime)

一個 JavaScript 運行時包含 棧(stack), 堆(heap), 隊列(queue);

棧 (stack)

具有 先進後出 (FILO, First In Last Out) 的特點,有時也叫做 堆棧,可以理解爲一個開口向上的容器,先進入的物體壓瓶底,後進入的物體一層層向上堆疊,最後取出時,也是一個個拿出來,先拿出最後放進去的,也就是在最上面那個,最後拿出的就是之前第一個放入瓶底的物體;其中容器裏的每一個物體叫做 棧幀,理解爲動畫的每一幀,即最小單元;

動畫描述:

stack_null.gif

JavaScript 執行時,每一個調用函數執行時會被壓入棧中,稱爲 壓棧,這個函數執行完畢後從棧中彈出,稱爲 彈棧;即某個物體放入容器一定時間後,再從容器裏面取出來,方便爲下一次放入物體騰出空間;

例如:

function fn1() {
    console.log('Message 1.');
}
console.log('Message 0.');
fn1();

// Message 0.
// Message 1.

如果一個函數執行時還會調用第二個函數,那麼第一個函數壓入棧底後,隨後第二個函數便會壓在第一個上面,如果還存在第三個、第四個等等,便以此類推向上堆疊,直到最後調用的一個函數執行完之後,在從後往前一次彈出每一個函數;

可以理解爲容器放入第一個物體後,本來應該隨後取出的,但是這個物體又牽連了第二個物體,所以又繼續放入第二個,甚至第三個、第四個等等;

例如:

function fn1() {
    console.log('Message 1.');
}
function fn2() {
    fn1();
    console.log('Message 2.');
}
function fn3() {
    fn2();
    console.log('Message 3.');
}

console.log('Message 0.');
fn3();

// Message 0.
// Message 1.
// Message 2.
// Message 3.
// 可以慢慢看幾遍捋一下順序

演示動畫:

stack_fn.gif

到這裏可能就有問題了,函數能無限調用下去?能無限向棧中壓入物體?當然,這個容器是有限制的,例如,在電腦瀏覽器控制檯輸入以下代碼:

(function fn(){fn()})()

其實就是一個遞歸函數,不斷調用自己,並且一直執行下去,那麼不出意外,會彈出如下錯誤提示:

stack-exceed-fn.png

大致意思就是說執行棧發生了溢出,就是不斷調用的函數太多了,超過了棧的規定大小;

也可以嘗試輸入以下代碼,看一下使用的瀏覽器的棧的尺寸:

var i = 0;
(function fn() {
    console.log(++i);
    fn();
})()

回車之後,在瀏覽器沒有卡死的情況下 -_-,n 分鐘之後,應該會出現以下錯誤提示:

stack-exceed-num.png

最後一個出現的數字應該就是極限了,這裏使用的是 Chrome 瀏覽器,可以看出還是比較大的;

堆 (heap)

在運行期間被用來動態分配內存,比如給變量、對象、數組、字符串等分配特定的內存地址,用以訪問,不像棧和隊列,它是一個非結構化的區域;

隊列 (queue)

隊列 具有 先進先出 (FIFO, First In First Out) 的特點,這裏就理解爲排隊取餐的一隊人,先到先得,然後從前面先走,後來的排在最後,並且不允許插隊;

在 JavaScript 運行時中,隊列的結構被應用到了 消息隊列 中;前面說到代碼執行時,調用函數執行時被壓入執行棧 (call stack) 中,並且需要等待該函數徹底執行完後,才能彈出棧,但是假如遇到 setTimeout 這樣延時事件,由於 JavaScript 引擎的 單線程 特點,區別於其他語言,因此執行是不會因爲延時函數而中斷的,此時便會將 setTimeout 延時調用的函數放入 消息隊列 中,等待當前環境所有壓棧、彈棧操作執行完畢,再按照順序執行隊列中的調用函數;

例如:

function fn1() {
    console.log('Message 1.');
}
function fn2() {
    fn1();
    setTimeout(function delay1(){
        console.log('Message 2.');
    }, 0)
}
function fn3() {
    fn2();
    setTimeout(function delay2(){
        console.log('Message 2.5.')
    }, 1000)
    console.log('Message 3.')
}

console.log('Message 0.')
fn3();

// Message 0.
// Message 1.
// Message 3.
// Message 2.
// Message 2.5. (大約 1 秒後)

動畫演示:

stack_fn_queue.gif

這裏的結果就明顯與之前的例子不同了,根據上面的描述,順序爲:

  • 輸出 Message 0.
  • fn3() 壓入棧底;
  • 然後壓入 fn2()
  • 最後壓入 fn1()
  • fn1() 內的語句執行完後,輸出 Message 1.
  • 執行函數 fn2() 的語句;
  • 由於 fn2() 內的 setTimeout() 函數是一個延時函數,所以其調用函數 delay1() 就被放到了消息隊列中;
  • 然後執行 fn3() 中的 setTiemout(),其調用函數 delay2() 也被放入了隊列中;
  • 由於 delay1() 的延時小於 delay2(),所以 delay2() 被放到了 delay() 的後面,反之顛倒順序;
  • 輸出 fn3() 中的 Message 3.
  • 此時開始執行消息隊列的函數;
  • 先執行 delay1() 輸出 Message 2.
  • 然後執行 delay2() 輸出 Message 2.5

注意,即使 delay1() 的延時爲 0,也並不意味着該回調函數會在 0 毫秒後執行,即不會立即執行,由於機制原因,同樣會被放入消息隊列中,只不過會 比較早執行 而已;

事件循環 (Event Loop)

所謂事件循環,大致就是上訴過程;這裏的 事件 指的就是消息隊列中的消息,即隊列中的調用函數;循環 即不斷執行完隊列中的消息,並等待是否有新消息到達,進而將其執行的這一循環過程;


技術文章推送
手機、電腦實用軟件分享
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章