介紹
JavaScript 提供了兩個方法供我們設置一個定時器,它們分別是 setTimeout() 和 setInterval()。這兩種方法的使用方法是相同的,都接收兩個參數(shù),第一個參數(shù)是一個回調函數(shù),第二個參數(shù)是延遲的毫秒數(shù)。這就造成了一種 JavaScript 是多線程語言的假象,因為相同的功能在 Java 中稱之為 sleep() 或者 Lock.lock(),它們的作用都是堵塞當前線程,為其他線程騰出處理器資源。
實際上 JavaScript 是運行在單線程環(huán)境中的,它擁有一個事件處理隊列,所有要處理的事件都會被放置在這個隊列中排隊等待執(zhí)行。這樣的話就出現(xiàn)了一個問題,瀏覽器并不能保證我們的代碼會在指定的時間內執(zhí)行。
舉個例子來說,如果一個事件的執(zhí)行時間非常長,那么在這個時間的執(zhí)行過程中,我們點擊頁面上的任何按鈕或者其他可點擊控件,都無法得到回饋,因為我們的點擊事件正在隊列中排隊執(zhí)行。
從上面的介紹中,我們明白了一個道理,那就是不能讓一個事件處理時間過長,否則就會導致用戶無法與頁面進行體驗良好的交互。所以,現(xiàn)在有很多技巧用于處理耗時操作,比如函數(shù)節(jié)流和分塊處理,接下來我會講解這些技巧。
兩種方法的比較
setTimeout() 的作用是在指定時間內執(zhí)行一個任務。setInterval() 的作用是以指定時間周期性的執(zhí)行任務。
按理說,這兩種方法分工明確,我們應該根據(jù)自身的需要選擇使用 setTimeout() 或者 setInterval(),但是目前的最佳實踐卻是始終使用 setTimeout(),即在應該使用 setTimeout() 的時候使用 setTimeout(), 在應該使用 setInterval() 的時候用 setTimeout() 去替代。
原因就是使用 setInterval() 的時候會出現(xiàn)間隔跳過問題。比如我們設置了一個 setInterval(callback, 10),如果這個 callback 的執(zhí)行時間是 20ms,那么就會出現(xiàn)無間隔連續(xù)執(zhí)行 callback 的情況,不過 JavaScript 引擎處理的過程卻不和我們想象的一樣,如果當前事件隊列中已經(jīng)有定時器代碼實例了,它就不會再放一個相同的定時器進去。這也就導致一部分定時器會被跳過的問題。
下面是利用 setTimeout() 代替 setInterval() 的例子。
function callback() {
console.log("Hello World!");
}
setInterval(callback, 10);
// After
function callback() {
console.log("Hello World");
setTimeout(callback, 10);
}
setTimeout(callback, 10);
使用了 setTimeout() 之后,可以保證在一個定時器任務執(zhí)行之后才會再次將一個定時器插入隊列,不會有丟失間隔的問題。
高級技巧
- 分塊處理
導致腳本長時間運行的兩個主要原因就是過深過長的嵌套函數(shù)調用和包含大量處理過程的循環(huán)。對于后一個問題,我們可以對循環(huán)進行切割,分時處理,騰出時間為其他事件進行服務。
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];
function printValue(i) {
console.log(i);
}
chunk(data, printValue);
可見,在以上代碼中我們以 100ms 的間隔去執(zhí)行打印事件,這樣的話在間隔的過程中,瀏覽器就能很好的處理與用戶的交互。在執(zhí)行耗時任務時,這一技巧十分重要,因為網(wǎng)頁的明顯卡頓會讓你的用戶離你而去。
-
函數(shù)節(jié)流
因為JavaScript是在瀏覽器中執(zhí)行的,所以它的限制非常大,這也就迫使我們去考慮代碼的性能以及資源的利用問題。函數(shù)節(jié)流的思想就是,某些代碼不可以在沒有間隔的情況下連續(xù)執(zhí)行。舉個例子來說,如果用戶瘋狂的點擊頁面中的一個按鈕,頻率非常之高,這個時候就不能按照用戶點擊的次數(shù)去調用處理程序。難道用戶一秒點擊 20 次按鈕我們還要重復的執(zhí)行 20 次處理程序嗎?這是完全沒有必要的,所以我們可以采取setTimeout()讓用戶請求結束后一段時間再去執(zhí)行。var clickButton = document.getElementById("click"); function throttle(method, context) { clearTimeout(method.tId); method.tId = setTimeout(function() { method.call(context); },2000); } function print() { console.log("You click the button!"); } clickButton.onclick = function() { throttle(print); }
以上代碼就保證了在 2s 之內無論你點擊了多少次按鈕,事件處理函數(shù)只會執(zhí)行一次,當然設置 2s 只是試驗性的,在實際開發(fā)過程中,2s 可能太長了,需要改成一個較小的值,比如說 100ms 。
- 中央定時器控制
如果我們同時創(chuàng)建了大量的定時器,將會在瀏覽器中增加垃圾回收任務發(fā)生的可能性。所以為了避免我們的定時器被當做垃圾回收掉,可以使用中央定時器控制的技術。下面是中央定時器控制的一些特點:
每個頁面在同一時間只需要運行一個定時器。
可以根據(jù)需要暫停和恢復定時器。
-
刪除回調函數(shù)的過程變得簡單。
var timers = { timerId: 0, timers: [], add: function(fn) { this.timers.push(fn); }, start: function() { if(this.timerId) { return; } (function runNext() { if(timers.timers.length > 0) { for(var i = 0; i < timers.timers.length; i++) { if(timers.timers[i]() == false) { timers.timers.splice(i, 1); i--; } } timers.timerId = setTimeout(runNext,0); } })(); }, stop: function() { clearTimeout(this.timerId); this.timerId = 0; } };
上述代碼就創(chuàng)建了一個簡單的中央定時器,首先我們在 timers 中定義了 timerId 和 timers, timerId用于保存定時器的 id,用于控制定時器的開啟和關閉,timers 用于保存要執(zhí)行的函數(shù)。
最主要的還是 start 函數(shù),首先對 timerId 進行判斷,如果還沒有啟動定時器,則立即啟動一個,如果已經(jīng)有定時器啟動了,則直接返回,用于確保整個環(huán)境中只存在一個定時器實例。每次執(zhí)行定時器實例,都會執(zhí)行一次保存在 timers 隊列中的函數(shù),如果執(zhí)行的函數(shù)返回結果為 false, 就會將其從隊列中刪除,這樣下次就不會再執(zhí)行該函數(shù)了。
End!