javaScript中定時器的用法和原理

js中的線程和定時器


  • 在js中定時器是一個非常強大但經常會被錯誤使用的一個特性,如果能夠正確使用定時器那麼就會給開發人員帶來非常多的好處
  • 定時器提供一種讓一段代碼在一定毫秒之後,再 異步執行的能力,由於js是單線程的(同一時間只能執行一處的javascript代碼),定時器提供一種跳過這種限制的方法。

1. 定時器和線程是如何工作的

1.1 設置和清楚定時器
js提供了兩種方式,用於創建定時器以及相應兩種方法(刪除),這些方法都是windows對象上的方法

js中操作定時器的方法 :

方法 格式 描述
setTImeout id=setTimemout(fn,time) 啓動一個定時器,在一段時間(time)之後執行傳入的回調函數 fn,返回一個定時器id用於clear
clearTimeout clearTimeout(id) 如果定時器還沒觸發,傳入id就可以取消該定時器
setinterval id=setinterval(fn,time) 啓動一個定時器,在每隔一段時間(time)之後執行傳入的回調 函數fn,並且返回一個定時器id涌入clear
clearinterval clearinterval(id) 傳入間隔定時器標識,即清除該定時器

這裏需要提示一下,js中的延遲時間是不能保證的,原因和js的單線程有很大關係

1.2 執行進程中的定時器運行
js中的單線程造成的結果是:異步事件的處理程序,如果用戶界面和定時器,在線程中沒有代碼執行的時候纔會執行,也就是說這些程序在執行時必須要排隊執行,並且一個處理程序不能中斷另一個。例如當一個異步事件觸發時(如鼠標單擊‘click’,定時器觸發,甚至是XMLHttpRequest完成事件),它就會排隊,並且當線程空閒時它才執行,實際上每一個瀏覽器的排隊機制是不同的。
一個例子
- 在0毫秒時,啓動一個10毫秒的setTimeout以及一個10毫秒的setinterval
- 在6毫秒時,執行鼠標點擊觸發click
- 在10毫秒時,定時器和間隔定時器觸發
- 但是第一個js代碼塊要執行18毫秒
- 直到這段時間內相繼出發了 鼠標單擊click時間 setTimeout 以及兩次setinterval事件,由於這個時間段內有別的代碼正在執行,所以這些時間中的處理程序就不能執行,所以就開始排隊
- 直到鼠標單擊事件結束 在timeout處理程序執行時(原本我們要在10毫秒執行的),注意在30秒的時候又觸發了一次間隔定時器,但是由於之前已經有一個interval代碼正在排隊,所以這次的處理程序就不會執行,按通俗易懂的話就是等到線程中沒有處理程序時,纔會將其添加到隊伍中,瀏覽器不會對特定的interval處理程序的多個實例進行排隊,
- 在34毫秒,timeout執行結束,隊列中的interval處理程序開始執行 由於該程序要執行6毫秒,所以在執行到40毫秒又觸發interval時間,進入隊列,所以到目前爲止進入隊列的interval只有10毫秒和40豪秒觸發的
- 在42秒開始執行40秒觸發的之後 因爲運行時間爲6毫秒所以 之後沒次的interval時間都會在每10毫秒運行
正如我們所看到的interval中的幾個處理程序完全被“擠沒了”
1.3timeout與interval的區別
乍一看感覺兩者並無什麼明顯區別

<script type="text/javascript">
setTimout(function(){
  //執行功能
  settimeout(repeatMe,10); //重新調用自己
})
setInterval(function(){
  //執行功能
},10)  //定義一個Interval定時器,每隔10秒觸發一次
<script>

在上述代碼中,兩者功能看似是相同的,實際上是不同的,其實在setTimeout()代碼中,要在前一個的執行功能結束後纔會添加一個定時器setTimeout(),而setInterval()則是每隔10毫秒,就嘗試回調函數,而不管上一個回調函數是否執行完畢。
所以有下述結論
1. 在js是單線程執行,異步事件必須排隊等待執行。
2. 如果無法立即執行定時器,改定時器就會被推遲到下一個進程空閒的時間點上執行。
3. setTimeout()和setInterval()的執行週期是完全不同的。
4.


2. 定時器用法1:處理大量計算過程

js單線程的本質可能是用js實現複雜操作的一個陷阱,在js執行繁忙的時候,瀏覽器的用戶交互,最好的情況是稍有緩慢,最差的情況則是反應遲鈍,這隨時可能導致瀏覽器隨時掛掉,因爲在js腳本執行的時候頁面所有渲染都要停止,但是如果我們要同時操作上千個DOM的時候,就會引起頁面卡頓,這個時候定時器就來救我們了 ,它可以暫停一段代碼,讓其在空閒的時間去執行
一下是一個例子

<table><tbody></tbody></table>
<script type="text/javascript">
 var tbody=document.getElementByTagName("tbody")[0];//找到tbody元素
   for(var i=0;i<20000;i++){
    var tr=document.createElement("tr")//創建大量行元素
    for(var t=0;t<6;t++){
     var td=document.createElemnt("td")//創建列
     td.appendChild(doument.creatTextNode(i+","+t))//每個列都有個文本節點
     tr.appendChild(td);//將列元素塞入行
    }
    tbody.appendChild(tr);//將每一行插入tbody
  }
</script>

