為什麼計時器計時不准?---瞭解計時器的底層機制

来源:https://www.cnblogs.com/freefy/archive/2018/10/28/9866960.html
-Advertisement-
Play Games

JavaScript提供定時執行代碼的功能,叫做定時器(timer),主要由setTimeout()和setInterval()這兩個函數來完成。它們向任務隊列添加定時任務。初始接觸它的人都覺得好簡單,實際上真的如此麽?這裡記載下,一路對其使用姿勢變遷的歷程。 setTimeout()基礎 setT ...


JavaScript提供定時執行代碼的功能,叫做定時器(timer),主要由setTimeout()setInterval()這兩個函數來完成。它們向任務隊列添加定時任務。初始接觸它的人都覺得好簡單,實際上真的如此麽?這裡記載下,一路對其使用姿勢變遷的歷程。

setTimeout()基礎

setTimeout()函數用來指定某個函數或某段代碼,在多少毫秒之後執行。它返回一個整數,表示定時器的編號,以後可以用來取消這個定時器。

var timerId = setTimeout(func|code, delay)

上面代碼中,setTimeout()函數接受兩個參數,第一個參數func|code是將要推遲執行的函數名或者一段代碼,第二個參數delay是推遲執行的毫秒數。

console.log(1);
setTimeout('console.log(2)',1000);
console.log(3);

上面代碼的輸出結果就是132,因為setTimeout()指定第二行語句推遲1000毫秒再執行(如果這在Sublime下運用插件以nodejs環境來執行,許解釋器不同,會報錯)。

需要註意的是,推遲執行的代碼必須以字元串的形式,放入setTimeout(),因為引擎內部使用eval()函數,將字元串轉為代碼。如果推遲執行的是函數,則可以直接將函數名,放入setTimeout()。一方面eval()函數有安全顧慮,另一方面為了便於JavaScript引擎優化代碼,setTimeout()方法一般總是採用函數名的形式,就像下麵這樣。

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

// 或者
setTimeout(function (){
    console.log(2)
},1000);

setTimeout()傳參數

除了前兩個參數,setTimeout()還允許添加更多的參數。它們將被傳入推遲執行的函數(回調函數)。

setTimeout(function(a,b){
    console.log(a+b);
},1000,1,1);

上面代碼中,setTimeout()共有4個參數。最後那兩個參數,將在1000毫秒之後回調函數執行時,作為回調函數的參數。

IE 9.0及以下版本,只允許setTimeout()有兩個參數,不支持更多的參數;可以在匿名函數中,讓回調函數帶參數運行,再把匿名函數輸入setTimeout();例如:

setTimeout(function() {
    myFunc("one", "two", "three");
}, 1000);

當然也可以使用bindapply方法來解決。

例如使用bind方法,把多餘的參數綁定在回調函數上面,生成一個新的函數輸入setTimeout()

setTimeout( function(arg1){}.bind(undefined, 10), 1000 );

上面代碼中,bind方法第一個參數是undefined,表示將原函數的this綁定全局作用域,第二個參數是要傳入原函數的參數。它運行後會返回一個新函數,該函數不帶參數。

setTimeout()註意點

setTimeout()中回調函數中的this

如果被setTimeout()推遲執行的回調函數是某個對象的方法,那麼該方法中的this關鍵字將指向全局環境,而不是定義時所在的那個對象。

var x = 1;
var o = {
    x: 2,
    y: function(){
        console.log(this.x);
    }
};
setTimeout(o.y,1000);// 1

上面代碼輸出的是1,而不是2,這表示o.ythis所指向的已經不是o,而是全局環境了。

再看一個不容易發現錯誤的例子。

function User(login) {
    this.login = login;
    this.sayHi = function() {
        console.log(this.login);
    }
}
var user = new User('John');
setTimeout(user.sayHi, 1000);

上面代碼只會顯示undefined,因為等到user.sayHi執行時,它是在全局對象中執行,所以this.login取不到值。

為了防止出現這個問題,一種解決方法是將user.sayHi放在匿名函數中執行。

setTimeout(function() {
    user.sayHi();
}, 1000);

上面代碼中,sayHi是在user作用域內執行,而不是在全局作用域內執行,所以能夠顯示正確的值。

另一種解決方法是,使用bind方法,將綁定sayHi綁定在user上面。

setTimeout(user.sayHi.bind(user), 1000);

HTML5標準規定,setTimeout()的最短時間間隔是4毫秒。為了節電,對於那些不處於當前視窗的頁面,瀏覽器會將時間間隔擴大到1000毫秒。另外,如果筆記本電腦處於電池供電狀態,Chrome和IE 9以上的版本,會將時間間隔切換到系統定時器,大約是15.6毫秒。

