JavaScript定時器及相關面試題

来源:http://www.cnblogs.com/Uncle-Keith/archive/2017/02/25/6443115.html
-Advertisement-
Play Games

在 "單線程JavaScript" 這篇文章中,在介紹JavaScript單線程的同時,也介紹了setTimeout是如何工作的。但是對於定時器的一些內容,並沒有做深入的討論。這篇文章,會詳細說說JS的兩種定時器,setTimeout和setInterval,以及它們的工作方式。同時,會談談有關se ...


單線程JavaScript這篇文章中,在介紹JavaScript單線程的同時,也介紹了setTimeout是如何工作的。但是對於定時器的一些內容,並沒有做深入的討論。這篇文章,會詳細說說JS的兩種定時器,setTimeout和setInterval,以及它們的工作方式。同時,會談談有關setTimeout的面試題。

setInterval

setInterval,也稱為間歇調用定時器,是指允許設置間歇時間來調用定時器代碼在特定的時刻執行。也就是說,setInterval會在每隔指定的時間就執行一次代碼。

setInterval屬於window對象上的私有方法,它可以接收多個參數,

第一個參數可以是一個函數,也可以是一個字元串。

第二個參數是每次執行之前需要等待的毫秒數,這裡有一個很大的誤區就是,當設定時間之後,很多人認為會立即執行定時器,其實不是。設定一個 150ms 後執行的定時器不代表到了 150ms 代碼就立刻執行,它表示代碼會在 150ms 後被加入到任務隊列中。如果在這個時間點上,主線程上的所有同步任務都執行完畢,並且任務隊列上沒有其他任務,那麼這個任務會被執行;如果主線程上的同步任務未執行完畢,且任務隊列上還存在其他非同步任務(包括時間更短的定時器),這時候就要等待以上同步任務和非同步任務執行完畢之後,這個150ms的任務才會開始執行。

第三個參數以後是指傳入函數的一些參數。其中,只有第一個參數是必須的,其他都是可選的。在預設情況下,第二個參數預設值為0。但是0毫秒實際上也是達不到的。根據HTML 5標準,setTimeout推遲執行的時間,最少是5毫秒。如果小於這個值,會被自動增加到5ms。

//let timer = setInterval(func[, delay, param1, param2, ...]);
let timer = setInterval(function(a, b) {
    console.log(a, b);
}, 1000, 1, 2);
//在執行棧為空時,每隔一秒鐘就會輸出 1, 2

//不建議這樣使用!傳遞字元串會導致性能損失
let timer = setInterval("alert('Hello world')", 1000);

調用完setInterval之後,該方法會返回一個定時器ID,主要用於取消超時調用。

關於setInterval間歇調用定時器,在MDN和《JavaScript高級程式設計(第三版)》上都是不推薦使用的,因為setInterval會帶來一些問題。所以,一般情況下,我們會使用setTimeout來代替setInterval。但作為學習,還是要理解其中的原理。

setInterval問題在於(1)某些間隔會被跳過;(2)多個定時器代碼之間的間隔可能會比預期的小。

假設,某個 onclick 事件處理程式使用 setInterval() 設置了一個 200ms 間隔的重覆定時器。如果事件處理程式花了 300ms 的時間完成,同時定時器代碼也花了差不多的時間,就會同時出現跳過間隔且連續運行定時器代碼的情況。

這個例子中的第 1 個定時器是在 205ms 處添加到隊列中的(即使任務隊列為空,0ms實際上是達不到的,因此至少為5ms),但是直到過了 300ms 處才能夠執行。當執行這個定時器代碼時,在 405ms 處又給任務隊列添加了另外一個副本。在下一個間隔,即 605ms 處,第一個定時器代碼仍在運行,同時在任務隊列中已經有了一個定時器代碼的實例。結果是,在這個時間點上的定時器代碼不會被添加到隊列中。結果在 5ms 處添加的定時器代碼結束之後,405ms 處添加的定時器代碼就立刻執行。因此,《JavaScript高級程式設計(第三版)》建議,使用超時調用(setTimeout)來模擬間歇調用(setInterval)的是一種最佳模式,原因是後一個間歇調用可能會在前一個間歇調用結束之前啟動。

setTimeout

關於setTimeout,它的語法同setInterval。

由於setInterval間歇調用定時器存在一些問題,所以一般會使用setTimeout代替setInterval,至少我本人在開發中是不會使用setInterval的..替換代碼如下。

setTimeout(function timer() {
    //需要執行的代碼
    //setTimeout會等到定時器代碼執行完畢之後才會重新調用自身(遞歸),要註意的是要給匿名函數添加一個函數名,以便調用自身。
    setTimeout(timer, 1000);
}, 1000)

這樣做的好處是,在前一個定時器執行完畢之前,不會向任務隊列中插入新的定時器代碼,因此確保不會有任何缺失的間隔。而且,它可以保證在下一次定時器代碼執行之前,至少要等待指定的間隔,避免了連續執行。這個模式主要用於重覆定時器。再看看一些實例。

