js之定时器

257 阅读10分钟

    BOM(Browser Object Model)浏览器对象模型,它提供了与网页无关的浏览器功能。对象是使用JavaScript开发web应用程序的核心之一。BOM核心是window对象,表示浏览器实例。

一、BOM

    window对象在浏览器中它即可以表示ECMAScript中的global对象,也可以表示浏览器窗口的JavaScript接口。

//这意味着通过var声明的所有全局变量和函数都会变成window对象的属性和方法
//不能使用let或者const替代var
var age = 22;
alert(window.age); //22

window对象常用属性

属性说明
document对话框中显示的当前文档
location指定当前文档的url
navigator浏览器对象

window对象常用方法

方法说明
alert弹出提示对话框
close关闭窗口
open打开一个指定url新窗口
setTimeout延时执行函数
setInterval重复执行函数

二、定时器

1、setInterval函数

  • setInterval()
    • 重复执行定时器,每隔一段时间就会去执行指定函数,重点在于重复执行
    • 语法:如下:
    • 参数:
      • 要执行的函数:当时间到了就会去执行这个函数
      • 时间:间隔时间,单位:ms(1s=1000ms)当第二个参数省略时,它为0
      • 在ie9及以上setInterval()方法还支持添加更多参数
    • 清除定时器:clearInterval(超时ID)
    var time = setInterval(function (a, b) {
        console.log(a + b) //3
    }, 1000, 1, 2)
    // clearInterval(time);
    <button id='btn'>click me!</button>
    <script>
        window.onload = function () {
            var i = 0;
            document.getElementById('btn').onclick = function () {
                setInterval(function () {
                    i++;
                    btn.innerHTML = i;
                }, 1000);
            }
        }
    </script>

2、setTimeout函数

  • setTimeout()
    • 延时执行定时器,当延迟时间到达后会执行指定函数,这个函数只执行一次
    • 语法:如下
    • 清除定时器:clearTimeout(超时ID)
setTimeout(function(){
    //do something
},时间)

    定时器里的this是指向window,因为它是window上的方法(严格模式下指向undefined)。如下所示:可以改变this指向。

 <button id='btn'>click me!</button>
    <script>
        window.onload = function () {
            var i = 0;
            console.log(this); //window
            document.getElementById('btn').onclick = function () {
                console.log(this); //btn
                var This = this;//进行this传递
                setTimeout(function () {
                    console.log(This) //可在内部进行调用 
                    i++;
                    This.innerHTML = i;
                }, 1000);//只执行一次
            }
        }
    </script>

    Javascript在浏览器中是单线程执行,虽然它允许使用定时器指定在某个时间之后或者是每隔一段时间执行相应代码,由于单线程这一特性它每次只能执行一段代码,为了调度不同代码的执行,js维护一个任务队列,其中任务会按照添加到队列地先后顺序来执行。如果队列是空,则会立即执行该代码,如果队列不是空则代码必须等到前面任务执行完才能执行。而计时器设定的延时没有保证,这就意味着,当一件异步事件触发时,这些事件的回调函数将排在执行队列最后去等待执行。

console.log("1");
setTimeout(function () {
    console.log("2")
}, 1000);
setTimeout(function () {
    console.log("3")
}, 0);
console.log(4);
// 1  4  3  2

