JavaScript定時器與執行機制解析

從JS執行機制提及ajax

 

瀏覽器(或者說JS引擎)執行JS的機制是基於事件循環。瀏覽器

 

因爲JS是單線程,因此同一時間只能執行一個任務,其餘任務就得排隊,後續任務必須等到前一個任務結束才能開始執行。dom

 

爲了不由於某些長時間任務形成的無心義等待,JS引入了異步的概念,用另外一個線程來管理異步任務。異步

 

同步任務直接在主線程隊列中順序執行,而異步任務會進入另外一個任務隊列,不會阻塞主線程。等到主線程隊列空了(執行完了)的時候,就會去異步隊列查詢是否有可執行的異步任務了(異步任務一般進入異步隊列以後還要等一些條件才能執行,如ajax請求、文件讀寫),若是某個異步任務能夠執行了便加入主線程隊列,以此循環。ide

 

JS定時器函數

 

JS的定時器目前有三個:setTimeout、setInterval和setImmediate。性能

 

定時器也是一種異步任務,一般瀏覽器都有一個獨立的定時器模塊,定時器的延遲時間就由定時器模塊來管理,當某個定時器到了可執行狀態,就會被加入主線程隊列。測試

 

JS定時器很是實用,作動畫的確定都用到過,也是最經常使用的異步模型之一。動畫

 

有時候一些奇奇怪怪的問題,加一個setTimeout(fn, 0)(如下簡寫setTimeout(0))就解決了。不過,若是對定時器自己不熟悉,也會產生一些奇奇怪怪的問題。spa

 

setTimeout

 

setTimeout(fn, x)表示延遲x毫秒以後執行fn。

 

使用的時候千萬不要太相信預期,延遲的時間嚴格來講老是大於x毫秒的,至於大多少就要看當時JS的執行狀況了。

 

另外,多個定時器如不及時清除(clearTimeout),會存在干擾,使延遲時間更加捉摸不透。因此,無論定時器有沒有執行完,及時清除已經不須要的定時器是個好習慣。

 

HTML5規範規定最小延遲時間不能小於4ms,即x若是小於4,會被當作4來處理。 不過不一樣瀏覽器的實現不同,好比,Chrome能夠設置1ms,IE11/Edge是4ms。

 

setTimeout註冊的函數fn會交給瀏覽器的定時器模塊來管理,延遲時間到了就將fn加入主進程執行隊列,若是隊列前面還有沒有執行完的代碼,則又須要花一點時間等待才能執行到fn,因此實際的延遲時間會比設置的長。如在fn以前正好有一個超級大循環,那延遲時間就不是一丁點了。

 

(function testSetTimeout() {

    const label = 'setTimeout';

    console.time(label);

    setTimeout(() => {

        console.timeEnd(label);

    }, 10);

    for(let i = 0; i < 100000000; i++) {}

})();

 

結果是:setTimeout: 335.187ms,遠遠不止10ms。

 

setInterval

 

setInterval的實現機制跟setTimeout相似,只不過setInterval是重複執行的。

 

對於setInterval(fn, 100)容易產生一個誤區:並非上一次fn執行完了以後再過100ms纔開始執行下一次fn。 事實上,setInterval並無論上一次fn的執行結果,而是每隔100ms就將fn放入主線程隊列,而兩次fn之間具體間隔多久就不必定了,跟setTimeout實際延遲時間相似,和JS執行狀況有關。

 

(function testSetInterval() {

    let i = 0;

    const start = Date.now();

    const timer = setInterval(() => {

        i += 1;

        i === 5 && clearInterval(timer);

        console.log(`第${i}次開始`, Date.now() - start);

        for(let i = 0; i < 100000000; i++) {}

        console.log(`第${i}次結束`, Date.now() - start);

    }, 100);

})();

 

輸出

 

第1次開始 100

第1次結束 1089

第2次開始 1091

第2次結束 1396

第3次開始 1396

第3次結束 1701

第4次開始 1701

第4次結束 2004

第5次開始 2004

第5次結束 2307

 

可見,雖然每次fn執行時間都很長,但下一次並非等上一次執行完了再過100ms纔開始執行的,實際上早就已經等在隊列裏了。

另外能夠看出,當setInterval的回調函數執行時間超過了延遲時間,已經徹底看不出有時間間隔了。

 