let num = 0;
let max = 10;

setTimeout(function timer() {
    num++;
    console.log(num);
    if (num === max) {return}
    setTimeout(timer, 500)
}, 500);
//或者是
setTimeout(function timer() {
    num++;
    console.log(num);
    if (num < max) {setTimeout(timer, 500)}
}, 500);

綜上,由於setInterval間歇調用定時器會因為在定時器代碼未執行完畢時又向任務隊列中添加定時器代碼,導致某些間隔被跳過等問題,所以應使用setTimeout代替setInterval

有關setTimeout的面試題

關於setTimeout的面試題,主要是迴圈中使用定時器以及定時器中this的指向性問題。在setTimeout內部,this綁定採用預設綁定規則,也就是說,在非嚴格模式下,this會指向window;而在嚴格模式下,this指向undefined。詳細可參考此答案如何理解JavaScript中的this關鍵字

閉包的一些特點:

1. 基於詞法作用域的查找規則
2. 將函數作為值傳遞(將函數作為參數傳入另一個函數,或者將函數作為另一個函數的結果返回)
3. 閉包擁有更長的生命周期
4. 閉包中的this預設指向全局作用域,閉包中的this會指向全局的原因在於閉包都是在當前詞法作用域之外被調用的(在ES6之前,this綁定取決於函數的調用位置)

對於迴圈中使用定時器,問題如下,然後各種問題慢慢開拓...

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000 * i)
}
//以上代碼輸入什麼?

回答:以上代碼輸出5個5,並且每隔1s輸出一個,一共用時4s。這裡我想解釋一下為什麼會這樣子輸出。以下解釋為個人想法,僅供參考。

我們給代碼做一些調整。

for (var i = 0; i < 5; i++) {
    let timer = setTimeout(function() {}, 1000 * i)
    console.log(timer);
    //輸出1, 2, 3, 4, 5
}

控制台輸出了5個不同的定時器ID,說明在for迴圈當中,創建了5個setTimeout定時器。(此部分由博友指出,已修改,加粗字體)//定時器會迴圈創建,但是會等到同步任務(for迴圈)執行完畢,輸出0, 1, 2, 3, 4之後,主線程才會執行任務隊列上的任務(定時器),幾乎同時開始計時(for迴圈完畢的時間極短,時間可以忽略不計,因此可以將5個定時器看做是同時創建的,理解這個非常重要),但是會等到其他非同步任務完畢才會執行定時器代碼//。並且,setTimeout的第二個參數(指定多少ms將定時器推入任務隊列中),並非引用的是全局作用域的i(即迴圈結束退出時的),而是正常情況,即按照迴圈變數i的累加(因為回調函數屬於閉包,而第二個參數不屬於閉包的一部分)。因此,可以將以上代碼改寫。

setTimeout(function() {
    console.log(5);
}, 0);
setTimeout(function() {
    console.log(5);
}, 1000);
setTimeout(function() {
    console.log(5);
}, 2000);
setTimeout(function() {
    console.log(5);
}, 3000);
setTimeout(function() {
    console.log(5);
}, 4000);

這裡需要註意的是,setTimeout回調函數中的i引用的是全局作用域下的i(即迴圈結束時的i),而設定時間的i與for迴圈的變數i累加相同。

這裡,為什麼會等待for執行完畢才開始計時,給出下麵一段代碼。

for (var i = 0; i < 5; i++) {
    console.log(i);
    setTimeout(function timer() {
        console.log(i);
    }, i * 1000);
}

//依次輸出:0, 1, 2, 3, 4  接著輸出5個5   

如果有不同意見的博友,請給我留言,共同學習。

問題二:問題一的代碼如何讓其輸出0, 1, 2, 3, 4呢?

回答:這裡有兩種解決方法,不過其中的原理都相同,即給setTimeout定時器外層創建一個塊作用域,或者是創建函數作用域以形成閉包。

關於閉包,我們知道,閉包的一個特點就是基於詞法作用域的查找規則,由於此時的回調函數引用的是迴圈結束後i的值(即,此時已經查找到了全局作用域下),因此,當我們在定時器外添加函數作用域並且傳入一個記錄迴圈變數的值,就意味著我們在函數作用域就擁有了此變數i,而不用到全局作用域下查找。此時的定時器仍然是迴圈創建,並且幾乎同時開始計時,不過唯一不同的是i的引用不再指向全局作用域。

在迭代內使用 IIFE 會為每個迭代都生成一個新的作用域,使得延遲函數的回調可以將新的作用域封閉在每個迭代內部,每個迭代中都會含有一個具有正確值的變數供我們訪問。

//方法一:ES6 let關鍵字,創建塊作用域
for (let i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000 * i)
}
//以上代碼實際上是這樣的
for (var i = 0; i < 5; i++) {
    let j = i;  //閉包的塊作用域
    setTimeout(function() {
        console.log(j);
    }, 1000 * j);
}

