本文將詳細介紹javascript中的事件循環event-loopjavascript
javascript是單線程的語言,也就是說,同一個時間只能作一件事。而這個單線程的特性,與它的用途有關,做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?java
爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質數組
【排隊】瀏覽器
單線程就意味着,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就不得不一直等着網絡
var i, t = Date.now()
for (i = 0; i < 100000000; i++) {} console.log(Date.now() - t) // 238
像上面這樣,若是排隊是由於計算量大,CPU忙不過來,倒也算了數據結構
可是,若是是網絡請求就不合適。由於一個網絡請求的資源何時返回是不可預知的,這種狀況再排隊等待就不明智了app
因而,任務分爲同步任務和異步任務異步
【同步】函數
若是在函數返回的時候,調用者就可以獲得預期結果(即拿到了預期的返回值或者看到了預期的效果),那麼這個函數就是同步的oop
Math.sqrt(2); console.log('Hi');
第一個函數返回時,就拿到了預期的返回值:2的平方根;第二個函數返回時,就看到了預期的效果:在控制檯打印了一個字符串
因此這兩個函數都是同步的
【異步】
若是在函數返回的時候,調用者還不可以獲得預期結果,而是須要在未來經過必定的手段獲得,那麼這個函數就是異步的
fs.readFile('foo.txt', 'utf8', function(err, data) { console.log(data); });
在上面的代碼中,咱們但願經過fs.readFile
函數讀取文件foo.txt中的內容,並打印出來。可是在fs.readFile
函數返回時,咱們指望的結果並不會發生,而是要等到文件所有讀取完成以後。若是文件很大的話可能要很長時間
因此,fs.readFile函數是異步的
正是因爲JavaScript是單線程的,而異步容易實現非阻塞,因此在JavaScript中對於耗時的操做或者時間不肯定的操做,使用異步就成了必然的選擇
從上文能夠看出,異步函數
實際上很快就調用完成了。可是後面還有執行異步操做、通知主線程、主線程調用回調函數等不少步驟。咱們把整個過程叫作異步過程
。異步函數的調用在整個異步過程當中,只是一小部分
一個異步過程一般是這樣的:主線程發起一個異步請求,異步任務接收請求並告知主線程已收到(異步函數返回);主線程能夠繼續執行後面的代碼,同時異步操做開始執行;執行完成後通知主線程;主線程收到通知後,執行必定的動做(調用回調函數)
所以,一個異步過程包括兩個要素:註冊函數和回調函數,其中註冊函數用來發起異步過程,回調函數用來處理結果
下面的代碼中,其中的setTimeout就是異步過程的發起函數,fn是回調函數
setTimeout(fn, 1000);
有一個很重要的問題,如何纔算是異步操做執行完成呢?對於不一樣類型的異步任務,操做完成的標準不一樣
【異步類型】
通常而言,異步任務有如下三種類型
一、普通事件,如click、resize等
二、資源加載,如load、error等
三、定時器,包括setInterval、setTimeout等
下面對這三種類型分別舉例說明,下面代碼中,鼠標點擊div時,就表明任務執行完成了
div.onclick = () => { console.log('click') }
下面代碼中,XHR對象的readyState值爲4,即已經接收到所有響應數據了,表明任務執行完成
xhr.onreadystatechange = function(){ if(xhr.readyState == 4){ if(xhr.status == 200){ //實際操做 result.innerHTML += xhr.responseText; } } }
下面代碼中,過1s後,表明任務執行完成
setTimeout(() => { console.log('timeout') },1000)
對於同步任務來講,按順序執行便可;可是,對於異步任務,各任務執行的時間長短不一樣,執行完成的時間點也不一樣,主線程如何調控異步任務呢?這就用到了消息隊列
【消息隊列】
有些文章把消息隊列稱爲任務隊列,或者叫事件隊列,總之是和異步任務相關的隊列
能夠肯定的是,它是隊列這種先入先出的數據結構,和排隊是相似的,哪一個異步操做完成的早,就排在前面。不論異步操做什麼時候開始執行,只要異步操做執行完成,就能夠到消息隊列中排隊
這樣,主線程在空閒的時候,就能夠從消息隊列中獲取消息並執行
消息隊列中放的消息具體是什麼東西?消息的具體結構固然跟具體的實現有關。可是爲了簡單起見,能夠認爲:消息就是註冊異步任務時添加的回調函數。
人們把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 運行時包含了一個待處理的消息隊列。每個消息都與一個函數相關聯。當棧擁有足夠內存時,從隊列中取出一個消息進行處理。這個處理過程包含了調用與這個消息相關聯的函數(以及於是建立了一個初始堆棧幀)。當棧再次爲空的時候,也就意味着消息處理結束
下面來詳細介紹事件循環。下圖中,主線程運行的時候,產生堆和棧,棧中的代碼調用各類外部API,異步操做執行完成後,就在消息隊列中排隊。只要棧中的代碼執行完畢,主線程就會去讀取消息隊列,依次執行那些異步任務所對應的回調函數
詳細步驟以下:
一、全部同步任務都在主線程上執行,造成一個執行棧
二、主線程以外,還存在一個"消息隊列"。只要異步操做執行完成,就到消息隊列中排隊
三、一旦執行棧中的全部同步任務執行完畢,系統就會按次序讀取消息隊列中的異步任務,因而被讀取的異步任務結束等待狀態,進入執行棧,開始執行
四、主線程不斷重複上面的第三步
【循環】
從代碼執行順序的角度來看,程序最開始是按代碼順序執行代碼的,遇到同步任務,馬上執行;遇到異步任務,則只是調用異步函數發起異步請求。此時,異步任務開始執行異步操做,執行完成後到消息隊列中排隊。程序按照代碼順序執行完畢後,查詢消息隊列中是否有等待的消息。若是有,則按照次序從消息隊列中把消息放到執行棧中執行。執行完畢後,再從消息隊列中獲取消息,再執行,不斷重複。
因爲主線程不斷的重複得到消息、執行消息、再取消息、再執行。因此,這種機制被稱爲事件循環
用代碼表示大概是這樣:
while (queue.waitForMessage()) {
queue.processNextMessage();
}
若是當前沒有任何消息queue.waitForMessage
會等待同步消息到達
【事件】
爲何叫事件循環?而不叫任務循環或消息循環。究其緣由是消息隊列中的每條消息實際上都對應着一個事件
DOM操做對應的是DOM事件,資源加載操做對應的是加載事件,而定時器操做能夠看作對應一個「時間到了」的事件
下面以一個實例來解釋事件循環機制
console.log(1) div.onclick = () => {console.log('click')} console.log(2) setTimeout(() => {console.log('timeout')},1000)
一、執行第一行代碼,第一行是一個同步任務,控制檯顯示1
二、執行第二行代碼,第二行是一個異步任務,發起異步請求,能夠在任意時刻執行鼠標點擊的異步操做
三、執行第三行代碼,第三行是一個同步任務,控制檯顯示2
四、執行第四行代碼,第四行是一個異步任務,發起異步請求,1s後執行定時器任務
五、假設從執行第四行代碼的1s內,執行了鼠標點擊,則鼠標任務在消息隊列中排到首位
六、從執行第四行代碼1s後,定時器任務到消息隊列中排到第二位
七、如今同步任務已經執行完畢,則從消息隊列中按照次序把異步任務放到執行棧中執行
八、則控制檯依次顯示'click‘、'timeout'
九、過了一段時間後,又執行了一次鼠標點擊,因爲消息隊列中已經空了,則鼠標任務在消息隊列中排到首位
十、同步任務執行完畢後,再從消息隊列中按照次序把異步任務放到執行棧中執行
十一、 則控制檯顯示'click'
【異步過程】
下面以一個實例來解釋一次完整的異步過程
div.onclick = function fn(){console.log('click')}
一、主線程經過調用異步函數div.onclick發起異步請求
二、在某一時刻,執行異步操做,即鼠標點擊
三、接着,回調函數fn到消息隊列中排隊
四、主線程從消息隊列中讀取fn到執行棧中
五、而後在執行棧中執行fn裏面的代碼console.log('click')
六、因而,控制檯顯示'click'
每個消息完整的執行後,其它消息纔會被執行。這點提供了一些優秀的特性,包括每當一個函數運行時,它就不能被搶佔,而且在其餘代碼運行以前徹底運行
這個模型的一個缺點在於當一個消息須要太長時間才能完成,Web應用沒法處理用戶的交互,例如點擊或滾動
因而,對於這種狀況的常見優化是同步變異步
一個例子是建立WebQQ的QQ好友列表。列表中一般會有成百上千個好友,若是一個好友用一個節點來表示,在頁面中渲染這個列表的時候,可能要一次性往頁面中建立成百上千個節點
在短期內往頁面中大量添加DOM節點顯然也會讓瀏覽器吃不消,看到的結果每每就是瀏覽器的卡頓甚至假死。代碼以下:
var ary = []; for ( var i = 1; i <= 1000; i++ ){ ary.push( i ); // 假設 ary 裝載了 1000 個好友的數據 }; var renderFriendList = function( data ){ for ( var i = 0, l = data.length; i < l; i++ ){ var div = document.createElement( 'div' ); div.innerHTML = i; document.body.appendChild( div ); } }; renderFriendList( ary );
這個問題的解決方案之一是數組分塊技術,下面的timeChunk函數讓建立節點的工做分批進行,好比把1秒鐘建立1000個節點,改成每隔200毫秒建立8個節點
function chunk(array,process,context){ setTimeout(function(){ //取出下一個條目並處理 var item = array.shift(); process.call(context,item); //若還有條目,再設置另外一個定時器 if(array.length > 0){ setTimeout(arguments.callee,100); } },100); }
var data = [1,2,3,4,5,6,7,8,9,0]; function printValue(item){ var div = document.getElementById('myDiv'); div.innerHTML += item + '<br>'; } chunk(data.concat(),printValue);
數組分塊的重要性在於它能夠將多個項目的處理在消息隊列上分開,在每一個項目處理以後,給予其餘的異步任務的執行機會,這樣就可能避免長時間運行腳本的錯誤。一旦某個函數須要花50ms以上的時間完成,那麼最好看看可否將任務分割爲一系列可使用定時器的小任務