若是setTimeout和setInterval都在延遲100ms以後執行,那麼誰先註冊誰就先執行回調函數。

 

setImmediate

 

這算一個比較新的定時器,目前IE11/Edge支持、Nodejs支持,Chrome不支持,其餘瀏覽器未測試。

 

從API名字來看很容易聯想到setTimeout(0),不過setImmediate應該算是setTimeout(0)的替代版。

 

在IE11/Edge中,setImmediate延遲能夠在1ms之內,而setTimeout有最低4ms的延遲,因此setImmediate比setTimeout(0)更早執行回調函數。不過在Nodejs中,二者誰先執行都有可能,緣由是Nodejs的事件循環和瀏覽器的略有差別。

 

(function testSetImmediate() {

    const label = 'setImmediate';

    console.time(label);

 

    setImmediate(() => {

        console.timeEnd(label);

    });

})();

 

Edge輸出:setImmediate: 0.555 毫秒

很明顯,setImmediate設計來是爲保證讓代碼在下一次事件循環執行,之前setTimeout(0)這種不可靠的方式能夠丟掉了。

 

其餘經常使用異步模型

 

requestAnimationFrame

 

requestAnimationFrame並非定時器,但和setTimeout很類似,在沒有requestAnimationFrame的瀏覽器通常都是用setTimeout模擬。

 

requestAnimationFrame跟屏幕刷新同步,大多數屏幕的刷新頻率都是60Hz,對應的requestAnimationFrame大概每隔16.7ms觸發一次,若是屏幕刷新頻率更高,requestAnimationFrame也會更快觸發。基於這點,在支持requestAnimationFrame的瀏覽器還使用setTimeout作動畫顯然是不明智的。

 

在不支持requestAnimationFrame的瀏覽器,若是使用setTimeout/setInterval來作動畫,最佳延遲時間也是16.7ms。 若是過小,極可能連續兩次或者屢次修改dom才一次屏幕刷新,這樣就會丟幀,動畫就會卡;若是太大,顯而易見也會有卡頓的感受。

 

有趣的是,第一次觸發requestAnimationFrame的時機在不一樣瀏覽器也存在差別,Edge中,大概16.7ms以後觸發,而Chrome則當即觸發,跟setImmediate差很少。按理說Edge的實現彷佛更符合常理。

 

 Edge輸出:requestAnimationFrame: 16.66 毫秒

Chrome輸出:requestAnimationFrame: 0.698ms

 

但相鄰兩次requestAnimationFrame的時間間隔大概都是16.7ms,這一點是一致的。固然也不是絕對的,若是頁面自己性能就比較低,相隔的時間可能會變大,這就意味着頁面達不到60fps。

 

Promise

 

Promise是很經常使用的一種異步模型,若是咱們想讓代碼在下一個事件循環執行,能夠選擇使用setTimeout(0)、setImmediate、requestAnimationFrame(Chrome)和Promise。

 

並且Promise的延遲比setImmediate更低,意味着Promise比setImmediate先執行。

 

function testSetImmediate() {

    const label = 'setImmediate';

    console.time(label);

 

    setImmediate(() => {

        console.timeEnd(label);

    });

}

 

function testPromise() {

    const label = 'Promise';

    console.time(label);

    new Promise((resolve, reject) => {

        resolve();

    }).then(() => {

        console.timeEnd(label);

    });

}

 

testSetImmediate();

testPromise();

 

Edge輸出:Promise: 0.33 毫秒 setImmediate: 1.66 毫秒

儘管setImmediate的回調函數比Promise先註冊,但仍是Promise先執行。

 

能夠確定的是,在各JS環境中,Promise都是最早執行的,setTimeout(0)、setImmediate和requestAnimationFrame順序不肯定。

 

process.nextTick

 

process.nextTick是Nodejs的API,比Promise更早執行。

 

事實上,process.nextTick是不會進入異步隊列的,而是直接在主線程隊列尾強插一個任務,雖然不會阻塞主線程,可是會阻塞異步任務的執行,若是有嵌套的process.nextTick,那異步任務就永遠沒機會被執行到了。

 

使用的時候要格外當心,除非你的代碼明確要在本次事件循環結束以前執行,不然使用setImmediate或者Promise更保險。

相關文章
相關標籤/搜索