在談js定時器以前,我覺得有必要了解下javascript的事件運(yùn)行機(jī)制,簡稱(javascript event loop)。眾所周知,javascript是單線程,就是說一次只能完成一個任務(wù),多個任務(wù)的執(zhí)行需要排隊。前一個任務(wù)結(jié)束,才能執(zhí)行后一個任務(wù),如果前一個任務(wù)耗時很長,后面的任務(wù)就處于掛起狀態(tài),不得不等到前一個任務(wù)完成。
js任務(wù)分為同步和異步任務(wù)。在了解同步和異步之前,需要理解瀏覽器的并發(fā)模型:
左邊的棧存儲的是同步任務(wù),所謂同步的任務(wù)就是那些能立即執(zhí)行的任務(wù),如變量和初始化、事件綁定等等直接聲明調(diào)用的操作。右邊的堆(heap)用來存儲聲明的對象。下面的就是任務(wù)隊列。一個某個異步任務(wù)有了響應(yīng)(觸發(fā)),就會被推入隊列中。如用戶的點擊事件、瀏覽器收到服務(wù)的響應(yīng)和之前提到的setTimeout定時器事件等等。每個異步任務(wù)都有一個回調(diào)函數(shù)。
再來說說setTimeout:
在單線程的Javascript引擎中,setTimeout()是如何運(yùn)行的呢,這里就要提到瀏覽器內(nèi)核中的事件循環(huán)模型了。簡單的講,在Javascript執(zhí)行引擎之外,有一個任務(wù)隊列,當(dāng)在代碼中調(diào)用setTimeout()方法時,注冊的延時方法會交由瀏覽器內(nèi)核其他模塊(以webkit為例,是webcore模塊)處理,當(dāng)延時方法到達(dá)觸發(fā)條件,即到達(dá)設(shè)置的延時時間時,這一延時方法被添加至任務(wù)隊列里。這一過程由瀏覽器內(nèi)核其他模塊處理,與執(zhí)行引擎主線程獨立,執(zhí)行引擎在主線程方法執(zhí)行完畢,到達(dá)空閑狀態(tài)時,會從任務(wù)隊列中順序獲取任務(wù)來執(zhí)行,這一過程是一個不斷循環(huán)的過程,稱為事件循環(huán)模型。
當(dāng)一個異步事件觸發(fā),它的回調(diào)函數(shù)先進(jìn)入事件隊列中排隊,任務(wù)隊列是一個先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),排在前面的事件,一旦執(zhí)行棧為空,優(yōu)先被主線程讀取到棧中執(zhí)行。主線程從任務(wù)隊列中讀取事件,這個過程循環(huán)不斷,所以整個的這種運(yùn)行機(jī)制又稱為Event loop。
下面有必要講下 javascript定時器的工作原理:
有必要看下原文鏈接,當(dāng)然也能看下袁鍋鍋的Javascript的定時器的工作原理
為了理解定時器的內(nèi)部工作原理,我們需要了解一個非常重要的概念:定時器設(shè)定的延時是沒有保證的。因為在所有瀏覽器中執(zhí)行的javascript單線程異步事件(比如鼠標(biāo)點擊事件和定時器)都只有在主線程有空即執(zhí)行棧為空的情況下采取執(zhí)行。通過圖片來說明:
在解釋上圖時,首先先解釋下setTimeout和setInterval的區(qū)別:
setTimeout(fun, delay):延時delay毫秒之后,啥也不管,直接將回調(diào)函數(shù)推入事件隊列
setInterval(fun, delay):延時delay毫秒之后,先看看事件隊列中是否存在還沒有執(zhí)行的回調(diào)函數(shù)(setInterval的回調(diào)函數(shù)),如果存在,就不需要再往事件隊列中加入回調(diào)函數(shù)了。
我們再來解釋上面的圖。
上圖藍(lán)色區(qū)域表示任務(wù)的執(zhí)行時間,首先是Javascript代表的同步任務(wù)。在10ms處注冊了一個setTimeout事件,并且在之后又多了一個鼠標(biāo)點擊事件,在鼠標(biāo)點擊事件后又多了個setInterval事件。
當(dāng)setTimeout相對注冊該事件過了10ms時,開始觸發(fā)事件。可是現(xiàn)在藍(lán)色區(qū)域的同步任務(wù)還未執(zhí)行完,即主線程任務(wù)未執(zhí)行完,執(zhí)行棧還不為空。則把該處的回調(diào)函數(shù)推入事件隊列。然而,事件隊列中已經(jīng)有一個點擊觸發(fā)的事件,因為他比定時器過10秒才觸發(fā)快,所以優(yōu)先進(jìn)入到隊列。
等Javascript代表的藍(lán)色區(qū)域同步任務(wù)執(zhí)行完之后,主線程便從任務(wù)隊列中取到讀到鼠標(biāo)點擊事件,開始執(zhí)行mouse click callback的藍(lán)色區(qū)域的回調(diào)函數(shù),然而在這個區(qū)域中setInterval的離注冊事件過10ms到時事件開始觸發(fā),并將回調(diào)函數(shù)推入事件隊列中。此時它的前面排著10ms setTimeout事件。等Mouse Click回調(diào)函數(shù)執(zhí)行完,執(zhí)行棧又為空,推入10ms setTimeout事件,并執(zhí)行,此時在Timer藍(lán)色區(qū)域下一個10ms setInterval事件觸發(fā),由于之前事件隊列中有一個interval事件了,則丟棄,不進(jìn)入隊列。
等Timer的回調(diào)執(zhí)行完,隨即執(zhí)行事件隊列里的第一個Interval事件,在Interval藍(lán)色執(zhí)行期間,又有個interval事件觸發(fā),則推入事件隊列,即本次Interval事件執(zhí)行完下一個直接取到執(zhí)行棧執(zhí)行,下面的setInterval事件可謂是暢通無阻了,按照每10ms執(zhí)行~
表達(dá)的不清楚,請見諒,本文也是結(jié)合了幾篇講解定時器文章做了一個總結(jié)。
參考鏈接:
javascript計時器工作原理
javascript定時器工作原理
javascript單線程異步機(jī)制
阮一峰:再談event loop