標籤:另一個 完成後 done 語言 發布 機制 完成 理解 狀態
運行時概念
下面的內容解釋了一個理論上的模型。現代 JavaScript 引擎著重實現和最佳化了描述的幾個語義。
可視化描述
棧
函數調用形成了一個棧幀。
function foo(b) { var a = 10; return a + b + 11;}function bar(x) { var y = 3; return foo(x * y);}console.log(bar(7));
當調用bar時,建立了第一個幀 ,幀中包含了bar的參數和局部變數。當bar調用foo時,第二個幀就被建立,並被壓到第一個幀之上,幀中包含了foo的參數和局部變數。當foo返回時,最上層的幀就被彈出棧(剩下bar函數的調用幀 )。當bar返回的時候,棧就空了。
堆
對象被分配在一個堆中,即用以表示一個大部分非結構化的記憶體地區。
隊列
一個 JavaScript 運行時包含了一個待處理的訊息佇列。每一個訊息都有一個為了處理這個訊息相關聯的函數。
在事件迴圈時,runtime (運行時)總是從最先進入隊列的一個訊息開始處理隊列中的訊息。正因如此,這個訊息就會被移出隊列,並將其作為輸入參數調用與之關聯的函數。為了使用這個函數,調用一個函數總是會為其創造一個新的棧幀( stack frame),一如既往。
函數的處理會一直進行直到執行棧再次為空白;然後事件迴圈(event loop)將會處理隊列中的下一個訊息(如果還有的話)。
事件迴圈
之所以稱為事件迴圈,是因為它經常被用於類似如下的方式來實現:
while (queue.waitForMessage()) { queue.processNextMessage();}
如果當前沒有任何訊息queue.waitForMessage 會等待同步訊息到達。
Javascript的單線程和非同步編程為什麼JavaScript是單線程?
JavaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。那麼,為什麼JavaScript不能有多個線程呢?這樣能提高效率啊。
JavaScript的單線程,與它的用途有關。作為瀏覽器指令碼語言,JavaScript的主要用途是與使用者互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很複雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上新增內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為準?
所以,為了避免複雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,將來也不會改變。
為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript指令碼建立多個線程,但是子線程完全受主線程式控制制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單線程的本質。
單線程--非同步
說到js的單線程(single threaded)和非同步(asynchronous),很多同學不禁會想,這不是自相矛盾嗎?其實,單線程和非同步確實不能同時成為一個語言的特性。js選擇了成為單線程的語言,所以它本身不可能是非同步,但js的宿主環境(比如瀏覽器,Node)是多線程的,宿主環境通過某種方式(事件驅動,下文會講)使得js具備了非同步屬性。往下看,你會發現js的機制是多麼的簡單高效!
瀏覽器--非同步
js是單線程語言,瀏覽器只分配給js一個主線程,用來執行任務(函數),但一次只能執行一個任務,這些任務形成一個任務隊列排隊等候執行,但前端的某些任務是非常耗時的,比如網路請求,定時器和事件監聽,如果讓他們和別的任務一樣,都老老實實的排隊等待執行的話,執行效率會非常的低,甚至導致頁面的假死。所以,瀏覽器為這些耗時任務開闢了另外的線程,主要包括http請求線程,瀏覽器定時觸發器,瀏覽器事件觸發線程,這些任務是非同步。說明了瀏覽器的主要線程。
圖片來自popAnt 畫得太好,忍不住引過來 (http://blog.csdn.net/kfanning/article/details/5768776)
任務隊列
剛才說到瀏覽器為網路請求這樣的非同步任務單獨開了一個線程,那麼問題來了,這些非同步任務完成後,主線程怎麼知道呢?答案就是回呼函數,整個程式是事件驅動的,每個事件都會綁定相應的回呼函數,舉個栗子,有段代碼設定了一個定時器
setTimeout(function(){ console.log(time is out);},50);
執行這段代碼的時候,瀏覽器非同步執行計時操作,當50ms到了後,會觸發定時事件,這個時候,就會把回呼函數放到任務隊列裡。整個程式就是通過這樣的一個個事件驅動起來的。
所以說,js是一直是單線程的,瀏覽器才是實現非同步那個傢伙。
主線程
js一直在做一個工作,就是從任務隊列裡提取任務,放到主線程裡執行。下面我們來進行更深一步的理解。
圖片來自Philip Roberts的演講《Help, I‘m stuck in an event-loop》非常深刻!
我們把剛才瞭解的概念和圖中做一個對應,上文中說到的瀏覽器為非同步任務單獨開闢的線程可以統一理解為WebAPIs,上文中說到的任務隊列就是callback queue,我們所說的主線程就是有虛線組成的那一部分,堆(heap)和棧(stack)共同組成了js主線程,函數的執行就是通過進棧和出棧實現的,比中有一個foo()函數,主線程把它推入棧中,在執行函數體時,發現還需要執行上面的那幾個函數,所以又把這幾個函數推入棧中,等到函數執行完,就讓函數出棧。等到stack清空時,說明一個任務已經執行完了,這時就會從callback queue中尋找下一個人任務推入棧中(這個尋找的過程,叫做event loop,因為它總是迴圈的尋找任務隊列裡是否還有任務)。
幾個容易困惑的問題
-
setTimeout(f1,0)是什麼鬼
這個語句最大的疑問是,f1是不是立刻執行?答案是不一定,因為要看主線程內的命令是否已經執行完了,如下代碼:
setTimeout(function(){console.log(1);},0);console.log(2);這段代碼的輸出結果是2,1。因為執行setTimeou後,會立即把匿名函數放到callback queue裡面等待主線程的召喚,但這個時候stack裡面並不是空的,因為還有一句console.log(2)。等到執行完console.log(2)後,才通過event loop把匿名函數放到stack裡面。所以setTimeout(f1,0)這個語句並不是沒有意義,如果f1是很耗時的任務,那就應該把任務放到callback queue裡面,等到主程式執行完後再執行。
- Ajax請求是否非同步
瞭解完上文內容,我們就知道了,ajax請求內容的時候是非同步,當請求完成後,會觸發請求完成的事件,然後把回呼函數放入callback queue,等到主線程執行該回呼函數時還是單線程的。
-
介面渲染線程是單獨開闢的線程,是不是DOM一變化,介面就立刻重新渲染?
如果DOM一變化,介面就立刻重新渲染,效率必然很低,所以瀏覽器的機制規定介面渲染線程和主線程是互斥的,主線程執行任務時,瀏覽器渲染線程處於掛起狀態。
如何利用瀏覽器的非同步機制
我們已經知道,js一直是單線程執行的,瀏覽器為幾個明顯的耗時任務單獨開闢線程解決耗時問題,但是js除了這幾個明顯的耗時問題外,可能我們自己寫的程式裡面也會有耗時的函數,這種情況怎麼處理呢?我們肯定不能自己開闢單獨的線程,但我們可以利用瀏覽器給我們開放的這幾個視窗,瀏覽器定時器線程和事件觸發線程是好利用的,網路請求線程不適合我們使用。下面我們具體看一下:
假設耗時函數是f1,f1是f2的前置任務。
-
利用定時器觸發線程
function f1(callback){setTimeout(function(){ // f1 的代碼 callback();},0);}f1(f2);
這種寫法的耦合度高。
-
利用事件觸發線程
$f1.on(‘custom‘,f2); //這裡綁定事件以jQuery寫法為例function f1(){setTimeout(function(){ // f1的代碼 $f1.trigger(‘custom‘);},0);}
這種方法通過綁定自訂事件,對方法一解耦,這樣可以通過綁定不同的事件,實現不同的回呼函數,但如果應用這種方法過多,不利於閱讀程式。
-
發布/訂閱
上面"事件",完全可以理解成"訊號"。
我們假定,存在一個"訊號中心",某個任務執行完成,就向訊號中心"發布"(publish)一個訊號,其他任務可以向訊號中心"訂閱"(subscribe)這個訊號,從而知道什麼時候自己可以開始執行。這就叫做"發布/訂閱模式"(publish-subscribe pattern),又稱"觀察者模式"(observer pattern)。
這個模式有多種實現,下面採用的是Ben Alman的Tiny Pub/Sub,這是jQuery的一個外掛程式。
首先,f2向"訊號中心"jQuery訂閱"done"訊號。
-
jQuery.subscribe("done", f2);
然後,f1進行如下改寫:
-
function f1(){ setTimeout(function () { // f1的任務代碼 jQuery.publish("done"); }, 1000); }
jQuery.publish("done")的意思是,f1執行完成後,向"訊號中心"jQuery發布"done"訊號,從而引發f2的執行。
此外,f2完成執行後,也可以取消訂閱(unsubscribe)。
-
jQuery.unsubscribe("done", f2);
-
這種方法的性質與"事件監聽"類似,但是明顯優於後者。因為我們可以通過查看"訊息中心",瞭解存在多少訊號、每個訊號有多少訂閱者,從而監控程式的運行。
非同步好處和適合的情境
- 非同步好處
我們直接通過一個例子對同步和非同步進行對比,假設有四個任務(編號為1,2,3,4),它們的執行時間都是10ms,其中任務2是任務3的前置任務,任務2需要20ms的回應時間。下面我們做下對比,你就知道怎麼實現的非阻塞I/O了。
圖片來自Soham Kaman的文章
- 適合的情境
可以看出,當我們的程式需要大量I/O操作和使用者請求時,js這個具備單線程,非同步,事件驅動多種氣質的語言是多麼應景!相比於多線程語言,它不必耗費過多的系統開銷,同時也不必把精力用於處理多線程管理,相比於同步執行的語言,宿主環境的非同步和事件驅動機制又讓它實現了非阻塞I/O,所以你應該知道它適合什麼樣的情境了吧!
參考:
https://www.cnblogs.com/woodyblog/p/6061671.html
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
Javascript的單線程和非同步編程