本系列文章內容所有梳理自如下幾個來源:javascript
做爲一個前端小白,入門跟着這幾個來源學習,感謝做者的分享,在其基礎上,經過本身的理解,梳理出的知識點,或許有遺漏,或許有些理解是錯誤的,若有發現,歡迎指點下。html
PS:梳理的內容以《JavaScript權威指南》這本書中的內容爲主,所以接下去跟 JavaScript 語法相關的系列文章基本只介紹 ES5 標準規範的內容、ES6 等這系列梳理完再單獨來說講。前端
本篇會講到一個單線程事件循環機制,但並非網絡上對於 js 執行引擎介紹中的單線程機制,也沒有涉及宿主環境瀏覽器的各類線程,如渲染線程、js 引擎執行線程、後臺線程等等這些內容。java
嚴謹來說,應該不屬於 JavaScript 自身的單線程機制,而是宿主對象,如瀏覽器處理執行 js 代碼的單線程事件循環機制。android
回到正題,本篇所要講的,就是類比於 Android 中的主線程消息隊列循環機制,來說講在 JavaScript 中,若是設置了某個異步任務後,當異步任務執行完成須要回調通知時,這個回調任務的執行時機。git
若是還不清楚要講的是什麼,那麼先來看個問題:github
<script type="text/javascript"> $.ajax({ url: "https://easy-mock.com/mock/5b592c01e4e04f38c7a55958/ywb/is/version/checkVersion", data: {"key": 122}, type: "POST", success: function (data) { console.log("----------success-----------"); //何時會執行回調 }, error: function (e) { console.log("----------error-----------"); } }); //... </script>
這是用 jQuery 寫的 ajax 網絡請求的示例,這條請求天然是異步進行的,但當請求結果回來後,會去觸發 success 或 error 回調,那麼,問題來了:web
Q:想過沒有,若是請求結果回來後,這個回調的代碼是在什麼時機會被執行的?是立馬就執行嗎,無論當前是否正在執行某個函數內的代碼?仍是等當前的函數執行結束?又或者是?ajax
也許你還沒看懂這個問題要問的是什麼,不要緊,下面舉例分析時,會講得更細,到時你就知道這個問題要問的是什麼了。chrome
先來看看 Android 中的主線程消息隊列循環機制,固然若是你不是從 Android 轉前端,那能夠跳過這趴:
這張圖來自 Android消息機制(一):概述設計架構這篇文章中,我懶得本身畫了,借大佬圖片一用,若是不容許使用,麻煩告知下,我再來本身畫。
在 Android 裏有個主線程,由於只能在主線程中進行 UI 操做,因此也叫 UI 線程,這個主線程在應用啓動時就進入一個死循環中,相似於執行了 while(true){...}
這樣的代碼,等到應用退出時,退出該死循環。而死循環之因此不會卡死 CPU,是由於利用了 Linux 的 epoll 機制,通俗的來將,就是,主線程會一直循環往消息隊列中取消息執行,若是隊列中沒有消息,那麼會進入阻塞狀態,等有新的消息到來時,喚醒繼續處理。而阻塞和喚醒就是利用了 Linux 的 epoll 機制。
因此,在 Android 中,打開頁面是一個 message,觸摸屏幕也是一個 message,message 中指示着當前應該執行的代碼段,只有當前的 message 執行結束後,下會輪到下個 message 執行。
因此,在 Android 中的異步任務的回調工做,好比一樣異步發起一個網絡請求,請求結果回來後,須要回調到主線程中處理,那麼這個回調工做的代碼段會被封裝到 message 中,發送到消息隊列中排隊,直到輪到它來執行。
而 message 發送到消息隊列是基於 Handler 來傳輸,因此,在 Android 中,若是想要查看 message 是以什麼爲粒度,查找在哪裏經過 Handler 發送了 message 便可。
那麼,在 JavaScript 中,又是如何處理異步工做的回調任務的呢?
查了一些相關的資料,發現講的都是 JavaScript 的單線程,事件循環機制等之類理論,但卻沒看到,事件的粒度是什麼?
看完我能理解,JavaScript 也是相似 Android,同樣執行了某段相似 while(true){...}
的代碼來循環處理事件,但看完我仍舊沒法理解,這個事件的粒度是什麼,怎麼查看事件的粒度?
再舉個例子來講明個人疑問好了:
<script type="text/javascript"> console.log("----------1-----------"); $.ajax({ url: "https://easy-mock.com/mock/5b592c01e4e04f38c7a55958/ywb/is/version/checkVersion", data: {"key": 122}, type: "POST", success: function (data) { console.log("----------success-----------"); //何時會執行回調 }, error: function (e) { console.log("----------error-----------"); } }); console.log("----------2-----------"); alert("2"); //第一個卡點 console.log("----------2.1-----------"); function A() { console.log("------------2.2-----------"); alert("A"); //第二個卡點 } A(); console.log("---------------2.3---------------") </script> <script type="text/javascript"> console.log("----------3-----------"); </script>
alert()
會阻塞當前程序,當 js 執行到 alert()
的代碼時卡在這裏,後續代碼不會被執行,直到取消彈窗。因此,咱們能夠經過註釋上例中相對應的 alert()
來模擬異步請求的結果在何時接收到,而這個回調任務又是在哪一個時機被執行的。
好,那麼疑問來了:
假設,程序卡在 alert("2")
這裏,這時候,異步的請求結果回來了,那麼回調任務是會被接到哪一個時機執行?等我取消 alert 的彈窗後就先執行回調任務而後再繼續處理 alert("2")
後的代碼嗎?
咱們將 alert("A")
註釋掉,運行一下,測試看看:
當前程序確實卡在 alert("2")
,並且咱們等到請求結果回來了,這時,咱們把 alert 彈窗取消掉,看看日誌:
回調任務中輸出的 success 在 alert("2")
後續代碼輸出的 2.1 下面,那麼就是先繼續執行 alert("2")
後面的代碼,而後纔會執行回調任務的代碼了,那麼這個後面的代碼究竟包括哪些代碼?
好,這個時候,咱們把 alert("2")
代碼註釋掉,讓程序卡在 alert("A")
這行代碼。
假設,當前程序正在執行某個函數內的代碼,這個時候異步請求的結果回來了,那麼這個回調任務會接在這個函數執行結束後嗎?也就是,咱們如今來驗證下事件的粒度是不是以函數爲粒度?
程序確實卡在函數 A 內部的代碼 alert("A")
,輸出的日誌上也能看到如今已經輸出到 2.2,且異步請求的結果也回來了,那麼這個回調任務的代碼會在函數調用執行結束後,就被處理嗎?若是是的話,那麼日誌 2.2 接下去應該要輸出 success 纔對,若是不是,那麼就會輸出 2.3,看看日誌:
也就是說,即便異步請求結果回來了,回調任務也不能在當前函數執行完後立馬被處理,它仍是得繼續等待,等到函數後面的代碼也執行完了,那這個後面的代碼究竟是什麼呢?也就是事件的粒度究竟是什麼呢?
咱們試過了以每行代碼爲粒度作測試,也試過了以函數爲粒度作測試,那還能以什麼做爲粒度呢?或者是以 <script> 爲粒度,只有等當前 <script> 標籤內的代碼都執行完,才輪到下個代碼段執行?
從上面兩種場景下,所獲得的日誌來看,彷佛確實也是這麼個結論,success 的日誌都是在 2.3 和 3 之間輸出,2.3 表示當前 <script> 標籤裏的最後一行代碼,而 3 表示下個 <script> 標籤內的第一行代碼。
既然這樣,咱們再來作個測試:
<script type="text/javascript"> console.log("----------1-----------"); $.ajax({ url: "https://easy-mock.com/mock/5b592c01e4e04f38c7a55958/ywb/is/version/checkVersion", data: {"key": 122}, type: "POST", success: function (data) { console.log("----------success-----------"); //何時會執行回調 }, error: function (e) { console.log("----------error-----------"); } }); /* console.log("----------2-----------"); alert("2"); //第一個卡點 console.log("----------2.1-----------"); function A() { console.log("------------2.2-----------"); alert("A"); //第二個卡點 } A(); console.log("---------------2.3---------------") */ </script> <script type="text/javascript"> console.log("----------3-----------"); alert("3"); //第三個卡點 console.log("----------3.1---------") </script>
咱們把第一個 <script> 標籤內那些用於上面兩種場景測試的代碼註釋掉,只留一個異步請求的代碼,而後在第二個 <script> 標籤內,加個 alert("3")
來模擬程序是在第一個 <script> 中發起異步請求,但直到程序運行到第二個 <script> 時,異步請求結果纔回來,這種場景下回調任務的執行時機會是在哪?
若是當程序卡在 alert("3")
,異步請求結果回來了,這時候尚未取消 alert 彈窗,或者一取消的時候,就先輸出 success,再輸出 3.1,則表示,回調任務的代碼塊是被安排到發起異步請求的這個 <script> 裏代碼都執行結束就去處理。
若是 success 是在 3.1 以後才輸出,那麼,就能夠說明,瀏覽器處理 js 代碼,是以 <script> 做爲事件粒度,放入事件循環隊列中去處理。看看日誌:
好了,如今能夠確認了,success 是在 3.1 以後才輸出的,那麼來整理下結論吧。
看到這裏的話,你必定要繼續看最後的一小節的內容,必定!
以後問了一些前端同窗,而後我基於對 Android 那邊的相似理解,我自行梳理了下面的這些結論,由於涉及底層運行機制、瀏覽器行爲的這些知識我還沒開始去看,因此下面結論不保證正確,只能說是,基於我目前的能力,針對於作實驗所獲得的現象,我梳理出一些能夠解釋得通的結論。
爲啥會想要梳理這個結論呢,是由於我碰到這麼一種場景:
<script type="text/javascript"> document.location.href = "http://www.baidu.com" //... </script>
以前有個 h5 項目中,有相似的代碼,就是知足必定條件下,須要將頁面跳轉至其餘頁面。
修改 location.href
貌似不是同步操做,我猜想應該是這行跳轉代碼會告訴瀏覽器,當前頁面準備跳轉,這時候,瀏覽器再生成一個跳轉事件,接入事件隊列中等待執行的吧。
由於,最初我覺得這是個同步操做,因此我認爲當程序執行到 document.location.href = xx
這行代碼以後,頁面就會發生跳轉,而後這行代碼下面的那些代碼都不會被執行,但最後實際運行時,卻發現,這行代碼下面的代碼也都被執行了。
後來通過測試,發現,跳轉語句這行代碼所在的 <script> 裏的代碼會被所有執行完,而後才發起頁面跳轉,下個 <script> 裏的代碼不會被執行,因此,那個時候,就有個疑惑了,在 js 中發起一個異步操做的話,這個異步工做的回調任務的執行時機到底在哪裏?
後來稍微查了相關資料,發現了個詞說 JavaScript 是單線程機制,聯想到 Android 中的主線程消息循環機制,這纔想來理一理。
臥槽,臥槽,臥槽~
不要怪我連罵粗話,這篇文章是挺早以前就寫好的了,只是一直還沒發表,待在草稿箱中。而最後這一小節,是等到我差很少要發表時才新增的內容。
爲何要罵粗話,由於我發現,我上面所梳理的結論,好像所有都是錯誤的了,但也不能說所有錯誤,我實在不想把辛辛苦苦寫好的都刪掉,也不想直接就發出來誤導大夥,因此我在最後加了這一小節,來講明狀況,大夥看這篇的結論時,看看就好,討論討論一下就好,不要太當真哈。
事情是這樣的,我一些前端同窗以爲個人理解有誤,因此嘗試將我上文中的例子在他的電腦上運行測試了下,結果大家看一下:
這是對應上文中第一個測試,即讓程序卡在 alert("2")
這裏,而後等到請求結果回來後,取消 alert 彈窗,這種場景,按照咱們上面梳理的結論,回調任務在當前 <script> 執行結束以前就被插入事件隊列中了,因此回調任務應該會在第二個 <script> 代碼以前先被處理,但我同窗的狀況倒是,回調任務等到全部 <script> 都處理完才被執行???
一臉懵逼???
而後,我懷疑是否是不一樣瀏覽器會有不一樣的行爲,因此一樣的測試步驟我在 IE 瀏覽器上測試了一下:
是否是更懵逼,明明程序卡在 alert("2")
這行代碼這裏,但異步請求回來後,回調任務竟然直接被處理了,不等當前 <script> 代碼塊執行結束就先行處理了回調任務?
最後,我讓我一些同事幫忙測試了一下,在 chrome 上測試、在 jsfiddle 上測試,測試結果,基本上所有都是我上文中梳理的結論。
只有個別狀況,行爲比較特異,對前端我纔剛入門,爲何會有這種狀況發生,有兩個猜測:
alert()
的處理不一致?總之,最後,我仍是以爲我本篇梳理出的結論比較符合大多數狀況下的解釋,固然,沒有能力保證結論是正確的,大夥當個例子看就好,後續等能力有了,搞懂了相關的原理,再來從新梳理。
最後,若是你有不一樣的見解,歡迎指點一下哈~
你們好,我是 dasu,歡迎關注個人公衆號(dasuAndroidTv),公衆號中有個人聯繫方式,歡迎有事沒事來嘮嗑一下,若是你以爲本篇內容有幫助到你,能夠轉載但記得要關注,要標明原文哦,謝謝支持~