摘要:例如處理請求的線程處理事件的線程定時器線程讀寫文件的線程例如在中等等。事件循環事件循環是指主線程重復從消息隊列中取消息執行的過程。事件觸發時,表示異步任務完成,會將事件監聽器函數封裝成一條消息放到消息隊列中,等待主線程執行。
一. 單線程
我們常說“JavaScript是單線程的”。
所謂單線程,是指在JS引擎中負責解釋和執行JavaScript代碼的線程只有一個。不妨叫它主線程。
但是實際上還存在其他的線程。例如:處理AJAX請求的線程、處理DOM事件的線程、定時器線程、讀寫文件的線程(例如在Node.js中)等等。這些線程可能存在于JS引擎之內,也可能存在于JS引擎之外,在此我們不做區分。不妨叫它們工作線程。
二. 同步和異步假設存在一個函數A:
A(args...);
同步:如果在函數A返回的時候,調用者就能夠得到預期結果(即拿到了預期的返回值或者看到了預期的效果),那么這個函數就是同步的。
例如:
Math.sqrt(2); console.log("Hi");
第一個函數返回時,就拿到了預期的返回值:2的平方根。
第二個函數返回時,就看到了預期的效果:在控制臺打印了一個字符串。
所以這兩個函數都是同步的。
異步:如果在函數A返回的時候,調用者還不能夠得到預期結果,而是需要在將來通過一定的手段得到,那么這個函數就是異步的。
例如:
fs.readFile("foo.txt", "utf8", function(err, data) { console.log(data); });
在上面的代碼中,我們希望通過fs.readFile函數讀取文件foo.txt中的內容,并打印出來。
但是在fs.readFile函數返回時,我們期望的結果并不會發生,而是要等到文件全部讀取完成之后。如果文件很大的話可能要很長時間。
下面以AJAX請求為例,來看一下同步和異步的區別:
異步AJAX:
主線程:“你好,AJAX線程。請你幫我發個HTTP請求吧,我把請求地址和參數都給你了?!?/p>
AJAX線程:“好的,主線程。我馬上去發,但可能要花點兒時間呢,你可以先去忙別的。”
主線程::“謝謝,你拿到響應后告訴我一聲啊?!?/p>
(接著,主線程做其他事情去了。一頓飯的時間后,它收到了響應到達的通知。)
同步AJAX:
主線程:“你好,AJAX線程。請你幫我發個HTTP請求吧,我把請求地址和參數都給你了?!?/p>
AJAX線程:“......”
主線程::“喂,AJAX線程,你怎么不說話?”
AJAX線程:“......”
主線程::“喂!喂喂喂!”
AJAX線程:“......”
(一炷香的時間后)
主線程::“喂!求你說句話吧!”
AJAX線程:“主線程,不好意思,我在工作的時候不能說話。你的請求已經發完了,拿到響應數據了,給你?!?/p>
正是由于JavaScript是單線程的,而異步容易實現非阻塞,所以在JavaScript中對于耗時的操作或者時間不確定的操作,使用異步就成了必然的選擇。異步是這篇文章關注的重點。
三. 異步過程的構成要素從上文可以看出,異步函數實際上很快就調用完成了。但是后面還有工作線程執行異步任務、通知主線程、主線程調用回調函數等很多步驟。我們把整個過程叫做異步過程。異步函數的調用在整個異步過程中,只是一小部分。
總結一下,一個異步過程通常是這樣的:
主線程發起一個異步請求,相應的工作線程接收請求并告知主線程已收到(異步函數返回);主線程可以繼續執行后面的代碼,同時工作線程執行異步任務;工作線程完成工作后,通知主線程;主線程收到通知后,執行一定的動作(調用回調函數)。
異步函數通常具有以下的形式:
A(args..., callbackFn)
它可以叫做異步過程的發起函數,或者叫做異步任務注冊函數。args是這個函數需要的參數。callbackFn也是這個函數的參數,但是它比較特殊所以多帶帶列出來。
所以,從主線程的角度看,一個異步過程包括下面兩個要素:
發起函數(或叫注冊函數)A
回調函數callbackFn
它們都是在主線程上調用的,其中注冊函數用來發起異步過程,回調函數用來處理結果。
舉個具體的例子:
setTimeout(fn, 1000);
其中的setTimeout就是異步過程的發起函數,fn是回調函數。
注意:前面說的形式A(args..., callbackFn)只是一種抽象的表示,并不代表回調函數一定要作為發起函數的參數,例如:
var xhr = new XMLHttpRequest(); xhr.onreadystatechange = xxx; // 添加回調函數 xhr.open("GET", url); xhr.send(); // 發起函數
發起函數和回調函數就是分離的。
四. 消息隊列和事件循環上文講到,異步過程中,工作線程在異步操作完成后需要通知主線程。那么這個通知機制是怎樣實現的呢?答案是利用消息隊列和事件循環。
用一句話概括:
工作線程將消息放到消息隊列,主線程通過事件循環過程去取消息。
消息隊列:消息隊列是一個先進先出的隊列,它里面存放著各種消息。
事件循環:事件循環是指主線程重復從消息隊列中取消息、執行的過程。
實際上,主線程只會做一件事情,就是從消息隊列里面取消息、執行消息,再取消息、再執行。當消息隊列為空時,就會等待直到消息隊列變成非空。而且主線程只有在將當前的消息執行完成后,才會去取下一個消息。這種機制就叫做事件循環機制,取一個消息并執行的過程叫做一次循環。
事件循環用代碼表示大概是這樣的:
while(true) { var message = queue.get(); execute(message); }
那么,消息隊列中放的消息具體是什么東西?消息的具體結構當然跟具體的實現有關,但是為了簡單起見,我們可以認為:
消息就是注冊異步任務時添加的回調函數。
再次以異步AJAX為例,假設存在如下的代碼:
$.ajax("http://segmentfault.com", function(resp) { console.log("我是響應:", resp); }); // 其他代碼 ... ... ...
主線程在發起AJAX請求后,會繼續執行其他代碼。AJAX線程負責請求segmentfault.com,拿到響應后,它會把響應封裝成一個JavaScript對象,然后構造一條消息:
// 消息隊列中的消息就長這個樣子 var message = function () { callbackFn(response); }
其中的callbackFn就是前面代碼中得到成功響應時的回調函數。
主線程在執行完當前循環中的所有代碼后,就會到消息隊列取出這條消息(也就是message函數),并執行它。到此為止,就完成了工作線程對主線程的通知,回調函數也就得到了執行。如果一開始主線程就沒有提供回調函數,AJAX線程在收到HTTP響應后,也就沒必要通知主線程,從而也沒必要往消息隊列放消息。
用圖表示這個過程就是:
從上文中我們也可以得到這樣一個明顯的結論,就是:
五. 異步與事件異步過程的回調函數,一定不在當前這一輪事件循環中執行。
上文中說的“事件循環”,為什么里面有個事件呢?那是因為:
消息隊列中的每條消息實際上都對應著一個事件。
上文中一直沒有提到一類很重要的異步過程:DOM事件。
舉例來說:
var button = document.getElement("#btn"); button.addEventListener("click", function(e) { console.log(); });
從事件的角度來看,上述代碼表示:在按鈕上添加了一個鼠標單擊事件的事件監聽器;當用戶點擊按鈕時,鼠標單擊事件觸發,事件監聽器函數被調用。
從異步過程的角度看,addEventListener函數就是異步過程的發起函數,事件監聽器函數就是異步過程的回調函數。事件觸發時,表示異步任務完成,會將事件監聽器函數封裝成一條消息放到消息隊列中,等待主線程執行。
事件的概念實際上并不是必須的,事件機制實際上就是異步過程的通知機制。我覺得它的存在是為了編程接口對開發者更友好。
另一方面,所有的異步過程也都可以用事件來描述。例如:setTimeout可以看成對應一個時間到了!的事件。前文的setTimeout(fn, 1000);可以看成:
timer.addEventListener("timeout", 1000, fn);六. 生產者與消費者
從生產者與消費者的角度看,異步過程是這樣的:
七. 總結一下工作線程是生產者,主線程是消費者(只有一個消費者)。工作線程執行異步任務,執行完成后把對應的回調函數封裝成一條消息放到消息隊列中;主線程不斷地從消息隊列中取消息并執行,當消息隊列空時主線程阻塞,直到消息隊列再次非空。
最后再用一個生活中的例子總結一下同步和異步:在公路上,汽車一輛接一輛,有條不紊的運行。這時,有一輛車壞掉了。假如它停在原地進行修理,那么后面的車就會被堵住沒法行駛,交通就亂套了。幸好旁邊有應急車道,可以把故障車輛推到應急車道修理,而正常的車流不會受到任何影響。等車修好了,再從應急車道回到正常車道即可。唯一的影響就是,應急車道用多了,原來的車輛之間的順序會有點亂。
這就是同步和異步的區別。同步可以保證順序一致,但是容易導致阻塞;異步可以解決阻塞問題,但是會改變順序性。改變順序性其實也沒有什么大不了的,只不過讓程序變得稍微難理解了一些 :)
PS:ECMAScript 262規范中,并沒有對異步、事件隊列等概念及其實現的描述。這些都是具體的JavaScript運行時環境使用的機制。本文重點是描述異步過程的原理,為了便于理解做了很多簡化。所以文中的某些術語的使用可能是不準確的,具體細節也未必是正確的,例如消息隊列中消息的結構。請讀者注意。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/78500.html
摘要:現實中是這樣的執行結果為結果告訴我們,是單線程沒錯,不過不是逐行同步執行。搜索了很多官方個人博客得到了一堆詞引擎主線程事件表事件隊列宏任務微任務,徹底懵逼。。。以此規則不停的執行下去就是我們所聽到的事件循環。 都知道javascript是單線程,那么問題來了,既然是單線程順序執行,那怎么做到異步的呢? 我們理解的單線程應該是這樣的,排著一個個來,是同步執行。 showImg(https...
摘要:心塞塞根據規范,事件循環是通過任務隊列的機制來進行協調的。等便是任務源,而進入任務隊列的是他們指定的具體執行任務回調函數。然后當前本輪的結束,主線程可以繼續取下一個執行。 依然是:經濟基礎決定上層建筑。 說明 首先,旨在搞清常用的同步異步執行機制 其次,暫時不討論node.js的Event Loop執行機制,以下關于瀏覽器的Event Loop執行機制 最后,借鑒了很多前輩的研究文...
摘要:想必面試題刷的多的同學對下面這道題目不陌生,能夠立即回答出輸出個,可是你真的懂為什么嗎為什么是輸出為什么是輸出個這兩個問題在我腦邊縈繞。同步任務都好理解,一個執行完執行下一個。本文只是我對這道面試題的一點思考,有誤的地方望批評指正。 想必面試題刷的多的同學對下面這道題目不陌生,能夠立即回答出輸出10個10,可是你真的懂為什么嗎?為什么是輸出10?為什么是輸出10個10?這兩個問題在我腦...
摘要:事件完成,回調函數進入。主線程從讀取回調函數并執行。終于執行完了,終于從進入了主線程執行。遇到,立即執行。宏任務微任務第三輪事件循環宏任務執行結束,執行兩個微任務和。事件循環事件循環是實現異步的一種方法,也是的執行機制。 本文的目的就是要保證你徹底弄懂javascript的執行機制,如果讀完本文還不懂,可以揍我。不論你是javascript新手還是老鳥,不論是面試求職,還是日常開發工作...
摘要:關于這部分有嚴格的文字定義,但本文的目的是用最小的學習成本徹底弄懂執行機制,所以同步和異步任務分別進入不同的執行場所,同步的進入主線程,異步的進入并注冊函數。宏任務微任務第三輪事件循環宏任務執行結束,執行兩個微任務和。 不論你是javascript新手還是老鳥,不論是面試求職,還是日常開發工作,我們經常會遇到這樣的情況:給定的幾行代碼,我們需要知道其輸出內容和順序。 因為javascr...
閱讀 1043·2021-11-15 18:11
閱讀 3167·2021-09-22 15:33
閱讀 3463·2021-09-01 11:42
閱讀 2659·2021-08-24 10:03
閱讀 3623·2021-07-29 13:50
閱讀 2927·2019-08-30 14:08
閱讀 1279·2019-08-28 17:56
閱讀 2263·2019-08-26 13:57