setTimeout()執行回調間隔時間長度

如果你在一段代碼中發現下麵內容:

var startTime = new Date();
setTimeout(function () {
    console.log(new Date() - startTime);
}, 100)

請問最後列印的是多少?其正確答案是,取決於後面同步執行的JS需要占用多少時間。

即為:MAX(同步執行的時間, 100);緣何如此,就得看下setTimeout()運行機制了。

setTimeout()運行機制

setTimeout()setInterval()的運行機制是,將指定的代碼移出本次執行,等到下一輪Event Loop時,再檢查是否到了指定時間。如果到了,就執行對應的代碼;如果不到,就等到再下一輪Event Loop時重新判斷。這意味著,setTimeout()指定的代碼,必須等到本次執行的所有代碼都執行完,才會執行。

每一輪Event Loop時,都會將“任務隊列”中需要執行的任務,一次執行完。setTimeout()setInterval()都是把任務添加到“任務隊列”的尾部。因此,它們實際上要等到當前腳本的所有同步任務執行完,然後再等到本次Event Loop的“任務隊列”的所有任務執行完,才會開始執行。由於前面的任務到底需要多少時間執行完,是不確定的,所以沒有辦法保證,setTimeout()setInterval()指定的任務,一定會按照預定時間執行。

setTimeout(someTask,100);
veryLongTask();

上面代碼的setTimeout,指定100毫秒以後運行一個任務。但是,如果後面立即運行的任務(當前腳本的同步任務))非常耗時,過了100毫秒還無法結束,那麼被推遲運行的someTask就只有等著,等到前面的veryLongTask運行結束,才輪到它執行。

setTimeout(func,0)

在使用backbone框架寫代碼的時候,因為有些需求因素,新手總會在render時操縱DOM,卻發現改變DOM元素狀態,代碼沒有問題,界面卻沒有變更。而使用setTimeout(func,time)卻能解決這個問題,即便time=0;探究一番,真相只有一個:

setTimeout(func,0)含義

運行下麵代,func1func2誰會先執行?很明顯func2先執行:

setTimeout(function () {
    func1();
}, 0)
func2();

setTimeout()的作用是將代碼推遲到指定時間執行,如果指定時間為0,即setTimeout(f,0),那麼會立刻執行嗎?

答案是不會。因為setTimeout()運行機制說過,必須要等到當前腳本的同步任務和“任務隊列”中已有的事件,全部處理完以後,才會執行setTimeout()指定的任務。也就是說,setTimeout()的真正作用是,在“任務隊列”的現有事件的後面再添加一個事件,規定在指定時間執行某段代碼。setTimeout()添加的事件,會在下一次Event Loop執行。

setTimeout(f,0)將第二個參數設為0,作用是讓f在現有的任務(腳本的同步任務和“任務隊列”中已有的事件)一結束就立刻執行。也就是說,setTimeout(f,0)的作用是,儘可能早地執行指定的任務。

setTimeout(function (){
    console.log("你好!");
}, 0);

上面代碼的含義是,儘可能早地顯示“你好!”。

setTimeout(f,0)指定的任務,最早也要到下一次Event Loop才會執行。請看下麵的例子。

setTimeout(function() {
    console.log("Timeout");
}, 0);
function a(x) {
    console.log("a() 開始運行");
    b(x);
    console.log("a() 結束運行");
}
function b(y) {
    console.log("b() 開始運行");
    console.log("傳入的值為" + y);
    console.log("b() 結束運行");
}
console.log("當前任務開始");
a(42);
console.log("當前任務結束");
// 當前任務開始
// a() 開始運行
// b() 開始運行
// 傳入的值為42
// b() 結束運行
// a() 結束運行
// 當前任務結束
// Timeout

上面代碼說明,setTimeout(f,0)必須要等到當前腳本的所有同步任務結束後才會執行。

0毫秒實際上達不到的。根據HTML5標準setTimeout()推遲執行的時間,最少是4毫秒。如果小於這個值,會被自動增加到4。這是為了防止多個setTimeout(f,0)語句連續執行,造成性能問題。

另一方面,瀏覽器內部使用32位帶符號的整數,來儲存推遲執行的時間。這意味著setTimeout()最多只能推遲執行2147483647毫秒(24.8天),超過這個時間會發生溢出,導致回調函數將在當前任務隊列結束後立即執行,即等同於setTimeout(f,0)的效果。

setTimeout(f,0)應用

調整事件的發生順序

setTimeout(f,0)有幾個非常重要的用途。它的一大應用是,可以調整事件的發生順序。比如,網頁開發中,某個事件先發生在子元素,然後冒泡到父元素,即子元素的事件回調函數,會早於父元素的事件回調函數觸發。如果,我們先讓父元素的事件回調函數先發生,就要用到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'
};

