你們好,我是wmingren,小夥伴們都知道JavaScript是單線程的語言,所謂的單線程呢就是指若是有多個任務就必須去排隊,前面任務執行完成後,後面任務再執行。到這裏咱們就產生了一個疑問,既然是單線程的,又怎麼會有異步操做呢?首先了解一下同步和異步的概念吧。
若是在函數返回結果的時候,調用者可以拿到預期的結果(就是函數計算的結果),那麼這個函數就是同步的.
console.log('hello');//執行後,得到了返回結果
若是函數是同步的,即便調用函數執行任務比較耗時,也會一致等待直到獲得執行結果。以下面的代碼:面試
function wait(){ var time = (new Date()).getTime();//獲取當前的unix時間戳 while((new Date()).getTime() - time > 5000){} console.log('5秒過去了'); } wait(); console.log('慢死了');
上面代碼中,函數wait是一個耗時程序,持續5秒,在它執行的這漫長的5秒中,下面的console.log()函數只能等待,這就是同步。ajax
若是在函數返回的時候,調用者還不能購獲得預期結果,而是未來經過必定的手段獲得(例如回調函數),這就是異步。例如ajax操做。
若是函數是異步的,發出調用以後,立刻返回,可是不會立刻返回預期結果。調用者沒必要主動等待,當被調用者獲得結果以後會經過回調函數主動通知調用者。
瞭解完同步和異步以後,咱們再來看看咱們的問題:單線程又怎麼會有異步呢?
JavaScript其實就是一門語言,說是單線程仍是多線程得結合具體運行環境。衆所周知,js的運行環境就是瀏覽器,具體由js引擎取解析和執行。下面咱們來了解下瀏覽器。
一個瀏覽器一般由如下幾個常駐的線程:瀏覽器
要注意的是渲染引擎和js引擎線程是不能同時進行的。渲染線程在執行任務的時候,js引擎線程會被掛起。由於如果在渲染頁面的時候,js處理了DOM,瀏覽器就不知道該聽誰的了
一般講到瀏覽器的時候,咱們會說到兩個引擎:渲染引擎和JS引擎。
一、 渲染引擎:Chrome/Safari/Opera用的是Webkit引擎,IE用的是Trdent引擎,FireFox用的是Gecko引擎。不一樣的引擎對同一個樣式的實現不一致,就致使瀏覽器的兼容性問題。
二、 JS引擎:js引擎能夠說是js虛擬機,負責解析js代碼的解析和執行。一般有如下步驟:
不一樣瀏覽器的js引擎也各不相同,Chrome用的是V8,FireFox用的是SpiderMonkey,Safari用的是JavaScriptCore,IE用的是Chakra。之因此說js是單線程就是由於瀏覽器運行時只開啓一個js解釋器,緣由是如有兩個線程操做DOM,瀏覽器就又暈了。多線程
JavaScript是單線程的,可是瀏覽器不是單線程的。一些I/O操做,定時器的計時和事件監聽是由其餘線程完成的。異步
由上面瀏覽器一篇的介紹能夠知道,瀏覽器中多個線程的合做完成了異步的操做,那麼異步的回調函數又是怎樣完成執行的呢?
這就須要瞭解消息隊列和事件循環了。ide
如上圖所示,左邊的棧存儲的是同步任務,就是那些能當即執行、不耗時的任務,如變量和函數的初始化、事件的綁定等等那些不須要回調函數的操做均可歸爲這一類。右邊的堆用來存儲聲明的變量、對象。下面的隊列就是消息隊列,一旦某個異步任務有了響應就會被推入隊列中。如用戶的點擊事件、瀏覽器收到服務的響應和setTimeout中待執行的事件,每一個異步任務都和回調函數相關聯。函數
JS引擎線程用來執行棧中的同步任務,當全部同步任務執行完畢後,棧被清空,而後讀取消息隊列中的一個待處理任務,並把相關回調函數壓入棧中,單線程開始執行新的同步任務。spa
JS引擎線程從消息隊列中讀取任務是不斷循環的,每次棧被清空後,都會在消息隊列中讀取新的任務,若是沒有新的任務,就會等待,直到有新的任務,這就叫事件循環。
線程上圖以AJAX異步請求爲例,發起異步任務後,由AJAX線程執行耗時的異步操做,而JS引擎線程繼續執行堆中的其餘同步任務,直到堆中的全部異步任務執行完畢。而後,從消息隊列中依次按照順序取出消息做爲一個同步任務在JS引擎線程中執行,那麼AJAX的回調函數就會在某一時刻被調用執行。unix
最後來一個經典的面試題來幫助你們理解js的同步和異步。
代碼以下:
//執行下面這段代碼,執行後,在 5s 內點擊兩下,過一段時間(>5s)後,再點擊兩下,整個過程的輸出結果是什麼? setTimeout(function(){ for(var i = 0; i < 100000000; i++){} console.log('timer a'); }, 0) for(var j = 0; j < 5; j++){ console.log(j); } setTimeout(function(){ console.log('timer b'); }, 0) function waitFiveSeconds(){ var now = (new Date()).getTime(); while(((new Date()).getTime() - now) < 5000){} console.log('finished waiting'); } document.addEventListener('click', function(){ console.log('click'); }) console.log('click begin'); waitFiveSeconds();
要想了解上述代碼的輸出結果,首先介紹下定時器。
setTimeout 的做用是在間隔必定的時間後,將回調函數插入消息隊列中,等棧中的同步任務都執行完畢後,再執行。由於棧中的同步任務也會耗時, 因此間隔的時間通常會大於等於指定的時間 。setTimeout(fn, 0) 的意思是,將回調函數fn馬上插入消息隊列,等待執行,而不是當即執行。看一個例子:
setTimeout(function() { console.log("a") }, 0) for(let i=0; i<10000; i++) {} console.log("b")
//打印結果,說明回調函數沒有當即執行,而是等待同步任務執行完成後才執行的 b a
下面來解釋一下面試題吧。先執行同步任務,for循環,而後是console.log('click begin') 最後是waitFiveSeconds函數
在同步任務執行的期間,‘timera’,‘timerb’對應的回調和click事件的回調前後入隊列。
同步任務結束後,js引擎線程空閒後會線查看是否有事件可執行,接着在處理其餘異步任務,所以會有下面的輸出:
0 1 2 3 4 click begin finished waiting 2 click //5s中兩次點擊 timer a timer b 2 click //5s後兩次點擊