給女朋友講了講 V8 引擎的“回調函數”!
回調函數相信大家并不陌生,但是對于女朋友來說比較陌生,要想給女朋友講明白回調函數是怎么回事,為什么要這樣設計,使用回調函數應該注意什么,確實不容易,所以有了這篇文章。
在 JavaScript 中,我個人覺得理解回調函數,就有助于我們理解 JavaScript 中的其它設計,比如異步編程、消息循環、一些常用到的 Web API 以及 Node 中的異步,這些都與回調函數息息相關,所以咱們一步步的來學習和使用。
廢話不多說,直接上干貨!
什么是回調函數?
回調函數也是函數的一種,回調函數與普通函數的區別在于它的調用時機。所謂的回調函數是把一個函數當作另一個函數的參數,并在這個函數的某個時機進行調用,我們就將這個傳入的函數稱為回調函數。
回調函數分為同步回調和異步回調。它倆的區別在于調用的位置不同。
1. 同步回調
所謂的同步回調函數是在執行的函數內部被調用的。舉個例子:
- function fn(){
- console.log("我是同步回調函數");
- }
- function bar(f){
- f();
- }
- bar(fn);
我們聲明了一個回調函數 fn,我們傳入 bar 函數,我們在 bar 內部調用了 fn,此時就是同步調用。
2. 異步回調
而異步回調函數是在執行的函數外部被調用的。如下代碼:
- function fn(){
- console.log("我是異步回調函數");
- }
- settimeout(fn, 1000);
上述代碼中,settimeout 的第一個參數 fn 是傳入的一個回調函數,當程序執行 1000 ms 的時候,此時這個回調函數被調用,而且是在 setTimeout 函數外部被調用的,我們稱 fn 為異步回調函數。
JS 線程架構
上述的問題理解起來非常簡單,尤其是同步回調函數。但是對于異步回調函數什么時候被調用的,是在什么位置被調用的,我們目前是非常模糊的。
所以要想知道異步回調函數的調用時機和位置,我們需要理解 JS 的線程架構,比如消息隊列、事件循環,從根上理解 JS 是如何設計這些東西的。
JS 的最初設計是單線程的,所謂的單線程就是同一時間只能干一件事情,而且 JS 運行的這個單線程就是頁面的 UI 線程,畢竟 JS 為了能夠更方便的操作 DOM 嘛。
那么問題來了,當我們把 JS 設計到 UI 頁面線程,就會出現一個問題,當用戶通過頁面交互產生一個事件時,如果當前的線程正在處理其他的任務,那么這個交互事件需要等當前 UI 線程的任務處理完畢才能被處理,所以這樣設計比較雞肋。
如果當前 UI 線程一直執行其他任務,那么這個用戶的交互事件任務一直不被處理,會出現頁面點擊無效的假象。
谷歌 V8 團隊為了解決這個問題,那么就引入了消息隊列。
消息隊列
有了消息隊列,我們把所有產生的事件,無論是 JavaScript 產生的事件還是用戶點擊頁面交互產生的事件,都統一按照先后循序放到消息隊列中,那么 UI 線程就不斷的循環這個消息隊列,有任務就取出來去執行。
有了消息隊列之后,這個主線程就可以有序的執行各種事件任務了。
異步任務什么時候調用?
還是上述的 setTimeout 異步回調函數的例子,假如當前線程在消息隊列中取出 setTimeout 這段代碼執行,發現 fn 這個任務是在 1000 ms 后執行的,1000 ms 過后,主線程就會將 fn 封裝成一個事件任務扔到消息隊列中去等待被執行。
等消息隊列中的執行到一定的時機,就會取出這個被封裝過的 fn 回調函數任務,在主線程中被執行。
這個理解起來并不是很難,但是有一類回調函數比較特殊。當主線程在消息隊列中取出一個網絡下載的任務時,網絡下載比較耗時,我們不可能讓它在主線程中阻塞其他任務執行,所以主線程就會交給網絡線程去執行這個下載任務,然后主線程回繼續有序的在消息隊列中取出其他任務執行。
此時網絡線程會接受到這個任務執行下載操作,當網絡線程把文件下載完畢之后,就將下載的結果封裝成一個事件任務,放入到消息隊列中。當主線程執行到這個任務時,就知道網絡線程已經下載完了,然后將下載的結果呈現給用戶。
最后
好了,今天主要和大家分享了 JavaScript 中回調函數的由來和異步回調函數的設計,這也是 V8 引擎中的一小部分,接下來會通過這樣一個個的知識點,挖掘 V8 谷歌引擎的各個方面的優秀設計方案。