上面代碼在點擊按鈕後,先觸發回調函數A,然後觸發函數C。在函數A中,setTimeout()將函數B推遲到下一輪Loop執行,這樣就起到了,先觸發父元素的回調函數C的目的了。

用戶自定義的回調函數,通常在瀏覽器的預設動作之前觸發。比如,用戶在輸入框輸入文本,keypress事件會在瀏覽器接收文本之前觸發。因此,下麵的回調函數是達不到目的的。

document.getElementById('input-box').onkeypress = function(event) {
    this.value = this.value.toUpperCase();
}

上面代碼想在用戶輸入文本後,立即將字元轉為大寫。但是實際上,它只能將上一個字元轉為大寫,因為瀏覽器此時還沒接收到文本,所以this.value取不到最新輸入的那個字元。只有用setTimeout()改寫,上面的代碼才能發揮作用。

document.getElementById('my-ok').onkeypress = function() {
    var self = this;
    setTimeout(function() {
        self.value = self.value.toUpperCase();
    }, 0);
}

上面代碼將代碼放入setTimeout()之中,就能使得它在瀏覽器接收到文本之後觸發;原來如此:這也就解釋了緣何在使用backbone調用render之時,操縱DOM是無效的了,因為當時連DOM元素都還沒獲取到(為何沒報錯?這牽扯到另一個話題),自然等頁面渲染完畢了也沒見想要的結果了。

分割耗時任何

眾所周知JavaScript是單線程的,特點就是容易出現阻塞。如果一段程式處理時間很長,很容易導致整個頁面hold住。什麼交互都處理不了怎麼辦?

簡化複雜度?複雜邏輯後端處理?HTML5的多線程?……

上面都是OK的做法,但是setTimeout()也是處理這種問題的一把好手。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) clearInterval(timer);
}
timer = setTimeout(func, 0);

上面代碼有兩種寫法,都是改變一個網頁元素的背景色。寫法一會造成瀏覽器“堵塞”,而寫法二就不會,這就是setTimeout(f,0)的好處。即:可利用setTimeout()實現一種偽多線程的概念

另一個使用這種技巧的例子是,代碼高亮的處理。如果代碼塊很大,就會分成一個個小塊,寫成諸如setTimeout(highlightNext, 50)的樣子,進行分塊處理。

clearTimeout()

setTimeout()setInterval()函數,都返回一個表示計數器編號的整數值,將該整數傳入clearTimeout()clearInterval()函數,就可以取消對應的定時器。

var id1 = setTimeout(f,1000);
var id2 = setInterval(f,1000);
clearTimeout(id1);
clearInterval(id2);

setTimeout()setInterval()返回的整數值是連續的(一定環境下,比如瀏覽器控制台,或者JS執行環境等),也就是說,第二個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()都無效了。

下麵是一個clearTimeout()實際應用的例子。有些網站會實時將用戶在文本框的輸入,通過Ajax方法傳回伺服器,jQuery的寫法如下:

$('textarea').on('keydown', ajaxAction);

這樣寫有一個很大的缺點,就是如果用戶連續擊鍵,就會連續觸發keydown事件,造成大量的Ajax通信。這是不必要的,而且很可能會發生性能問題。正確的做法應該是,設置一個門檻值,表示兩次Ajax通信的最小間隔時間。如果在設定的時間內,發生新的keydown事件,則不觸發Ajax通信,並且重新開始計時。如果過了指定時間,沒有發生新的keydown事件,將進行Ajax通信將數據發送出去。

這種做法叫做debounce(防抖動)方法,用來返回一個新函數。只有當兩次觸發之間的時間間隔大於事先設定的值,這個新函數才會運行實際的任務。假定兩次Ajax通信的間隔不小於2500毫秒,上面的代碼可以改寫成下麵這樣。

$('textarea').on('keydown', debounce(ajaxAction, 2500))

利用setTimeout()clearTimeout(),可以實現debounce方法。該方法用於防止某個函數在短時間內被密集調用,具體來說,debounce方法返回一個新版的該函數,這個新版函數調用後,只有在指定時間內沒有新的調用,才會執行,否則就重新計時。

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);
    };
}
// 用法示例
var todoChanges = debounce(batchLog, 1000);
Object.observe(models.todo, todoChanges);

現實中,最好不要設置太多個setTimeout()setInterval(),它們耗費CPU。比較理想的做法是,將要推遲執行的代碼都放在一個函數里,然後只對這個函數使用setTimeout()setInterval()

如何使用setTimeout()

