JavaScript 提供定時執行代碼的功能,叫作定時器(timer),主要由setTimeout()
和setInterval()
這兩個函數來完成。它們向任務隊列添加定時任務。javascript
setTimeout
函數用來指定某個函數或某段代碼,在多少毫秒以後執行。它返回一個整數,表示定時器的編號,之後能夠用來取消這個定時器。java
var timerId = setTimeout(func|code, delay);
上面代碼中,setTimeout
函數接受兩個參數,第一個參數func|code
是將要推遲執行的函數名或者一段代碼,第二個參數delay
是推遲執行的毫秒數。ajax
console.log(1); setTimeout('console.log(2)',1000); console.log(3); // 1 // 3 // 2
上面代碼會先輸出1和3,而後等待1000毫秒再輸出2。注意,console.log(2)
必須以字符串的形式,做爲setTimeout
的參數。瀏覽器
若是推遲執行的是函數,就直接將函數名,做爲setTimeout
的參數。服務器
function f() { console.log(2); } setTimeout(f, 1000);
setTimeout
的第二個參數若是省略,則默認爲0。app
setTimeout(f) // 等同於 setTimeout(f, 0)
除了前兩個參數,setTimeout
還容許更多的參數。它們將依次傳入推遲執行的函數(回調函數)。函數
setTimeout(function (a,b) { console.log(a + b); }, 1000, 1, 1);
上面代碼中,setTimeout
共有4個參數。最後那兩個參數,將在1000毫秒以後回調函數執行時,做爲回調函數的參數。性能
還有一個須要注意的地方,若是回調函數是對象的方法,那麼setTimeout
使得方法內部的this
關鍵字指向全局環境,而不是定義時所在的那個對象。動畫
var x = 1; var obj = { x: 2, y: function () { console.log(this.x); } }; setTimeout(obj.y, 1000) // 1
上面代碼輸出的是1,而不是2。由於當obj.y
在1000毫秒後運行時,this
所指向的已經不是obj
了,而是全局環境。this
爲了防止出現這個問題,一種解決方法是將obj.y
放入一個函數。
var x = 1; var obj = { x: 2, y: function () { console.log(this.x); } }; setTimeout(function () { obj.y(); }, 1000); // 2
上面代碼中,obj.y
放在一個匿名函數之中,這使得obj.y
在obj
的做用域執行,而不是在全局做用域內執行,因此可以顯示正確的值。
另外一種解決方法是,使用bind
方法,將obj.y
這個方法綁定在obj
上面。
var x = 1; var obj = { x: 2, y: function () { console.log(this.x); } }; setTimeout(obj.y.bind(obj), 1000) // 2
setInterval
函數的用法與setTimeout
徹底一致,區別僅僅在於setInterval
指定某個任務每隔一段時間就執行一次,也就是無限次的定時執行。
var i = 1 var timer = setInterval(function() { console.log(2); }, 1000)
上面代碼中,每隔1000毫秒就輸出一個2,會無限運行下去,直到關閉當前窗口。
與setTimeout
同樣,除了前兩個參數,setInterval
方法還能夠接受更多的參數,它們會傳入回調函數。
下面是一個經過setInterval
方法實現網頁動畫的例子。
var div = document.getElementById('someDiv'); var opacity = 1; var fader = setInterval(function() { opacity -= 0.1; if (opacity >= 0) { div.style.opacity = opacity; } else { clearInterval(fader); } }, 100);
上面代碼每隔100毫秒,設置一次div
元素的透明度,直至其徹底透明爲止。
setInterval
的一個常見用途是實現輪詢。下面是一個輪詢 URL 的 Hash 值是否發生變化的例子。
var hash = window.location.hash; var hashWatcher = setInterval(function() { if (window.location.hash != hash) { updatePage(); } }, 1000);
setInterval
指定的是「開始執行」之間的間隔,並不考慮每次任務執行自己所消耗的時間。所以實際上,兩次執行之間的間隔會小於指定的時間。好比,setInterval
指定每 100ms 執行一次,每次執行須要 5ms,那麼第一次執行結束後95毫秒,第二次執行就會開始。若是某次執行耗時特別長,好比須要105毫秒,那麼它結束後,下一次執行就會當即開始。
爲了確保兩次執行之間有固定的間隔,能夠不用setInterval
,而是每次執行結束後,使用setTimeout
指定下一次執行的具體時間。
var i = 1; var timer = setTimeout(function f() { // ... timer = setTimeout(f, 2000); }, 2000);
上面代碼能夠確保,下一次執行老是在本次執行結束以後的2000毫秒開始。
setTimeout
和setInterval
函數,都返回一個整數值,表示計數器編號。將該整數傳入clearTimeout
和clearInterval
函數,就能夠取消對應的定時器。
var id1 = setTimeout(f, 1000); var id2 = setInterval(f, 1000); clearTimeout(id1); clearInterval(id2);
上面代碼中,回調函數f
不會再執行了,由於兩個定時器都被取消了。
setTimeout
和setInterval
返回的整數值是連續的,也就是說,第二個setTimeout
方法返回的整數值,將比第一個的整數值大1。
function f() {} setTimeout(f, 1000) // 10 setTimeout(f, 1000) // 11 setTimeout(f, 1000) // 12
上面代碼中,連續調用三次setTimeout
,返回值都比上一次大了1。
利用這一點,能夠寫一個函數,取消當前全部的setTimeout
定時器。
(function() { // 每輪事件循環檢查一次 var gid = setInterval(clearAllTimeouts, 0); function clearAllTimeouts() { var id = setTimeout(function() {}, 0); while (id > 0) { if (id !== gid) { clearTimeout(id); } id--; } } })();
上面代碼中,先調用setTimeout
,獲得一個計算器編號,而後把編號比它小的計數器所有取消。
有時,咱們不但願回調函數被頻繁調用。好比,用戶填入網頁輸入框的內容,但願經過 Ajax 方法傳回服務器,jQuery 的寫法以下。
$('textarea').on('keydown', ajaxAction);
這樣寫有一個很大的缺點,就是若是用戶連續擊鍵,就會連續觸發keydown
事件,形成大量的 Ajax 通訊。這是沒必要要的,並且極可能產生性能問題。正確的作法應該是,設置一個門檻值,表示兩次 Ajax 通訊的最小間隔時間。若是在間隔時間內,發生新的keydown
事件,則不觸發 Ajax 通訊,而且從新開始計時。若是過了指定時間,沒有發生新的keydown
事件,再將數據發送出去。
這種作法叫作 debounce(防抖動)。假定兩次 Ajax 通訊的間隔不得小於2500毫秒,上面的代碼能夠改寫成下面這樣。
$('textarea').on('keydown', debounce(ajaxAction, 2500)); 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); }; }
上面代碼中,只要在2500毫秒以內,用戶再次擊鍵,就會取消上一次的定時器,而後再新建一個定時器。這樣就保證了回調函數之間的調用間隔,至少是2500毫秒。
setTimeout
和setInterval
的運行機制,是將指定的代碼移出本輪事件循環,等到下一輪事件循環,再檢查是否到了指定時間。若是到了,就執行對應的代碼;若是不到,就繼續等待。
這意味着,setTimeout
和setInterval
指定的回調函數,必須等到本輪事件循環的全部同步任務都執行完,纔會開始執行。因爲前面的任務到底須要多少時間執行完,是不肯定的,因此沒有辦法保證,setTimeout
和setInterval
指定的任務,必定會按照預約時間執行。
setTimeout(someTask, 100); veryLongTask();
上面代碼的setTimeout
,指定100毫秒之後運行一個任務。可是,若是後面的veryLongTask
函數(同步任務)運行時間很是長,過了100毫秒還沒法結束,那麼被推遲運行的someTask
就只有等着,等到veryLongTask
運行結束,才輪到它執行。
再看一個setInterval
的例子。
setInterval(function () { console.log(2); }, 1000); sleep(3000); function sleep(ms) { var start = Date.now(); while ((Date.now() - start) < ms) { } }
上面代碼中,setInterval
要求每隔1000毫秒,就輸出一個2。可是,緊接着的sleep
語句須要3000毫秒才能完成,那麼setInterval
就必須推遲到3000毫秒以後纔開始生效。注意,生效後setInterval
不會產生累積效應,即不會一會兒輸出三個2,而是隻會輸出一個2。
setTimeout
的做用是將代碼推遲到指定時間執行,若是指定時間爲0
,即setTimeout(f, 0)
,那麼會馬上執行嗎?
答案是不會。由於上一節說過,必需要等到當前腳本的同步任務,所有處理完之後,纔會執行setTimeout
指定的回調函數f
。也就是說,setTimeout(f, 0)
會在下一輪事件循環一開始就執行。
setTimeout(function () { console.log(1); }, 0); console.log(2); // 2 // 1
上面代碼先輸出2
,再輸出1
。由於2
是同步任務,在本輪事件循環執行,而1
是下一輪事件循環執行。
總之,setTimeout(f, 0)
這種寫法的目的是,儘量早地執行f
,可是並不能保證馬上就執行f
。
實際上,setTimeout(f, 0)
不會真的在0毫秒以後運行,不一樣的瀏覽器有不一樣的實現。以 Edge 瀏覽器爲例,會等到4毫秒以後運行。若是電腦正在使用電池供電,會等到16毫秒以後運行;若是網頁不在當前 Tab 頁,會推遲到1000毫秒(1秒)以後運行。這樣是爲了節省系統資源。
setTimeout(f, 0)
有幾個很是重要的用途。它的一大應用是,能夠調整事件的發生順序。好比,網頁開發中,某個事件先發生在子元素,而後冒泡到父元素,即子元素的事件回調函數,會早於父元素的事件回調函數觸發。若是,想讓父元素的事件回調函數先發生,就要用到setTimeout(f, 0)
。
// HTML 代碼以下 // <input type="button" id="myButton" value="click"> var input = document.getElementById('myButton'); input.onclick = function A() { setTimeout(function B() { input.value +=' input'; }, 0) }; document.body.onclick = function C() { input.value += ' body' };
上面代碼在點擊按鈕後,先觸發回調函數A
,而後觸發函數C
。函數A
中,setTimeout
將函數B
推遲到下一輪事件循環執行,這樣就起到了,先觸發父元素的回調函數C
的目的了。
另外一個應用是,用戶自定義的回調函數,一般在瀏覽器的默認動做以前觸發。好比,用戶在輸入框輸入文本,keypress
事件會在瀏覽器接收文本以前觸發。所以,下面的回調函數是達不到目的的。
// HTML 代碼以下 // <input type="text" id="input-box"> document.getElementById('input-box').onkeypress = function (event) { this.value = this.value.toUpperCase(); }
上面代碼想在用戶每次輸入文本後,當即將字符轉爲大寫。可是實際上,它只能將本次輸入前的字符轉爲大寫,由於瀏覽器此時還沒接收到新的文本,因此this.value
取不到最新輸入的那個字符。只有用setTimeout
改寫,上面的代碼才能發揮做用。
document.getElementById('input-box').onkeypress = function() { var self = this; setTimeout(function() { self.value = self.value.toUpperCase(); }, 0); }
上面代碼將代碼放入setTimeout
之中,就能使得它在瀏覽器接收到文本以後觸發。
因爲setTimeout(f, 0)
實際上意味着,將任務放到瀏覽器最先可得的空閒時段執行,因此那些計算量大、耗時長的任務,經常會被放到幾個小部分,分別放到setTimeout(f, 0)
裏面執行。
var div = document.getElementsByTagName('div')[0]; // 寫法一 for (var i = 0xA00000; i < 0xFFFFFF; i++) { div.style.backgroundColor = '#' + i.toString(16); } // 寫法二 var timer; var i=0x100000; function func() { timer = setTimeout(func, 0); div.style.backgroundColor = '#' + i.toString(16); if (i++ == 0xFFFFFF) clearTimeout(timer); } timer = setTimeout(func, 0);
上面代碼有兩種寫法,都是改變一個網頁元素的背景色。寫法一會形成瀏覽器「堵塞」,由於 JavaScript 執行速度遠高於 DOM,會形成大量 DOM 操做「堆積」,而寫法二就不會,這就是setTimeout(f, 0)
的好處。
另外一個使用這種技巧的例子是代碼高亮的處理。若是代碼塊很大,一次性處理,可能會對性能形成很大的壓力,那麼將其分紅一個個小塊,一次處理一塊,好比寫成setTimeout(highlightNext, 50)
的樣子,性能壓力就會減輕。