根據HTML 5標準,setTimeout推遲執行的時間,最少是5毫秒。如果小于這個值,會被自動增加到5ms。
每一個setTimeout在執行時,會返回一個唯一ID,把該ID保存在一個變量中,并傳入clearTimeout,可以清除定時器。
在setTimeout內部,this綁定采用默認綁定規則,也就是說,在非嚴格模式下,this會指向window;而在嚴格模式下,this指向undefined。
setTimeout不止有2個參數,第一個參數是回調函數,第二個參數是時間,第三個參數以后都是第一個回調函數的參數。
一、用setTimeout代替setInterval
由于setInterval間歇調用定時器會因為在定時器代碼未執行完畢時又向任務隊列中添加定時器代碼,導致某些間隔被跳過等問題,所以應使用setTimeout代替setInterval。
setTimeout(function myTimer() {/*** 需要執行的代碼* setTimeout會等到定時器代碼執行完畢后才會重新調用自身(遞歸),記得給匿名函數添加一個函數名,以便調用自身。*/setTimeout(myTimer, 1000); }, 1000);
這樣做的好處是,在前一個定時器執行完畢之前,不會向任務隊列中插入新的定時器代碼,可以避免任何缺失的間隔,還可以保證在下一次定時器代碼執行前,至少要等待指定的間隔,避免了連續執行。這個模式主要用于重復定時器。
// 代碼段1,間歇性輸出1到10 let num = 0; let max = 10; setTimeout(function myTimer() {num++;console.log(num);if (num === max) {return;}setTimeout(myTimer, 500); }, 500);
// 代碼段2,間歇性輸出1到10 setTimeout(function myTimer() {num++;console.log(num);if (num < max) {setTimeout(myTimer, 500);} }, 500);
二、在for循環中創建setTimeout定時器
1、根據事件循環和任務隊列的原理,定時器通常在循環結束后才會加入到任務隊列執行。
2、定時器是循環創建的。
3、定時器幾乎是同時開始計時的。
4、定時器中的回調函數屬于閉包,包含著對循環后全局變量i的引用。在塊作用域和定時器外創建一個函數作用域時,此時不會查找全局作用域。
5、定時器的第二個參數不屬于閉包的一部分,其值與循環i的值相同。
程序運行遵循同步優先,異步靠邊,回調墊底。
// 代碼段1,輸出6個5 for (var i = 0; i < 5; i++) {setTimeout(function() {console.log(i);}, 1000 * i); } console.log(i);
第1個5直接輸出,1 秒之后,輸出 5 個 5,并且每隔1s輸出一個,一共用時4s。
for循環和循環體外部的console是同步的,所以先執行for循環,再執行外部的console.log。等for循環執行完,就會給setTimeout傳參,最后執行。
JavaScript單線程如何處理回調呢?JavaScript同步的代碼是在堆棧中順序執行的,而setTimeout回調會先放到消息隊列,for循環每執行一次,就會放一個setTimeout到消息隊列排隊等候,當同步的代碼執行完了,再去調用消息隊列的回調方法。這個消息隊列執行的時間,需要等待到函數調用棧清空之后才開始執行。即所有可執行代碼執行完畢之后,才會開始執行由setTimeout定義的操作。而這些操作進入隊列的順序,則由設定的延遲時間來決定,消息隊列遵循先進先出(FIFO)原則。因此,即使我們將延遲時間設置為0,它定義的操作仍然需要等待所有代碼執行完畢后才開始執行。這里的延遲時間,并非相對于setTimeout執行這一刻,而是相對于其他代碼執行完畢這一刻。
先執行for循環,按順序放了5個setTimeout回調到消息隊列,然后for循環結束,下面還有一個同步的console,執行完console之后,堆棧中已經沒有同步的代碼了,就去消息隊列找,發現找到了5個setTimeout,注意setTimeout是有順序的。
JavaScript在把setTimeout放到消息隊列的過程中,循環的i是不會及時保存進去的,相當于你寫了一個異步的方法,但是ajax的結果還沒返回,只能等到返回之后才能傳參到異步函數中。
for循環結束之后,因為i是用var定義的,所以var是全局變量(這里沒有函數,如果有就是函數內部的變量),這個時候的i是5,從外部的console輸出結果就可以知道。那么當執行setTimeout的時候,由于全局變量的i已經是5了,所以傳入setTimeout中的每個參數都是5。很多人都會以為setTimeout里面的i是for循環過程中的i,這種理解是不對的。
for (var i = 0; i < 5; i++) {console.log(i);setTimeout(function myTimer() {console.log(i);}, i * 1000); }
立刻輸出0 1 2 3 4
間歇性輸出5個5
溫馨提示:如果在開發者工具console面板運行這段程序,你會看到不一樣的結果。
立刻輸出0 1 2 3 4
立即輸出定時器ID
間歇性輸出5個5
for (var i = 0; i < 5; i++) {setTimeout((function() {console.log(i);})(), 1000 * i); }
立即輸出0 1 2 3 4。因為setTimeout的第一個參數是函數或者字符串,而此時函數又立即執行了。因此,定時器失效,直接輸出0 1 2 3 4。
for (var i = 0; i < 5; i++) {(function() {console.log(i); })(); }
該程序也是立即輸出0 1 2 3 4。
三、如何讓程序間歇性輸出0 1 2 3 4呢?
這里有兩種思路,不過原理都相同。
思路1:ES6 let關鍵字,給setTimeout定時器外層創建一個塊作用域。
for (let i = 0; i < 5; i++) {setTimeout(function() {console.log(i);}, 1000 * i); }
思路1的另一種表達
for (var i = 0; i < 5; i++) {let j = i; //閉包的塊作用域setTimeout(function() {console.log(j);}, 1000 * j); }
思路2:IIFE,創建函數作用域以形成閉包。
Immediately Invoked Function Expression:聲明即執行的函數表達式。
for (var i = 0; i < 5; i++) {(function iife(j) { //閉包的函數作用域setTimeout(function() {console.log(j);}, 1000 * i); //這里將i換為j, 可以證明以上的想法。 })(i); }
給定時器外層創建了一個IIFE,并且傳入變量i。此時,setTimeout會形成一個閉包,記住并且可以訪問所在的詞法作用域。因此,會間歇輸出0 1 2 3 4。
實際上,函數參數,就相當于函數內部定義的局部變量,因此下面的寫法也是可以的,思路2的另一種表達。
for (var i = 0; i < 5; i++) {(function iife() {var j = i;setTimeout(function() {console.log(j);}, 1000 * i); //如果這里將i換為j, 可以證明以上的想法。 })(); }
思路3
for (var i = 0; i < 5; i++) {setTimeout(function(j) {return function(){console.log('index is ',j);} }(i), 1000 * i); //如果這里將i換為j, 可以證明以上的想法。 }
思路4
var myTimer = function (i) {setTimeout(function() {console.log(i);}, 1000); }; for (var i = 0; i < 5; i++) {myTimer(i); //這里傳過去的i值被復制了 } console.log(i);//5
代碼執行時,立即輸出5,之后每隔1秒依次立刻輸出0 1 2 3 4。
四、如何讓程序間歇性輸出0 1 2 3 4 5呢?
思路1
for (var i = 0; i < 5; i++) {(function(j) {setTimeout(function() {console.log( j);}, 1000 * j); //這里修改0~4的定時器時間 })(i); } setTimeout(function() { //這里增加定時器,超時設置為5秒 console.log(i); }, 1000 * i);
我們都知道使用Promise處理異步代碼比回調機制讓代碼可讀性更高,但是使用Promise的問題也很明顯,即如果沒有處理Promise的reject,會導致錯誤被丟進黑洞,好在新版的Chrome和Node 7.x 能對未處理的異常給出Unhandled Rejection Warning,而排查這些錯誤還需要一些特別的技巧(瀏覽器、Node.js)
思路2
const myArr = []; for (var i = 0; i < 5; i++) { // 這里i的聲明不能改成let,如果要改該怎么做?((j) => {myArr.push(new Promise((resolve) => {setTimeout(() => {console.log(new Date, j);resolve(); //這里一定要resolve,否則代碼不會按預期執行}, 1000 * j); //定時器的超時時間逐步增加 }));})(i); }Promise.all(myArr).then(() => {setTimeout(() => {console.log(new Date, i);}, 1000); // 注意這里只需要把超時設置為1秒 });
思路3
const myArr = []; //這里存放異步操作的Promise const myTimer = (i) => new Promise((resolve) => {setTimeout(() => {console.log(new Date, i);resolve();}, 1000 * i); }); // 生成全部的異步操作 for (var i = 0; i < 5; i++) {myArr.push(myTimer(i)); } // 異步操作完成之后,輸出最后的 i Promise.all(myArr).then(() => {setTimeout(() => {console.log(new Date, i);}, 1000); });
思路4:使用ES7中的async await特性
// 模擬其他語言中的sleep,實際上可以是任何異步操作。 const sleep = (timeountMS) => new Promise((resolve) => {setTimeout(resolve, timeountMS); }); (async () => { //聲明即執行的async函數表達式for (var i = 0; i < 5; i++) {await sleep(1000);console.log(new Date, i);}await sleep(1000);console.log(new Date, i); })();
五、清除定時器
function fn1(){for(var i = 0;i < 5; i++){var tc = setTimeout(function(i){console.log(i);clearTimeout(tc);},10,i);} } fn1();//0 1 2 3
解讀fn1,這個tc是定義在閉包外面的,也就是說tc并沒有被閉包保存,所以這里的tc指的是最后一個循環留下來的tc,所以最后一個4被清除了,沒有輸出。
function fn2(){for(var i = 0;i < 5; i++){var tc = setInterval(function(i,tc){console.log(i);clearInterval(tc);},10,i,tc);} } fn2();//0 1 2 3 4 4 4 4
解讀fn2,可以發現最后一個定時器沒被刪除。在瀏覽器中單步調試,在第一次循環的時候tc并沒有被賦值,所以是undefined,在第二次循環的時候,定時器其實清理的是上一個循環的定時器。所以導致每次循環都是清理上一次的定時器,而最后一次循環的定時器沒被清理,導致一直輸出4。
六、閱讀下列程序,說出運行結果順序。
let a = new Promise(function(resolve, reject) {console.log(1);setTimeout(() => console.log(2), 0);console.log(3);console.log(4);resolve(true);} ); a.then(v => {console.log(8); }); let b = new Promise(function() {console.log(5);setTimeout(() => console.log(6), 0);} ) console.log(7);
輸出結果:1?3?4?5?7?8?2?6。
程序結果分析如下:
1、a變量是一個Promise,Promise本身是同步的,Promise的then()和catch()方法是異步的,所以這里先執行a變量內部的Promise同步代碼,輸出1?3?4。(同步優先)至于setTimeout回調,先去消息隊列排隊等著吧。(回調墊底)執行resolve(true),進入then(),then是異步,下面還有同步沒執行呢,所以then也去消息隊列排隊等候吧。(異步靠邊)
2、b變量也是一個Promise,和a一樣,執行內部的同步代碼,輸出5,setTimeout滾去消息隊列排隊等候。
3、最下面同步輸出7。
4、同步的代碼執行完了,JavaScript就跑去消息隊列呼叫異步的代碼。這里只有一個異步then,所以輸出8。
5、異步執行結束,終于輪到回調啦。這里有2個回調在排隊,他們的時間都設置為0,所以不受時間影響,只跟排隊先后順序有關。這時,先輸出a里面的回調2,最后輸出b里面的回調6。
?
我們還可以稍微做一點修改,把a里面Promise的 setTimeout(() => console.log(2), 0)改成 setTimeout(() => console.log(2), 2),對,時間改成了2ms,為什么不改成1試試呢?1ms的話,瀏覽器都還沒有反應過來呢。你改成大于或等于2的數字就能看到2個setTimeout的輸出順序發生了變化。所以回調函數正常情況下是在消息隊列順序執行的,但是使用setTimeout的時候,還需要注意時間的大小也會改變它的順序。