3、requestAnimationFrame函数(H5新增)

      requestAnimationFrame是请求动画帧。实现动画方法比较多,在js中可以用setTimeout实现,css中可以用transition和animation实现,html5中的canvas和requestAnimationFrame也可以实现。而编写动画循环关键是要知道延迟时间多长合适。为了让不同动画效果显示平滑流畅且确保浏览器有能力渲染产生的变化。所以要有一个合适的循环间隔。

     普通电脑显示器刷新频率是60Hz,相当于每秒钟重绘60次。大多数浏览器会对重绘操作加以限制,不超过显示器的重绘频率所以最平滑动画循环间隔是1000ms/60,约为16.7ms。

     setTimeout它是通过设定间隔时间来不断改变图像位置达到动画效果,容易出现卡顿,抖动等现象。它内部在运行机制上当setTimeout任务被放入异步队列中,只有主线程任务执行完成后才会执行队列中的任务,如果队列前面已经加入了其它任务,那动画代码要等前面任务完成后再执行,实际执行时间比设定时间要晚。requestAnimationFrame它采用的是系统时间间隔,由系统决定回调函数执行时机,60Hz的刷新频率每次刷新间隔中会执行一次回调函数,不会卡顿,改善视觉效果。

     requestAnimationFrame会把每一帧中所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流时间间隔紧紧跟随浏览器刷新频率。

     使用setTimeout实现动画,当页面被隐藏或是最小化时,setTimeout仍然在后台执行动画任务,造成不必要的资源浪费,而requestAnimationFrame当页面处于未激活状态下,这个页面屏幕刷新任务也被系统暂停,当页面激活时动画就从上次停留地方继续执行,有效节省了cpu开销。

     在高频事件中,为了防止在一个刷新间隔内发生多次函数执行,使用requestAnimationFrame可保证每个刷新间隔内函数只被执行一次,这样可以更好的节省函数执行的开销,函数节流。

     使用requestAnimationFrame的用法和setTimeout类似,只不过不需要设置时间间隔。requestAnimationFrame使用一个回调函数作为参数,这个回调函数会在浏览器重绘前调用。它返回一个整数,表示定时器的编号,这个值可以传递给cancelAnimationFrame用于取消这个函数的执行。

     虽然requestAnimationFrame很好用,但是它有兼容问题,ie9及以下浏览器不支持需要降级对接口进行封装。

    <button id="btn"></button>
    <script>
        window.onload=function(){
            var btn=document.getElementById('btn');
            var a=0;
            function add(){
                a++;
                btn.innerHTML=a;
                var timer=requestAnimationFrame(add);
                if(a>=100){
                    //注意:cancelAnimationFrame()一定要放在后面
                    cancelAnimationFrame(timer);
                }
            }
            add();
        }     
      </script>

4、防抖和节流(throttle&debounce)

     在前端开发中有一部分用户行为会频繁触发事件执行,如scroll,mousemove等,函数被非常频繁调用从而造成相当大的性能问题。这才有了函数的防抖和节流。

     函数防抖就是在频繁触发任务的情况下,我们只识别执行一次可以规定是在一开始执行还是在最后执行这个高频时间可以自己进行设定。在点击事件中,如果规定1000ms算是高频,那么这个事件在1000ms内函数只执行一次,如果在1000ms内又触发了该事件则会重新计算函数执行时间,我们不停点击哪怕我们操作了100次也是只触发一次。

     如下例所示,当我们点击按钮向服务器发送请求,因为服务器响应是需要一定时间,如果我们不停点击,那说明这是向服务器不停发送请求,这会造成不必要资源浪费,也不符合业务逻辑。这时我们就要想办法进行处理。

  • 方法一:标识判断
    <button id="submit">click me!</button>
    <script>
        let submit = document.querySelector('#submit');
        //标识判断
        let flag = false;
        submit.onclick = function () {
            if (flag) return;
            flag = true;
            console.log('ok');
            setTimeout(() => {
                flag = false
            }, 1000);
        }
    </script>

     其实每次点击时事件都会触发,只是在函数内部进行了标识判断,当flag为true时才进行触发。这种方法有局限性第一次点击时flag为fasle直接返回第一次没有进行处理。

  • 方法二:移除点击事件,按钮为灰
    <button id="submit">click me!</button>
    <script>
        let submit = document.querySelector('#submit');
          function handle() {
            submit.onclick = null;
            submit.disabled = true;
            //  做想做的一切事
            console.log('ok');

            setTimeout(() => {
                submit.onclick = handle;
                submit.disabled = false;
            }, 1000)
        }
        submit.onclick = handle;
    </script>

     在具体业务场景中,提交事件很少用button或者input元素来写并且按钮为灰。

  • debounce()函数      我们可以封装一个方法,使用setTimeout延时执行定时器,当某个事件高频触发让其只识别执行一次实现真正意义上的防抖。
    <button id="submit">click me!</button>
    <script>
        let submit = document.querySelector('#submit');
        // func:最终要执行的函数 wait:频繁操作的设定时间 immediate:是否一开始就触发[边界判断]
        function debounce(func, wait, immediate) {
            //参数传递及默认值处理
            if (typeof func !== 'function') throw new TypeError('func must be function');
            if (typeof wait === 'undefined') wait = 1000;
            if (typeof wait == 'boolean') {
                immediate = wait;
                wait = 1000;
            }
            if (typeof immediate !== 'boolean') immediate = true;
            //设置定时器的返回标识
            let timer = null;
            return function proxy(...params) {
            // 每点击一次,proxy都会执行一次
                let self = this;
                let now = immediate && !timer;
                //第一次触发并在开始边界 => 立即执行
                if (now) func.call(self, ...params);
                //清除前一次设置的定时器
                if (timer) {
                    clearTimeout(timer);
                    timer = null;
                };
                timer = setTimeout(() => {
                    // 新一轮判断,timer不为null,清定时器 
                    if (timer) {
                        clearTimeout(timer);
                        timer = null;
                    }
                    //结束边界
                    if (!immediate) func.call(self, ...params);
                }, wait)

            }
        }
        function fn(ev) {
            console.log('ok', ev, this);
        }
        submit.onclick = debounce(fn, 200, false);
    </script>

    &nbsp当点击submit按钮时,其实是想执行fn函数。在上面代码中我们把fn当成参数传递给debounce函数,并让debounce函数返回一个proxy函数,点击submit时最终执行proxy函数,我们要在proxy函数中,按照时间及规定让业务fn函数只执行一次。

