文章出自個人博客 https://knightyun.github.io/2019/06/20/js-event-loop,轉載請申明。
運行時(Runtime)
一個 JavaScript 運行時包含 棧(stack), 堆(heap), 隊列(queue);
棧 (stack)
棧 具有 先進後出 (FILO, First In Last Out) 的特點,有時也叫做 堆棧,可以理解爲一個開口向上的容器,先進入的物體壓瓶底,後進入的物體一層層向上堆疊,最後取出時,也是一個個拿出來,先拿出最後放進去的,也就是在最上面那個,最後拿出的就是之前第一個放入瓶底的物體;其中容器裏的每一個物體叫做 棧幀,理解爲動畫的每一幀,即最小單元;
動畫描述:
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.
// 可以慢慢看幾遍捋一下順序
演示動畫:
到這裏可能就有問題了,函數能無限調用下去?能無限向棧中壓入物體?當然,這個容器是有限制的,例如,在電腦瀏覽器控制檯輸入以下代碼:
(function fn(){fn()})()
其實就是一個遞歸函數,不斷調用自己,並且一直執行下去,那麼不出意外,會彈出如下錯誤提示:
大致意思就是說執行棧發生了溢出,就是不斷調用的函數太多了,超過了棧的規定大小;
也可以嘗試輸入以下代碼,看一下使用的瀏覽器的棧的尺寸:
var i = 0;
(function fn() {
console.log(++i);
fn();
})()
回車之後,在瀏覽器沒有卡死的情況下 -_-,n 分鐘之後,應該會出現以下錯誤提示:
最後一個出現的數字應該就是極限了,這裏使用的是 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 秒後)
動畫演示:
這裏的結果就明顯與之前的例子不同了,根據上面的描述,順序爲:
- 輸出
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)
所謂事件循環,大致就是上訴過程;這裏的 事件 指的就是消息隊列中的消息,即隊列中的調用函數;循環 即不斷執行完隊列中的消息,並等待是否有新消息到達,進而將其執行的這一循環過程;