异步之定时器

定时器
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);
}
//fn()
setTimeout(fn,1000);

setTimeout能够保证只有前一个setTimeout任务执行完毕,才会创建一个新的setTimeout任务,这样不会丢帧,也能保证setTimeout任务执行的时间间隔至少为指定的时间间隔。

呃,还有个问题,怎么把定时器停下来?

var count = 0;
function fn(){
    console.log(count++);
    if(count === 10){
        fn = null;
    }
    setTimeout(fn,1000);
}
setTimeout(fn,1000);
使用定时器来实现动画
//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,interval);
    }
}

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

在这里插入图片描述

var start = document.querySelector(".start");
start.onclick = function(){
    var startTime = Date.now();
    var lastedTime = startTime;
    function requestAnimationFrame(fn){
        var currentTime = Date.now();
        var diff = currentTime - lastedTime;
        var delay = diff>16?0:(16-diff);
        setTimeout(function(){
            fn(currentTime+delay);
        },delay)
        lastedTime = currentTime;
    }
    
    function move(timeStamp){
        var now = Date.now();
        var left = 8+ (now-startTime)/16;
        if(left<200){
            box.style.left = left + "px";
            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!==null) 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!=null) 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);//节流

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值