- 本文也參考了一篇不錯的文章: https://zhuanlan.zhihu.com/p/138140285
- 讀懂本文需要你對JS和ES6有一定的使用經驗( feihua )
回調函數和異步編程是什麼鬼?JS爲什麼需要他們?
- 首先需要了解, JS是單線程的, 這意味着同一時刻JS引擎只能幹一件事情
- 將下面這段代碼放到瀏覽器的控制檯console中運行試試
console.log(1);
alert(2);
console.log(3);
- 運行結果顯而易見, 先輸出了1, 緊接着彈層顯示2, 當點擊彈層的按鈕使彈層消失後, 輸出了3
- 這裏有一個概念, 基於JS單線程的特點, 當我們執行的一段JS代碼中含有( 網絡請求, 定時任務 )等不能立即獲得結果的代碼, JS代碼的執行就會卡住
- 就比如當上面代碼執行到第二行, 當你沒有點擊彈層的按鈕時, alert(2)這行代碼就沒有獲得響應, JS的執行就會卡住, 此時CPU就是空閒的, JS既不繼續執行, 頁面也不會接着渲染, 這時候就出現了頁面卡頓的"白屏"效果, 會給用戶造成很差的使用體驗
- 好了, 現在需求出現了, 我們想改善用戶的使用體驗, 也就是說我們希望當遇到不能立即獲得結果的代碼時, JS代碼的執行不會卡住, 聰明的前輩們想出瞭如下一招
- 當遇到這種不能立即獲得結果的代碼, 就將這些代碼放入一個函數中, 然後, 將這個函數暫存在其他地方並繼續執行JS代碼, 當暫存的函數"時機到了", 再回過頭來執行它( 所以這種函數叫回調函數 ), 這種編程方式就被稱爲異步編程
- 那行, 使用基於回調函數的異步方式改寫如上代碼
console.log(1);
setTimeout(function cb() {
alert(2);
});
console.log(3);
-
再放到瀏覽器console中運行一下試試? 成功地先輸出1和3, 然後彈層顯示2
- 注意, 這裏用定時器可以實現回調函數, 原因在後面談到event-loop事件循環實現回調的流程的時候會說明
- 異步( 定時器, ajax )要基於回調來實現, DOM事件也基於回調來實現
-
我個人非常佩服前人的這種處理方式, 但只有我們再深入一些瞭解這種基於回調函數的異步寫法背後的實現方式, 才能讓我們看得透徹, 用的順手, 講的明白, 對面試, 工作都大有裨益, 接下來開始講述其實現方式—eventLoop事件循環
Event Loop 事件循環實現回調的流程介紹
- 依舊是這段代碼, 以小看大
console.log(1);
setTimeout(function cb() {
alert(2);
});
console.log(3);
- 淺嘗這段代碼的執行過程
- 執行第 1 行代碼,
console.log(1)
被壓入callStack調用棧, 執行輸出1, 然後該行代碼從調用棧頂彈出 - 執行2~4行代碼, 先被壓入call stack調用棧, 發現是異步代碼, 然後在Web API專用的存儲位置記錄一個包含函數cb的定時器, 然後代碼從棧頂彈出
- 執行第 5 行代碼,
console.log(3)
被壓入callStack調用棧, 執行輸出3, 然後該行代碼從調用棧頂彈出 - 此時同步代碼全部執行完畢, 啓動event-loop事件循環機制輪詢callbackQueue回調隊列, 由於當定時器"時機到了", 就會將函數cb放入callbackQueue回調隊列, 所以callbackQueue中有事件cb, 事件cb被壓入callStack調用棧開始執行
alert(2)
被壓入callStack調用棧, 執行後該行代碼從調用棧頂彈出, 然後事件cb從調用棧頂彈出
- 執行第 1 行代碼,
- 換一段代碼, 依舊以小見大
console.log(1);
setTimeout(function cb1() {
alert(2);
});
Promise.resolve().then(function cb2() {
alert(3);
})
console.log(4);
-
運行這段代碼, 執行順序是: 1, 4, 3, 2
-
最開始我也有疑問, 如果cb1和cb2是被暫存在了同一個地方,那麼callbackQueue肯定符合先入先出的方式, 怎麼說也應該先alert(2), 很顯然, 暫存cb1和cb2的時候他們被放在了不同的地方
-
這裏先給出結論
- 微任務: 由ES6語法規定, 包括
Promise, async/await
- 宏任務: 由瀏覽器規定, 包括
setTimeout, setInterval, Ajax, DOM事件
- 微任務執行時機比宏任務早 !!!
- 微任務: 由ES6語法規定, 包括
-
接下來我從事件循環的層面解釋一下, 爲什麼微任務執行時機更早
-
第一步, 同步代碼, 一行一行放在 callStack 執行
-
遇到異步宏任務, 記錄到 WebAPI, 等待時機( 定時, 網絡請求等 ), 時機到了, 就移動到 callbackQueue
-
遇到異步微任務, 記錄到 micro-task-queue
-
如果 callStack 爲空( 即同步代碼執行完 ), 先執行micro-task-queue中的微任務
-
微任務執行完畢, 嘗試進行DOM渲染
-
然後 Event Loop 開始工作
-
輪詢查找 callbackQueue, 如果有則移動到 callStack 執行
-
然後繼續輪詢查找( 永動機一樣 )
-
- promise 和 async/await 單獨寫博客吧…也太多了(理不直氣也壯!)