定時器并不屬于JavaScript
雖然我們一直在JavaScript中使用定時器,但是它并不是javascript的一項功能新锈。定時器作為對象和方法的一部分,才能在瀏覽器中使用晤愧。也就是說泳梆,在非瀏覽器環(huán)境中使用JavaScript,可能定時器并不存在烈炭。比如Rhino中的定時器功能需要特定實現(xiàn)溶锭。
定時器和線程是如何工作的
2.1設置和清除定時器(setTimeout)
setTimeout 語法
var timeoutID = scope.setTimeout(function[,delay,param1,param2,...])
var timeoutID = scope.setTimeout(function[,delay])
var timeoutID = scope.setTimeout(code[,delay])
需要注意的是,IE9及更早的IE瀏覽器不支持第一語法中向函數(shù)傳遞額外參數(shù)的功能符隙。
返回值
返回值timeoutID是一個正整數(shù)趴捅,表示定時器的編號垫毙。這個值可以傳遞給clearTimeout()來取消該定時器。
注意 setTimeout()和setInterval()共用一個編號池拱绑。同一個對象上(一個window或worker)综芥,setTimeout()或setInterval()返回的定時器編號不會重復。但是不同的對象使用獨立的編號池猎拨。
看下demo膀藐。
如何讓低版本瀏覽器能夠使用符合HTML5標準的定時器?
(function() {
setTimeout(function(arg1) {
if(arg1 === 'test') {
return;
}
var __nativeST__ = window.setTimeout;
window.setTimeout = function(vCallback, nDelay) {
var aArgs = Array.prototype.slice.call(arguments, 2);
return __nativeST__(vCallback instanceof Function ? function() {
vCallback.apply(null, aArgs);
} : vCallback, nDelay);
};
}, 0, 'test');
var interval = setInterval(function(arg1) {
clearInterval(interval);
if(arg1 === 'test') {
return;
}
var __nativeSI__ = window.setInterval;
window.setInterval = function(vCallback, nDelay) {
var aArgs = Array.prototype.slice.call(arguments, 2);
return __nativeSI__(vCallback instanceof Function ? function() {
vCallback.apply(null, aArgs);
} : vCallback, nDelay);
};
}, 0, 'test');
}())
setTimeout(fn,0)真的是零延遲嗎红省?
不是额各。至少4ms延遲。
證據(jù)代碼如下:
<script>
var start = Date.now();
var i = 0;
function test() {
if(++i == 1000) {
console.log(Date.now() - start);
} else {
setTimeout(test, 0);
}
}
test();
</script>
定時器的延遲能否得到保證?
不能吧恃。下面會講虾啦。
如何寫出清理所有定時器的方法?
function clearAllTimeouts() {
var id = setTimeout(function() {}, 0);
while (id > 0) {
if (id !== gid) {
clearTimeout(id);
}
id--;
}
}
能實現(xiàn)零延遲的定時器嗎蚜枢?
能缸逃。
代碼如下:
(function() {
var timeouts = [];
var messageName = "zero-timeout-message";
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, "*");
}
function handleMessage(event) {
if(event.source == window && event.data == messageName) {
event.stopPropagation();
if(timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}
window.addEventListener("message", handleMessage, true);
window.setZeroTimeout = setZeroTimeout;
})();
setZeroTimeout的實現(xiàn)主要依靠HTML5中狂拽酷炫吊炸天的API:跨文檔消息傳輸Cross Document Messaging,這個功能實現(xiàn)非常簡單主要包括接受信息的”message”事件和發(fā)送消息的”postMessage”方法厂抽。
postMessage語法:
otherWindow.postMessage(message, targetOrigin, [transfer]);
otherWindow
其他窗口的一個引用需频,比如iframe的contentWindow屬性、執(zhí)行window.open返回的窗口對象筷凤、或者是命名過或數(shù)值索引的window.frames昭殉。
message
將要發(fā)送到其他 window的數(shù)據(jù)。
targetOrigin通過窗口的origin屬性來指定哪些窗口能接收到消息事件藐守,其值可以是字符串"*"(表示無限制)或者一個URI
監(jiān)聽派遣的message:
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event){
}
event 的屬性有:
data-從其他 window 中傳遞過來的對象挪丢。
origin-調用 postMessage 時消息發(fā)送方窗口的 origin . 這個字符串由 協(xié)議、“://“卢厂、域名乾蓬、“ : 端口號”拼接而成。
source-對發(fā)送消息的窗口對象的引用; 你可以使用此來在具有不同origin的兩個窗口之間建立雙向通信慎恒。
2.2 timeout與interval之間的區(qū)別
先看一個例子任内,這樣更好說明setTimeout()和setInterval()之間的差異:
setTimeout(function repeatMe() {
/*假設這里有一段很長很長的代碼塊*/
setTimeout(repeatMe, 10);
}, 10);
setInterval(function() {
/*假設這里有一段很長很長的代碼塊*/
}, 10);
2.3 執(zhí)行線程中的定時器執(zhí)行
在web worker 出現(xiàn)之前,瀏覽器中所有的JavaScript都在單線程中執(zhí)行的融柬。因此死嗦,異步事件的處理程序,如用戶界面事件和定時器在線程中沒有代碼執(zhí)行的時候才進行執(zhí)行粒氧。這就是說越除,處理程序在執(zhí)行時必須進行排隊執(zhí)行,并且一個處理程序并不能中斷另一個處理程序的執(zhí)行。
下面先看一個例子:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
大家不妨先思考一下上面代碼執(zhí)行的結果是什么摘盆。
任務隊列
單線程就意味著翼雀,所有任務需要排隊,前一個任務結束骡澈,才會執(zhí)行后一個任務锅纺。如果前一個任務耗時很長,后一個任務就不得不一直等著肋殴。
所有任務可以分成兩種囤锉,一種是同步任務(synchronous),另一種是異步任務(asynchronous)护锤。同步任務指的是官地,在主線程上排隊執(zhí)行的任務,只有前一個任務執(zhí)行完畢烙懦,才能執(zhí)行后一個任務驱入;異步任務指的是,不進入主線程氯析、而進入"任務隊列"(task queue)的任務亏较,只有"任務隊列"通知主線程,某個異步任務可以執(zhí)行了掩缓,該任務才會進入主線程執(zhí)行雪情。
具體來說,異步執(zhí)行的運行機制如下你辣。(同步執(zhí)行也是如此巡通,因為它可以被視為沒有異步任務的異步執(zhí)行。)
(1)所有同步任務都在主線程上執(zhí)行舍哄,形成一個執(zhí)行棧(execution context stack)宴凉。
(2)主線程之外,還存在一個"任務隊列"(task queue)表悬。只要異步任務有了運行結果弥锄,就在"任務隊列"之中放置一個事件。
(3)一旦"執(zhí)行棧"中的所有同步任務執(zhí)行完畢蟆沫,系統(tǒng)就會讀取"任務隊列"叉讥,看看里面有哪些事件。那些對應的異步任務饥追,于是結束等待狀態(tài),進入執(zhí)行棧罐盔,開始執(zhí)行但绕。
(4)主線程不斷重復上面的第三步。
只要主線程空了,就會去讀取"任務隊列"捏顺,這就是JavaScript的運行機制六孵。這個過程會不斷重復。
Event Loop
主線程從"任務隊列"中讀取事件幅骄,這個過程是循環(huán)不斷的劫窒,所以整個的這種運行機制又稱為Event Loop(事件循環(huán))。
Mircotasks
Mircotasks 通常用于安排一些事拆座,它們應該在正在執(zhí)行的代碼之后立即發(fā)生主巍,例如響應操作,或者讓操作異步執(zhí)行挪凑,以免付出一個全新 task 的代價孕索。mircotask 隊列在回調之后處理,只要沒有其它執(zhí)行當中的(mid-execution)代碼躏碳;或者在每個 task 的末尾處理搞旭。在處理 microtasks 隊列期間,新添加的 microtasks 添加到隊列的末尾并且也被執(zhí)行菇绵。 microtasks 包括process.nextTick,Promise, MutationObserver肄渗,Object.observe。
看下面的例子:
<div class="outer">
<div class="inner"></div>
</div>
有如下的 Javascript 代碼咬最,假如我點擊 div.inner 會發(fā)生什么 log 呢翎嫡?
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
new MutationObserver(function() {
console.log('mutate');
}).observe(outer, {
attributes: true
});
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
看下vue.js的nextTick的實現(xiàn)
看一下setImmediate.js異步的實現(xiàn)
再看下es6-promise.js中,異步的實現(xiàn)丹诀。
定時器的應用
3.1. 可以調整事件的發(fā)生順序
比如: 網(wǎng)頁開發(fā)中钝的,某個事件先發(fā)生在子元素,然后冒泡到父元素铆遭,即子元素的事件回調函數(shù)硝桩,會早于父元素的事件回調函數(shù)觸發(fā)。如果枚荣,我們先讓父元素的事件回調函數(shù)先發(fā)生碗脊,就要用到setTimeout(f, 0)。
var input = document.getElementsByTagName('input[type=button]')[0];
input.onclick = function A() {
setTimeout(function B() {
input.value +=' input';
}, 0)
};
document.body.onclick = function C() {
input.value += ' body'
};
3.2 可以實現(xiàn)debounce方法
debounce(防抖動)方法橄妆,用來返回一個新函數(shù)衙伶。只有當兩次觸發(fā)之間的時間間隔大于事先設定的值,這個新函數(shù)才會運行實際的任務
該方法用于防止某個函數(shù)在短時間內被密集調用害碾。具體來說矢劲,debounce方法返回一個新版的該函數(shù),這個新版函數(shù)調用后慌随,只有在指定時間內沒有新的調用芬沉,才會執(zhí)行躺同,否則就重新計時。
function debounce(fn, delay){
var timer = null; // 聲明計時器
return function(){
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function(){
fn.apply(context, args);
}, delay);
};
}
// 用法示例
$('textarea').on('keydown', debounce(ajaxAction, 2500))
3.3 處理昂貴的計算過程
當我們在操作成千上萬個DOM元素的時候丸逸,會產(chǎn)生不響應的用戶界面蹋艺。
先來看看沒有優(yōu)化過的代碼:
<table>
<tbody></tbody>
</table>
<script>
var tbody = document.getElementsByTagName("tbody")[0];
for(var i = 0;i<100000;i++){
var tr = document.createElement('tr');
for(var t=0;t<6;t++){
var td = document.createElement("td");
td.appendChild(document.createTextNode(i+","+t));
tr.appendChild(td);
}
tbody.appendChild(tr);
}
</script>
這個例子,我們創(chuàng)建了600000個DOM節(jié)點黄刚,并使用大量的單元格來填充一個表格捎谨,這個操作非常昂貴,頁面會阻塞很久憔维。
使用定時器來優(yōu)化上面的代碼:
<table>
<tbody></tbody>
</table>
<script>
var rowCount = 100000;
var divideInto = 4;
var chunkSize = rowCount / divideInto;
var iteration = 0;
var tbody = document.getElementsByTagName("tbody")[0];
setTimeout(function generateRows(){
var base = (chunkSize)*iteration;
for(var i=0;i<chunkSize;i++){
var tr = document.createElement("tr");
for(var t=0;t<6;t++){
var td = document.createElement("td");
td.appendChild(document.createTextNode((i+base)+","+t+","+iteration));
tr.appendChild(td);
}
tbody.appendChild(tr);
}
iteration++;
if(iteration < divideInto){
setTimeout(generateRows,0);
}
},0);
</script>
頁面渲染的時間明顯快了不少涛救。
使用定時器解決了瀏覽器環(huán)境的單線程限制是多么容易的事情,而且還提供了很好的用戶體驗埋同。
3.4 中央定時器控制
使用定時器可能出現(xiàn)的問題是對大批量定時器的管理州叠。這在處理動畫時尤其重要,因為在試圖操縱大量屬性的同時凶赁,我們還需要一種方式來管理它們咧栗。
同時創(chuàng)建大量的定時器,將會在瀏覽器中增加垃圾回收任務的可能性虱肄。
在多個定時器中使用中央定時器控制致板,可以帶來很大的威力和靈活性。
什么是中央定時器控制:
- 每個頁面在同一時間只需要運行一個定時器咏窿。
- 可以根據(jù)需要暫停和恢復定時器斟或。
- 刪除回調函數(shù)的過程變得很簡單。
實現(xiàn)代碼如下:
var timers = { //聲明了一個定時器控制對象
timerID: 0, //記錄狀態(tài)
timers: [], //記錄狀態(tài)
add: function(fn) { //創(chuàng)建添加處理程序的函數(shù)
this.timers.push(fn);
},
start: function() {//創(chuàng)建開啟定時器的函數(shù)
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() {//創(chuàng)建停止定時器的函數(shù)
clearTimeout(this.timerID);
this.timerID = 0;
}
}
看看jquery中的中央定時器控制fx.tick
好了集嵌,講完了萝挤。如果有收獲的話,雙擊666根欧。
參考文檔如下:
Tasks, microtasks, queues and schedules
Concurrency model and Event Loop
setTimeout with a shorter delay
JS中的異步以及事件輪詢機制
這是個視頻
JavaScript 運行機制詳解:再談Event Loop
JavaScript參考標準教程--定時器
setImmediate.js
參考書籍:
《JavaScript Ninja》