異步之定時器

setInterval()/clearInterval()
setTimeout()/clearTimeout()

簡單舉例

setInterval
//test.html
<body>0
    <script src="./test.js"></script>
</body>
//test.js
var count = Number(document.body.textContent);
var timer = setInterval(function(){
    document.body.textContent = ++count;
},1000);
setInterval存在的問題
setInterval(function(){
 //dosomething ...
},200)

在這裏插入圖片描述
使用setInterval來重複定時器任務可能會出現兩個問題:

  • 丟幀,某個時間間隔可能會被丟掉
    由於405ms時的定時器任務仍然在任務隊列中,所以605ms時的定時器任務被丟棄
  • 定時器任務執行的時間間隔可能會小於指定的時間間隔
    205ms時的定時器任務在主線程執行完畢,主線空閒下來,405ms時的定時器任務立即從任務隊列進入主線程開始執行,前後的時間間隔小於200ms
setTimeout模擬setInterval
function fn(){
    document.body.textContent = ++count;
    setTimeout(fn,1000);
}
setTimeout(fn,1000);

setTimeout能夠保證只有前一個setTimeout任務執行完畢,纔會創建一個新的setTimeout任務,這樣不會丟幀,也能保證setTimeout任務執行的時間間隔至少爲指定的時間間隔。

使用定時器來實現動畫

//test.html
<html>
<head>
    <link rel="stylesheet" href="./test.css">
</head>
<body>
    <div class="box"></div>
    <div class="start">開始</div>
    <div class="reset">重置</div>
    <script src="./test.js"></script>
</body>
</html>
//test.css
.box{
    background:#fb3;
    width:100px;height:100px;
    position:absolute;
}
.start,.reset{
    position:absolute;
    top:120px;
    background-color:orange;
    color:white;
    font-size:0.75em;
    padding:0.5em;
    cursor:pointer;
}
.reset{
    left:5em;
}
.start:hover,.reset:hover{
    background-color:olive;
}
//test.js
var box = document.querySelector(".box");
var left = Number(window.getComputedStyle(box).left.slice(0,-2));
var original = left;
const interval = 16;

// var last;
function move(){
    // var current = Date.now();
    // if(last == undefined) last = current;
    left += 10;
    // console.log("時間差:",current-last,"    left:",left);
    // last = current;
    box.style.left = left + "px";
    if(left < 200){
        setTimeout(move,16);
    }
}

var start = document.querySelector(".start");
start.onclick = function(){
    setTimeout(move,interval);
}

function back(){
    left -= 10;
    box.style.left = left + "px";
    if(left > original){
        setTimeout(back,interval);
    }
}
var reset = document.querySelector(".reset");
reset.onclick = function(){
    left = Number(window.getComputedStyle(box).left.slice(0,-2));
    setTimeout(back,interval);
}

在這裏插入圖片描述
每秒渲染60幀,即 約16ms渲染1幀能夠獲得比較流暢的動畫效果,於是將時間間隔指定爲16.666ms,以期望定時器 每隔16.666ms改變一次 絕對定位元素box的left值,從而實現動畫。代碼中指定延時時間爲16ms

存在的問題

如果定時器能保證 每隔 16ms 就一定會調用一次回調函數,那麼動畫效果確實差強人意。
可惜,不是!
在這裏插入圖片描述
定時器作爲作爲瀏覽器的異步線程之一,它會兢兢業業地計着時,但每過16ms,回調函數會不會被執行,它做不了主,因爲所有的同步任務都是在js引擎線程上執行。
如果同步任務太多,js引擎線程忙不過來,那麼定時器回調函數就會一直在任務隊列裏待着,待着待着,時間慢慢過去了,box不偏移了,動畫也就卡在那兒了。
也就是說,定時器無法保證回調函數按照指定的時間間隔被調用從而導致動畫卡頓,這就是使用定時器實現動畫的缺點。
好了,既然出現問題了,就得想法子解決它。

解決方法
認識requestAnimationFrame

window.requestAnimationFrame(callback),字面意思是“請求動畫幀”。requestAnimationFrame(callback)會請求瀏覽器在 下次刷新前 調用回調函數。瀏覽器的刷新頻率是60Hz,即每秒刷新60次,所以requestAnimationFrame裏的回調函數 每秒會被調用60次。
看下面一個例子,借requestAnimationFrame計算瀏覽器的刷新頻率。

var startTime;
var count = 0;
function render(timeStamp){
    if(!startTime) startTime = timeStamp;
    count++;
    if(count%100 === 0){
        var time = (timeStamp - startTime)/1000;
        var f = Math.round(count / time);
        console.log(f);
    }
    requestAnimationFrame(render);
}
requestAnimationFrame(render);

在這裏插入圖片描述
requestAnimationFrame(callback)的回調函數callback接受一個參數,這個參數timeStamp是一個時間戳,表示開始執行回調函數的時刻,和window.performance.now()是一樣的。
舉個例子理解一下。

function doSomething(){
    console.log("hello world");
}
let t0;
let t1;
console.log(t0 = window.performance.now());
doSomething();
console.log(t1 = window.performance.now());
console.log("doSomething 執行了"+(t1-t0)+"毫秒");

在這裏插入圖片描述
時間戳相減,我們就知道執行某個函數消耗了多長時間。

使用requestAnimationFrame實現動畫

前面我們提過,每秒渲染60幀能夠獲得較流暢的動畫效果,而requestAnimationFrame剛好能夠幫我們協調到這麼一個節奏。