//方法二:IIFE
for (var i = 0; i < 5; i++) {
    (function iife(j) {     //閉包的函數作用域
        setTimeout(function() {
            console.log(j);
        }, 1000 * i);   //這裡將i換為j, 可以證明以上的想法。
    })(i);
}
//實際上,函數參數,就相當於函數內部定義的局部變數,因此下麵的寫法是相同的。
for (var i = 0; i < 5; i++) {
    (function iife() {
        var j = i;
        setTimeout(function() {
            console.log(j);
        }, 1000 * i);   //如果這裡將i換為j, 可以證明以上的想法。
    })();
}

這裡簡單說明方法二使用立即執行的函數表達式的原因。

給定時器外層創建了一個IIFE,並且傳入變數i。此時,setTimeout會形成一個閉包,記住並且可以訪問所在的詞法作用域。因此,就會正常輸出1, 2, 3, 4。

問題三: 如果原問題改為如下,會輸出什麼?

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
    })();
}

問題四,代碼如下,輸出順序是什麼?

console.log(1);

setTimeout(function() {
  console.log(2);
}, 0);

$.ajax({
    url: "../index.php",  //假如上一級目錄下有php文件,並且echo '3';
    data: 'GET',
    success: function(data) {
        console.log(data);
    },      
})

new Promise(function(resolve, reject) {
    console.log(4);
    resolve();
}).then(function() {
    console.log(5);
}).then(function() {
    console.log(6);
})
console.log(7);

回答:此時的輸出順序是1, 4, 7, 5, 6, 3, 2。這裡涉及Promise對象,這道題的解釋先留著,等到介紹Promise對象時再在Pormise的相關文章中回答。

總結:

最後,就此題做出一個關於在for迴圈中創建setTimeout定時器的總結:

1. 根據事件迴圈和任務隊列的原理,定時器會在迴圈結束後才會加入到任務隊列執行。
2. 定時器是迴圈創建的。
3. 定時器幾乎是同時開始計時的。
4. 定時器中的回調函數屬於閉包,包含著對迴圈後全局變數i的引用。在塊作用域和定時器外創建一個函數作用域時,此時不會查找全局作用域。
5. 定時器的第二個參數不屬於閉包的一部分,其值與迴圈i的值相同。

參考連接

定時器

window.setTimeout

window.setInterval

單線程JavaScript

如何理解 JavaScript 中的 this 關鍵字?

深入理解javascript函數參數與閉包(一)

深入理解javascript閉包(二)

什麼是閉包?


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 想寫這篇文章很久了,今天終於有時間總結一下,一個大型的系統里,消息中間件是必不可少的,它將併發環境處理的數據非同步進行處理,有效的提高了系統的併發能力,有很多系統的瓶頸點都在於此,而消息中間件在這個時候就要登場了,它解決的問題也就是高併發的處理,將同步的阻塞變成非同步的處理! 我們工作中經常使用到的消息 ...
  • 大型網站架構是一個系列文檔,歡迎大家關註。本次分享主題:電商網站架構案例。從電商網站的需求,到單機架構,逐步演變為常用的,可供參考的分散式架構的原型。除具備功能需求外,還具備一定的高性能,高可用,可伸縮,可擴展等非功能質量需求(架構目標)。 根據實際需要,進行改造,擴展,支持千萬PV,是沒問題的。 ...
  • #user nobody; worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; ev ...
  • 下載並解壓Keepalived安裝包到兩台nginx所在的伺服器 192.168.200.1 192.168.200.2 執行編譯安裝(安裝目錄設置為 /usr/local/keepalived ) ./configure --prefix=/usr/local/keepalived && make ...
  • 前言:這是筆者學習之後自己的理解與整理。如果有錯誤或者疑問的地方,請大家指正,我會持續更新! 1. css盒模型有標準盒模型和IE盒模型,結構是:content、padding、border、margin。 css3有個box-sizing屬性,設置用哪種盒模型; box-sizing:conten ...
  • 概述 typeof操作符返回一個字元串,指示未經計算的操作數的類型。 語法 常規用法 null 問題 // 在 JavaScript 最初的實現中,JavaScript 中的值是由一個表示類型的標簽和實際數據值表示的。對象的類型標簽是0。由於 null 代表的是空指針(大多數平臺下值為0x00),因 ...
  • 前言: 今天寫一篇jQuery,發現內容太多了,那就分成兩篇寫吧……寫完jQuery基礎知識後會再寫一些jQuery實例~~ jQuery下載。jQuery是一個相容多瀏覽器的javascript庫,核心理念是write less,do more(寫得更少,做得更多),對javascript進行了封 ...
  • JavaScript-警告(alert 消息對話框) 註意: 1. 在點擊對話框"確定"按鈕前,不能進行任何其它操作。 2. 消息對話框通常可以用於調試程式。 3. alert輸出內容,可以是字元串或變數,與document.write 相似。 // JavaScript-確認(confirm 消息 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...