從JS執(zhí)行機制說起
瀏覽器(或者說JS引擎)執(zhí)行JS的機制是基于事件循環(huán)。
由于JS是單線程,所以同一時間只能執(zhí)行一個任務(wù),其他任務(wù)就得排隊,后續(xù)任務(wù)必須等到前一個任務(wù)結(jié)束才能開始執(zhí)行。
為了避免因為某些長時間任務(wù)造成的無意義等待,JS引入了異步的概念,用另一個線程來管理異步任務(wù)。
同步任務(wù)直接在主線程隊列中順序執(zhí)行,而異步任務(wù)會進入另一個任務(wù)隊列,不會阻塞主線程。等到主線程隊列空了(執(zhí)行完了)的時候,就會去異步隊列查詢是否有可執(zhí)行的異步任務(wù)了(異步任務(wù)通常進入異步隊列之后還要等一些條件才能執(zhí)行,如ajax請求、文件讀寫),如果某個異步任務(wù)可以執(zhí)行了便加入主線程隊列,以此循環(huán)。
JS定時器
JS的定時器目前有三個:setTimeout、setInterval和setImmediate。
定時器也是一種異步任務(wù),通常瀏覽器都有一個獨立的定時器模塊,定時器的延遲時間就由定時器模塊來管理,當某個定時器到了可執(zhí)行狀態(tài),就會被加入主線程隊列。
JS定時器非常實用,做動畫的肯定都用到過,也是最常用的異步模型之一。
有時候一些奇奇怪怪的問題,加一個setTimeout(fn, 0)(以下簡寫setTimeout(0))就解決了。不過,如果對定時器本身不熟悉,也會產(chǎn)生一些奇奇怪怪的問題。
setTimeout
setTimeout(fn, x)表示延遲x毫秒之后執(zhí)行fn。
使用的時候千萬不要太相信預(yù)期,延遲的時間嚴格來說總是大于x毫秒的,至于大多少就要看當時JS的執(zhí)行情況了。
另外,多個定時器如不及時清除(clearTimeout),會存在干擾,使延遲時間更加捉摸不透。所以,不管定時器有沒有執(zhí)行完,及時清除已經(jīng)不需要的定時器是個好習慣。
HTML5規(guī)范規(guī)定最小延遲時間不能小于4ms,即x如果小于4,會被當做4來處理。 不過不同瀏覽器的實現(xiàn)不一樣,比如,Chrome可以設(shè)置1ms,IE11/Edge是4ms。
setTimeout注冊的函數(shù)fn會交給瀏覽器的定時器模塊來管理,延遲時間到了就將fn加入主進程執(zhí)行隊列,如果隊列前面還有沒有執(zhí)行完的代碼,則又需要花一點時間等待才能執(zhí)行到fn,所以實際的延遲時間會比設(shè)置的長。如在fn之前正好有一個超級大循環(huán),那延遲時間就不是一丁點了。
(function testSetTimeout() {
const label = 'setTimeout';
console.time(label);
setTimeout(() => {
console.timeEnd(label);
}, 10);
for(let i = 0; i < 100000000; i++) {}
})();
setInterval結(jié)果是:setTimeout: 335.187ms,遠遠不止10ms。
setInterval的實現(xiàn)機制跟setTimeout類似,只不過setInterval是重復(fù)執(zhí)行的。
對于setInterval(fn, 100)容易產(chǎn)生一個誤區(qū):并不是上一次fn執(zhí)行完了之后再過100ms才開始執(zhí)行下一次fn。 事實上,setInterval并不管上一次fn的執(zhí)行結(jié)果,而是每隔100ms就將fn放入主線程隊列,而兩次fn之間具體間隔多久就不一定了,跟setTimeout實際延遲時間類似,和JS執(zhí)行情況有關(guān)。
(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}次結(jié)束`, Date.now() - start);
}, 100);
})();
輸出
第1次開始 100
第1次結(jié)束 1089
第2次開始 1091
第2次結(jié)束 1396
第3次開始 1396
第3次結(jié)束 1701
第4次開始 1701
第4次結(jié)束 2004
第5次開始 2004
第5次結(jié)束 2307
另外可以看出,當setInterval的回調(diào)函數(shù)執(zhí)行時間超過了延遲時間,已經(jīng)完全看不出有時間間隔了。可見,雖然每次fn執(zhí)行時間都很長,但下一次并不是等上一次執(zhí)行完了再過100ms才開始執(zhí)行的,實際上早就已經(jīng)等在隊列里了。
如果setTimeout和setInterval都在延遲100ms之后執(zhí)行,那么誰先注冊誰就先執(zhí)行回調(diào)函數(shù)。
setImmediate
這算一個比較新的定時器,目前IE11/Edge支持、Nodejs支持,Chrome不支持,其他瀏覽器未測試。
從API名字來看很容易聯(lián)想到setTimeout(0),不過setImmediate應(yīng)該算是setTimeout(0)的替代版。
在IE11/Edge中,setImmediate延遲可以在1ms以內(nèi),而setTimeout有最低4ms的延遲,所以setImmediate比setTimeout(0)更早執(zhí)行回調(diào)函數(shù)。不過在Nodejs中,兩者誰先執(zhí)行都有可能,原因是Nodejs的事件循環(huán)和瀏覽器的略有差異。
(function testSetImmediate() {
const label = 'setImmediate';
console.time(label);
setImmediate(() => {
console.timeEnd(label);
});
})();
Edge輸出:setImmediate: 0.555 毫秒
很明顯,setImmediate設(shè)計來是為保證讓代碼在下一次事件循環(huán)執(zhí)行,以前setTimeout(0)這種不可靠的方式可以丟掉了。
其他常用異步模型
requestAnimationFrame
requestAnimationFrame并不是定時器,但和setTimeout很相似,在沒有requestAnimationFrame的瀏覽器一般都是用setTimeout模擬。
requestAnimationFrame跟屏幕刷新同步,大多數(shù)屏幕的刷新頻率都是60Hz,對應(yīng)的requestAnimationFrame大概每隔16.7ms觸發(fā)一次,如果屏幕刷新頻率更高,requestAnimationFrame也會更快觸發(fā)?;谶@點,在支持requestAnimationFrame的瀏覽器還使用setTimeout做動畫顯然是不明智的。
在不支持requestAnimationFrame的瀏覽器,如果使用setTimeout/setInterval來做動畫,最佳延遲時間也是16.7ms。 如果太小,很可能連續(xù)兩次或者多次修改dom才一次屏幕刷新,這樣就會丟幀,動畫就會卡;如果太大,顯而易見也會有卡頓的感覺。
有趣的是,第一次觸發(fā)requestAnimationFrame的時機在不同瀏覽器也存在差異,Edge中,大概16.7ms之后觸發(fā),而Chrome則立即觸發(fā),跟setImmediate差不多。按理說Edge的實現(xiàn)似乎更符合常理。
(function testRequestAnimationFrame() {
const label = 'requestAnimationFrame';
console.time(label);
requestAnimationFrame(() => {
console.timeEnd(label);
});
})();
Edge輸出:requestAnimationFrame: 16.66 毫秒
Chrome輸出:requestAnimationFrame: 0.698ms
但相鄰兩次requestAnimationFrame的時間間隔大概都是16.7ms,這一點是一致的。當然也不是絕對的,如果頁面本身性能就比較低,相隔的時間可能會變大,這就意味著頁面達不到60fps。
Promise
Promise是很常用的一種異步模型,如果我們想讓代碼在下一個事件循環(huán)執(zhí)行,可以選擇使用setTimeout(0)、setImmediate、requestAnimationFrame(Chrome)和Promise。
而且Promise的延遲比setImmediate更低,意味著Promise比setImmediate先執(zhí)行。
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的回調(diào)函數(shù)比Promise先注冊,但還是Promise先執(zhí)行。
可以肯定的是,在各JS環(huán)境中,Promise都是最先執(zhí)行的,setTimeout(0)、setImmediate和requestAnimationFrame順序不確定。
process.nextTick
process.nextTick是Nodejs的API,比Promise更早執(zhí)行。
事實上,process.nextTick是不會進入異步隊列的,而是直接在主線程隊列尾強插一個任務(wù),雖然不會阻塞主線程,但是會阻塞異步任務(wù)的執(zhí)行,如果有嵌套的process.nextTick,那異步任務(wù)就永遠沒機會被執(zhí)行到了。
使用的時候要格外小心,除非你的代碼明確要在本次事件循環(huán)結(jié)束之前執(zhí)行,否則使用setImmediate或者Promise更保險。
免費學習課堂
- 免費推廣知識
- 競價推廣知識
- 新媒體營銷知識
- 網(wǎng)站運營知識
- 網(wǎng)站設(shè)計知識
- 網(wǎng)站建設(shè)知識
- Web前端知識
- 軟文營銷知識
- 網(wǎng)站策劃知識
- 整合營銷
推薦文章
JavaScript定時器與執(zhí)行機制解析
來源:北京匯仁智杰科技有限公司 時間:2016-05-25 點擊: 次
推薦文章
- 織夢dedecms漏洞修復(fù)大全含任意文件2016-09-05
- 整頓微信公眾號過度營銷 對嚴重違2016-01-26
- SEO優(yōu)化過程要避免什么?2016-01-26
- 網(wǎng)站空間被掛馬的原因原因及解決2016-01-26
- 2016企業(yè)該如何運用互聯(lián)網(wǎng)進行營銷2016-01-26
- WEB前端項目開發(fā)中需注意的細節(jié)2016-01-26
- 低價網(wǎng)站建設(shè)的危害有哪些?2016-01-15
- 如何詳細的分析你網(wǎng)站的競爭對手2015-06-10
- 網(wǎng)站設(shè)計中四個常犯的錯誤2015-01-22
- 如何搭配網(wǎng)站設(shè)計中的色彩?2015-01-22