假如面試回答js的運行機制時,你可能說出這麼一段話:「Javascript的事件分同步任務和異步任務,遇到同步任務就放在執行棧中執行,而碰到異步任務就放到任務隊列之中,等到執行棧執行完畢以後再去執行任務隊列之中的事件。」但你能說出背後的緣由嗎?html
進程:是系統資源分配和調度的單元。一個運行着的程序就對應了一個進程。一個進程包括了運行中的程序和程序所使用到的內存和系統資源。面試
線程:線程是進程下的執行者,一個進程至少會開啓一個線程(主線程),也能夠開啓多個線程。ajax
同步和異步關注的是:消息(結果)通訊機制。瀏覽器
同步:發出調用後,在沒有獲得結果前,該調用不返回。可是一旦調用返回,就獲得返回值數據結構
異步:發出調用後,調用直接返回,沒有返回結果。但結果由回調函數給出,至於何時給出,不知道。(這個回調函數確定是在當前js執行完後才執行)多線程
阻塞和非阻塞關注的是:程序在等待調用結果時的狀態.dom
阻塞調用:調用結果返回以前,當前線程被掛起。調用線程只有在獲得結果後纔會返回。
非阻塞調用:在不能馬上獲得結果以前,該調用不會阻塞當前線程。異步
JavaScript是單線程,程序按照順序排列,前面的必須處理好,後面的纔會執行。JavaScript的設計初衷是做爲瀏覽器腳本語言,主要是簡單用戶交互、操做DOM等,因此這門語言要圍繞單線程來設計,不然出現複雜的同步問題。函數
不矛盾!!!首先記住這句話:Js執行是單線程,但瀏覽器是多線程。oop
5.1:JS的單線程
一個瀏覽器進程中只有一個JS的執行線程,同一時刻內只會有一段代碼在執行。
5.2:瀏覽器是多線程
查閱資料,有些文章也說是模塊,本文就以瀏覽器是多線程來講,它有以下常駐線程:
渲染引擎線程:負責頁面的渲染。
JS引擎線程:負責JS的解析和執行(本文說的主線程就指js引擎線程)
定時器觸發線程:處理定時事件,好比setTimeout, setInterval
事件觸發線程:處理DOM事件
異步http請求線程:處理http請求
瀏覽器是Js的使用場景,瀏覽器自己是典型的 GUI 工做線程(GUI 工做線程在絕大多數系統中都實現爲事件處理,避免阻塞交互)。故瀏覽器是事件驅動的(Event driven),瀏覽器中不少行爲是異步,會建立事件並放入任務隊列中。
因爲Javascript 是單線程,它須要藉助異步完成耗時或者時間不肯定的操做,這些操做由瀏覽器的其它線程執行,這造成了異步事件驅動。異步事件驅動每每由瀏覽器的兩個或以上常駐線程共同完成的。例如ajax異步請求是由JS執行線程和異步http請求線程,事件觸發線程共同完成的。
函數調用造成一個棧幀。
function foo(b) {
let a = 10;
return a + b + 11;
}
function bar(x) {
let y = 3;
return foo(x * y);
}
console.log(bar(7));
當調用 bar 時,建立了第一個幀 ,幀中包含了 bar 的參數和局部變量。
當 bar 調用 foo 時,第二個幀就被建立,並被壓到第一個幀之上,幀中包含了 foo 的參數和局部變量。
當 foo 返回時,最上層的幀就被彈出棧(剩下 bar 函數的調用幀 )。
當 bar 返回的時候,棧就空了。
6.2:堆
對象被分配在一個堆中,一個用以表示一個內存中大的未被組織的區域。
每個線程只有一個棧,每個程序只有一個堆。
6.3:隊列
一個 JavaScript 運行時包含了一個待處理的消息隊列。每個消息都與一個函數相關聯。
當棧爲空時,從隊列中取出一個消息進行處理。這個處理過程包含了調用與這個消息相關聯的函數。
當棧再次爲空的時候,也就意味着消息處理結束。
任務隊列是一個先進先出的數據結構,當主線程執行棧一清空,任務隊列的回調函數就自動進入主線程。任務分紅兩種:
1、同步任務:在主線程上排隊執行的任務。只有執行完當前任務,才能執行後一個任務。
2、異步任務:該任務不進入主線程、而進入任務隊列。當執行棧清空後,纔去執行任務隊列中的任務。
因爲JavaScript只能一次執行一段代碼(因爲其單線程性質),這些代碼塊中的每個都「阻止」其餘異步事件的進度。這意味着當異步事件發生時(如鼠標點擊,定時器觸發或XMLHttpRequest完成),它將排隊等待稍後執行(這種排隊實際發生的肯定會因瀏覽器到瀏覽器而異)。
1、全部同步任務都在主線程上執行,造成一個執行棧。
2、當遇到異步任務時(IO設備操做等),就在任務隊列中添加一個事件,這個事件對應着該異步任務的回調函數。
3、執行棧中的全部同步任務執行完畢,系統就會讀取任務隊列,進入執行棧,開始執行。
4、主線程不斷重複第三步。這就造成了事件循環
結論:Javascript的事件分同步任務和異步任務,遇到同步任務就放在執行棧中執行,而碰到異步任務就放到任務隊列之中,等到執行棧執行完畢以後再去執行任務隊列之中的事件。
工做線程完成一項任務,就向任務隊列中添加一個事件。這裏的完成任務是指完成操做(click、mouse、touch,ajax的數據徹底請求回來......),並不是指執行它的回調函數
a.onclick = function () {
console.log("roro")
}
如上段代碼,僅是操做了click,但並無執行回調函數打印roro
事件循環是:主線程重複從任務隊列中取消息(事件),執行對應回調函數的過程。
上圖中,主線程運行的時候,產生堆(heap)和棧(stack),棧中的代碼調用各類外部API,它們在任務隊列中加入各類事件(click,load,done)。只要執行引擎棧棧中的代碼執行完畢,主線程就會去讀取任務隊列,依次執行那些事件所對應的回調函數。
setTimeout(function () {
console.log('a');
}, 5000)
Javascript執行引擎(主線程)運行的時候,產生堆和棧。程序中代碼依次進入棧中等待執行,當調用setTimeout()方法時,在瀏覽器的定時器線程下處理延時方法,當setTimeout方法執行5秒後,到達觸發條件,方法被添加到用於回調的任務隊列。
當執行引擎的執行棧爲空,執行引擎開始輪詢檢查任務隊列是否有任務須要被執行,當檢查到已經符合執行條件的延時方法時,將延時方法console.log('a')壓入執行棧,引擎發現調用了log()方法,因而又將log()方法入棧。而後對執行棧依次出棧執行,輸出‘a’,清空執行棧,整個執行完畢。
btn.onclick = function () {
setTimeout(function () {
console.log('a')
}, 0);
}
setTimeout(fn,0)的含義是:指定某個任務在主線程的空閒時間下,儘量早地執行。它被添加進任務隊列,所以要等到同步任務和任務隊列中的前一個事件都處理完,才會執行。
1、JS的執行線程(主線程)發起異步請求,瀏覽器會開一條新的HTTP請求線程來執行請求,繼續執行棧中剩下的任務,
2、在新線程(HTTP請求線程)中,在執行請求的同時,瀏覽器會正常處理其餘任務的執行。
3、在將來的某一時刻,當數據徹底請求回來之後,事件觸發線程監視到以前發起的HTTP請求已完成,會將指定的回調函數放入任務隊列中。
4、當瀏覽器執行棧空閒時,去掃描任務隊列中的回調函數,依次壓入執行棧中處理。
因此:ajax請求是異步。由瀏覽器新開一個線程請求,事件回調的時候放入Event loop任務隊列等候處理。詳細的例子,能夠參考MDN文檔對ajax的描述:同步和異步
這裏順帶提一下:事件循環雖然涉及到相似隊列的結構,並非採用棧的方式處理任務。事件循環做爲一個進程被劃分爲多個階段,每一個階段處理一些特定任務,各階段輪詢調度。這些階段能夠是定時器處理,dom事件處理,ajax異步處理......
JavaScript引擎只有一個線程,強制異步事件排隊等待執行,Javascript語言的事件循環,是瀏覽器的處理和行爲。另外,本文是我我的的學習筆記,通篇結合我的的理解,在某些地方表述不嚴謹,若有錯誤,但願指出。