对于bedounce函数而言:

  • 首先进行参数判断,判断参数类型及个数,进行提前存储(函数的柯里化思想);
  • 其次设置定时器返回标识timer,这样后文中使用及清除定时器时方便;
  • 最后返回proxy函数,proxy函数它要让真正执行的fn函数只执行一次;
    • 点击事件会有this指向及ev事件所以需要在proxy函数中进行处理;
    • 如果第一次触发并且immediate为true,这时要立即执行传递进来函数;
    • 如果immediate为false且频繁触发
      • 在设置定时器前要把上一次定时器进行清除,保证只执行最后一次
      • 定时器设置在规定时间后执行相应操作。immediate为fasle立即执行传递进来的函数。当timer不为null,新一轮点击开始时也要清除定时器。

     节流则指在频繁操作某一事件时能降低触发频率,不只识别一次,而是按照一定间隔时间,每到达这个频率都触发执行一次。

    <script>
        //func:最终要执行的函数 wait:触发执行的频率
        const throttle = function throttle(func, wait) {
            // 参数验证
            if (typeof func !== 'function') throw new Error('func must be function');
            if (typeof wait !== 'number') wait = 1000;
            let timer,
                previous = 0;
            return function proxy(...params) {
                //  self:this   now:当前时间   remaining:时间间隔差
                var self = this,
                    now = +new Date(),
                    remaining = wait - (now - previous);
                //间隔时间已经超过wait,立即执行
                if (remaining <= 0) {
                    func.call(self, ...params);
                    //把当前时间作为下一次比较中的上一次时间
                    previous = now;
                    //没有设置过定时器才设置定时器
                } else if (!timer) {
                    //设置定时器,当remaining时间后执行func
                    timer = setTimeout(() => {
                        //timer执行后要重新赋值为null,清除定时器
                        if (timer) {
                            clearTimeout(timer);
                            timer = null;
                        }
                        func.call(self, ...params);
                        // 
                        now = +new Date();//当前时间是等待remaining后的时间重新获取
                    }, remaining)
                }
            }
        }
        function fn() {
            console.log('ok')
        };
       
        window.onscroll = throttle(fn);
    </script>

     默认情况下,在页面滚动中浏览器每间隔最快的反应时间就会识别监听一次事件触发(谷歌:5-7ms),把绑定方法执行,这样方法执行次数过多,会造成不必要的资源浪费。这时我们就需要节流,规定好触发执行频率也就是每间隔多长时间执行一次。

    &nbsp在上面代码中,当滚动条进行滚动时,其实是想执行fn函数,在throttle函数中把fn当作参数传入,并让throttle函数返回一个proxy函数,我们要在proxy函数中按照触发频率执行多数fn方法。

对于throttle函数而言:

  • 首先进行参数判断,判断参数类型提前进行存储(函数柯里化)
  • 其次设置定时器并记录上一次所触发的时间
  • 最后返回proxy函数,proxy函数它要让真正的fn函数执行多次
    • 点击事件会有this指向及ev事件所以需要在proxy函数中进行处理
    • 拿到触发事件当前时间并得到其与上一次触发时间的差值将这个差值与wait进行比较
      • 如果比较结果remaining小于或者是等于0 说明间隔时间已经超出了wait触发频率,无需等待,立即执行相应函数,并且把当前时间作为下一次比较中的上一次时间
      • 如果间隔时间大于0,并且从来没有设置过定时器,那么设置一个setTimeout定时器,让定时器过了remaining后再执行fn函数,也要重新设置时间,清除定时器。

     节流与防抖它们都可以防止函数过于频繁调用,一般用于性能优化。二者区别在于频繁触发任务防抖我们只识别执行一次函数,而节流将间隔触发频率执行多次。使用时要具体问题具体分析。