在本例中我們創建了240000個DOM節點,並使用大量單元格來填充一個表格,這是非常大量的DOM操作,明顯會增加瀏覽器的執行時間,從而阻斷正常的用戶交互操作,定時器的作用就來了,在代碼執行的時候可以暫停休息,修改後的代碼如下

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
</head>
<body>
    <table>
        <tbody>

        </tbody>
    </table>
</body>
<script type="text/javascript">
    var rowCount=20000;//有多少行
    var divideInto=4;  //分幾步
    var chunkSize=rowCount/divideInto; //每步執行多少行
    var iteration=0; //當前步
    var table=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);
            }
            table.appendChild(tr);
        }
        iteration++;//步數增加
        if(iteration<divideInto){
            setTimeout(generateRows,0); //下一階段
        }
    },0);
</script>
</html>

3.定時器用法2:中央定時器控制

在使用定時器出現的問題就是對大批定時器的管理,這在處理動畫效果時尤爲重要,所以在操縱大量定時器屬性的時候,我們需要用一種方式來管理他們
同時管理多個定時器會有多問題,如如何保留大量間隔定時器的應用,然後還得取消他們(儘管可以使用閉包這種方法),而且還干擾了瀏覽器的正常運行(這還要看不同瀏覽器的垃圾處理機制)。
在一個簡單輪播圖中 自動播放與運動函數 兩個定時器疊加在一起,可能會發現在一個瀏覽器運行良好,可到了另一個瀏覽器裏 卻變的非常的卡頓,甚至是崩掉。所以現在動畫都使用一種爲中央定時器控制的技術

  • 每個頁面在同一時間只需要運行一個定時器
  • 可以根據需要恢復和暫停定時器
  • 刪除回調函數過程變得很簡單
<script type="text/javascript">
 var timer = { //一個定時器控制對象
    timerID: 0, //記錄狀態
    timers: [],
    add: function(fn) {
        this.timers.push(fn);
    }, //添加定時器函數
    start: function() {
        if(this.tiemrID) return;  //若已有定時器運行 返回
        (function runNext() {       //若定時器序列中有定時器  
          if(timer.timers.length>0){       //尋找序列中的定時器 看那個執行完畢
            for(var i=0;i<timer.timers.length;i++){
               if(timer.timers[i]()===false){
                timer.timers.splice(i,1);
                i--;   //清除定時器 i--
               }
            }     //直到序列中定時器被清除完畢,結束
            timer.timerId=setTimeout(runNext,0);  //進行下一次調用
          }  
        })();  //閉包保存所有屬性值
    },            //開啓函數
    stop:function(){
        clearTimeout(this.timerID);
        this.timerID=0;   //停止函數
    }
 };
</script>

這裏的核心就是start方法,首先確認沒有定時器在運行,如果沒有就立即執行一個即時函數(閉包保存屬性)來開啓中央處理定時器 ,在這個即時函數內,如果有處理程序,就遍歷每一個,如果有某個程序完成返回false我們就從數組中刪除。然後進入下一次調用

我們來測試一下

CTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>

    <body>
        <div id="box" style="position: relative;">
            hello!
        </div>
    </body>
    <script type="text/javascript">
        var timer = { //一個定時器控制對象
            timerID: 0, //記錄狀態
            timers: [],
            add: function(fn) {
                this.timers.push(fn);
            }, //添加定時器函數
            start: function() {
                if(this.tiemrID) return; //若已有定時器運行 返回
                (function runNext() { //若定時器序列中有定時器  
                    if(timer.timers.length > 0) { //尋找序列中的定時器 看那個執行完畢
                        for(var i = 0; i < timer.timers.length; i++){
                            if(timer.timers[i]() === false) {
                                timer.timers.splice(i, 1);
                                i--; //清除定時器 i--
                            }
                        } //序列中定時器被清除完畢,結束
                        timer.timerID = setTimeout(runNext, 0);
                    }
                })(); //閉包保存所有屬性值
            }, //開啓函數
            stop: function() {
                clearTimeout(this.timerID);
                this.timerID = 0; //停止函數
            }
        };
        var box = document.getElementById("box"),
            x = 0,
            y = 20;
        timer.add(function() {
            box.style.left = x + "px";
            if(++x > 500) return false;
        });
        timer.add(function() {
            box.style.top = y + "px";
            y += 2;
            if(y > 500) return false;
        });
        timer.start();
    </script>
</html>

這種方式組織定時器,可以確保回調函數總是按照順序執行,而普通的定時器 並不會這樣。
這種方法還有另一種作用

4.定時器用法3:異步測試

當我們要對還沒完成操作的代碼執行測試的時候,我們需要將這種測試從測試套件中分離出來,以便測試是否異步

(function() {
    var queue = [],
        paused = falsed; //狀態表
    this.test = function(fn) {
        queue.push(fn);    //定義測試函數
        runTest();
    };
    this.paused = function() {
        paused = true;       //定義停止測試函數
    }
    this.resume = function() {
        paused = false;            
        setTimeout(runTest,1);   //定義恢復測試函數
    }
    function runTest(){
        if(!paused&&queue.length){
            queue.shift()();
            if(!paused) 
            resume();
        }                    
    }                              //運行測試函數
})();  //這段代碼只是將異步的代碼按添加的順序執行 已測試 即在等待執行的時候,執行隊列中的第一個否則就完全停止

總之js中的定時器是很有用的,看似簡單的特性但實際實現包含着許多陷阱

在複雜的應用中,定時器就顯的格外重要 如計算密集型代碼,動畫,異步測試

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章