setTimeout()自然不止於這些,但已足見其強大。那麼問題來了,需要在項目中大量使用麽?視個人和項目而定吧;如不能熟練掌握,不建議多用。畢竟在某些情景之下,setTimeout()作為一個hack的方式而存在的(打亂模塊的生命周期,並且在問題出現時很難調試,你懂的),譬如:當一個實例還沒有初始化的前,我們就使用這個實例,錯誤的解決辦法是使用實例時加個setTimeout(),確保實例使用前已初始化。

但只要足夠熟悉它,以及使用的場景(包括模塊生命周期),使用它也就無可厚非了。比如underscore中不少方法也是基於這setTimeout()方法寫的;比如非常強大的_.defer: 延遲調用function直到當前調用棧清空為止,類似使用延時為0setTimeout()方法。對於執行開銷大的計算和無阻塞UI線程的HTML渲染時候非常有用。 如果傳遞arguments參數,當函數function執行時, arguments 會作為參數傳入。

也比如前文提到的防抖動方法debounce_.debounce(function, wait, [immediate]) ;在underscore中其實現方法如下:

_.debounce = function(func, wait, immediate) {
    var timeout, args, context, timestamp, result;
    var later = function() {
        var last = _.now() - timestamp;
        if (last < wait && last >= 0) {
            timeout = setTimeout(later, wait - last);
        } else {
            timeout = null;
            if (!immediate) {
                result = func.apply(context, args);
                if (!timeout) context = args = null;
            }
        }
    };
    return function() {
        context = this;
        args = arguments;
        timestamp = _.now();
        var callNow = immediate && !timeout;
        if (!timeout) timeout = setTimeout(later, wait);
        if (callNow) {
            result = func.apply(context, args);
            context = args = null;
        }
        return result;
    };
};

本文轉載自: https://www.w3cplus.com/blog/2103.html © w3cplus.com


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

-Advertisement-
Play Games
更多相關文章
  • C/S架構(Client/Server,即客戶機/伺服器模式)分為客戶機和伺服器兩層:第一層是在客戶機系統上結合了表示與業務邏輯,第二層是通過網路結合了資料庫伺服器。簡單的說就是第一層是用戶表示層,第二層是資料庫層。客戶端和伺服器直接相連,這兩個組成部分都承擔著重要的角色。 Android內核是基於 ...
  • 前面已經封裝了很多常用、基礎的組件了: "base module" , 包括了: crash 處理 常用工具類 apk 升級處理 log 組件 logcat 採集 ftp 文件上傳 blur 高斯模糊 fresco 圖片處理 等等 那麼,今天繼續再來封裝一個網路組件,基於 "volley" 的二次封 ...
  • Application是Android的又一大組件,在App運行過程中,有且僅有一個Application對象貫穿應用的整個生命周期,所以適合在Application中保存應用運行時的全局變數。而開展該工作的基礎,是必須獲得Application對象的唯一實例,也就是將Application單例化。 ...
  • 元素分類: 1.行級元素:內聯元素 inline 特征:內容決定元素所占位置,不可以通過CSS改變寬高 span strong em a del 2.塊級元素:block特征:獨占一行,可以通過CSS改變寬高 div p ul li ol form address 3.行級塊元素:inline-bl ...
  • 看完一整本書,結果寫不出什麼東西,按書上教程來,基本能把例子完成個七七八八,可是用padding還是margin完全整不清……,而且只要結構一複雜,元素就各種不受控制。 沒辦法 找來韓順平老師的視頻(沒錯,就是在網上不知道怎麼搞來的) 看完老師講完整個html&CSS部分2,終於把這個內容吃透了。 ...
  • Vue.js 的源碼都是在src 目錄下,其目錄結構如下。 1.compiler 目錄包含Vue.js 所有編譯相關的代碼。它包括把所有模板解析成ast 語法樹, ast 語法樹優化等功能。 2.core 目錄 包含了Vue.js 的核心代碼,包括內置組件,全局API封裝,Vue 實例化,觀察者,虛 ...
  • 上一篇博客我向大家介紹了基於ko-easyui實現的開發模板,博客地址:https://www.cnblogs.com/cqhaibin/p/9825465.html#4095185。但在還遺留三個問題。本篇幅文章就以解決這三問題展開。 一、代理 前後端分離的開發模式,一定會存在前端開發工程,與後端 ...
  • 用node搞web服務和直接用tomcat、Apache做伺服器不太一樣, 很多工作都需要自己做。緩存策略也要自己選擇,雖然有像koa-static,express.static這些東西可以用來管理靜態資源,但是為了開發或配置時更加得心應手,知其所以然,有瞭解http緩存的必要。另外,http緩存作... ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...