var box = document.querySelector(".box");
var left = Number(window.getComputedStyle(box).left.slice(0,-2));

var start = document.querySelector(".start");
start.onclick = function(){
    // var last;
    function move(timeStamp){
        // if(last == undefined) last = timeStamp;       
        left += 10;
        box.style.left = left + "px";
        // console.log("時間差:",(timeStamp-last).toFixed(3),"   left:",left);
        // last = timeStamp;
        if(left < 200){
            requestAnimationFrame(move);
        }
        
    }
    requestAnimationFrame(move);
}

在這裏插入圖片描述
相較於setTimeout,使用requestAnimationFrame來實現動畫,效果更理想。
另外,有requestAnimationFrame,就有cancelAnimationFrame
好了,問題解決了。繼續深入下,瞭解下requestAnimationFrame的實現。

requestAnimationFrame的實現
var box = document.querySelector(".box");
var left = Number(window.getComputedStyle(box).left.slice(0,-2));

var start = document.querySelector(".start");
start.onclick = function(){
    var startTimeStamp = Date.now();
    var lastTimeStamp = startTimeStamp;
    function requestAnimationFrame(callback){
        var currentTimeStamp = Date.now();
        var diff = currentTimeStamp - lastTimeStamp;
        var delay = diff>16 ? 0 : (16-diff);
        setTimeout(() => {
            callback(lastTimeStamp = currentTimeStamp+delay);
        },delay);
    }
    // var last;
    function move(timeStamp){
        // if(last == undefined) last = timeStamp;
        left += 10;
        // console.log("時間差:",timeStamp-last,"    left:",left);
        // last = timeStamp;
        box.style.left = left + "px";
        if(left < 200){
            requestAnimationFrame(move);
        }
    }
    requestAnimationFrame(move);
}

在這裏插入圖片描述
在這裏插入圖片描述
這裏涉及到兩個時間戳。

  • currentTimeStamp
    本次動畫開始執行的時刻
  • lastTimeStamp
    上次動畫開始執行的時刻

diff = currentTimeStamp - lastTimeStamp,我們會判斷這兩次動畫的時間間隔diff是否大於16ms
如果大於,那麼setTimeout中延時將設置爲0,從而保證只要js引擎線程空閒,就立即執行定時器的回調函數來更新動畫
如果小於,setTimeout中延時將設置爲(16-diff),也就是說,即使js引擎線程空閒,也要等待(16-diff)這麼長時間,纔會執行回調函數來更新動畫。
相較於setTimeout(callback,interval),期望以固定的時間間隔來調用回調然並卵,它實現不了requestAnimationFrame(callback)會更靈活地選擇時機來調用回調:如果上次太慢,耽擱了進度,這次就不等了,立即執行;如果上次給力,時間充裕,這次就不着急,可以等等。

定時器助力函數防抖節流

不論是bind的實現,還是函數節流防抖,採用了 函數柯里化。
瞧,它們的寫法是相同的。

  • bind的實現
const slice = Array.prototype.slice;
//bind的實現
function bind(fn,context){
    var args = slice.call(arguments,2);
    return function(){
        var finalArgs = args.concat(slice.call(arguments));
        fn.apply(context,finalArgs);
    }
}
  • 函數防抖
const slice = Array.prototype.slice;
//函數防抖
function debounce(fn,context,delay=100){
    var timer = null;
    var args = slice.call(arguments,3);
    return function(){
        if(timer) clearTimeout(timer);
        var finalArgs = args.concat(slice.call(arguments));
        timer = setTimeout(() => {
            fn.apply(context,finalArgs);
        },delay);
    }
}

我們來看下它的應用吧,最典型的就是resize事件了。
改變瀏覽器窗口大小、最大化或最小化瀏覽器窗口都會觸發resize事件。如果resize事件的事件程序中包含了大量的DOM操作,它們將佔用較多的內存,消耗較多的CPU計算能力,由此可能導致瀏覽器掛起甚至崩潰。這時候,函數防抖 就派上用場了。

const handler = function(){
    console.log("resizing");
}
// document.body.onresize = handler;  //不防抖
document.body.onresize = debounce(handler); //防抖

如果上一次的定時器任務執行完畢,那麼clearTimeout(timer)沒啥意義;
如果上一次的定時器任務沒有執行完,那麼clearTimeout(timer)將取消掉上一次的定時器任務,並setTimeout()來創建新的定時器任務。這樣一來,即使100ms內連續觸發了20次resize事件,其事件處理程序也只會執行一次。

  • 函數節流
const slice = Array.prototype.slice;
//函數節流
function throttle(fn,context,delay=100){
    var timer = null;
    var args = slice.call(arguments,3);
    return function(){
        if(timer) return;
        var finalArgs = args.concat(slice.call(arguments));
        timer = setTimeout(() => {
            fn.apply(context,finalArgs);
            timer = null;
        },delay);
    }
}

函數節流的典型應用就是scroll事件了。連續觸發scroll事件,單位時間內觸發一次每隔一段時間會觸發一次

body{
	height:1000px;
}
var count = 0;
const handler = function(){
    count++;
    var t2 = window.performance.now();
    var diff = (t2-t1)/1000;
    console.log("scroll",Math.floor(count/diff));
}

var t1 = window.performance.now();
//document.body.onscroll = handler; //不節流
document.body.onscroll = throttle(handler);//節流

在這裏插入圖片描述

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