高级定时器

# 高级定时器

[TOC]

# 一、重复的定时器

  • 一般情况下setTimeout用于延迟执行某方法或功能;setInterval则一般用于刷新表单,对于一些表单的假实时指定时间刷新同步。

# 1.1 setInterval的问题

当使用setInterval()时,仅当没有该定时器的任何其他代码实例时,JS引擎才将定时器代码添加到队列,确保了定时器代码加入队列中的最小时间间隔为指定间隔。

但仍然会存在两个问题(详见高程611页):

  1. 某些间隔会被跳过。
  2. 多个定时器的代码执行之间的间隔可能会比预期的小。

# 1.2 解决:链式setTimerout()调用

setTimeout(function) {
    
    // 处理中...
    
    // 使用arguments.callee来获取当前执行的函数的引用
    setTimeout(arguments.callee, interval);
}, interval);
1
2
3
4
5
6
7
  • 在前一个定时器代码执行之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。
  • 可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。

# 二、数组分块

  • 运行在浏览器中的JavaScript都被分配了一个数量的资源。
    • 如果代码循行超过特定的时间或者特定语句数量就不让它继续执行。
  • 脚本长时间运行的问题通常是有两个原因造成的:
    • 过长的、过深的嵌套函数调用
    • 进行大量处理的循环

# 2.1 大量处理的循环

for (var i=0, len=data.length; i < len; i++ ){
    process(data[i]);
}
1
2
3

由于JavaScript的执行是一个阻塞操作,脚本运行所花时间越久,用户无法与页面交互的时间也越久。

# 2.2 数组分块

当该处理不是必须同步完成,数据不是必须按顺序完成,就可以用定时器分割这个循环。

这是一种叫做数组分块的技术,小块小块地处理数组,通常每次一小块。

# 2.2.1 基本思路

为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。

# 2.2.2 实现

// 处理的项目的数组,用于处理项目的函数,可选的运行该函数的上下文
function chunk(array, process, context) {
    setTimeout(function() {
        var item = array.shift();
        process.call(context, item);
        
        if(array.length > 0) {
            setTimeout(arguments.callee, 100);
        }
    }, 100);
}
1
2
3
4
5
6
7
8
9
10
11
  • 重要性:将多个项目的处理在执行队列上分开,在每个项目处理之后,给予其他的浏览器处理机会运行,这样就可以避免长时间运行脚本错误。
  • 一旦某个函数需要花50ms以上的时间完成,那么最好看看能否将任务分割为一系列可以使用定时器的小任务。

# 三、节流与防抖

DOM操作比起非DOM交互需要更多的内存和CPU时间。连续尝试进行过多的DOM相关操作可能会导致浏览器挂起,有时候甚至会崩溃。

参考链接::https://juejin.im/post/5cbc68095188251ae95d3f4a

# 3.1 函数节流

节流:在指定的时间间隔内只会执行一次任务。

触发高频事件后,n秒后函数才会执行一次,如果n秒内高频事件再次被触发,则无视该触发,等事件执行完后,才重新触发。

某些代码不可以在没有间断的情况连续重复执行,只有在执行函数的请求停止了一段时间后才执行。

可用来处理scroll和resize事件。

function throttle(fn, interval, params) {
    
    let last = 0;

    return () => {
        let now = +new Date();

        if (now - last >= interval) {
            last = now;
            fn(...params);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3.2 函数防抖

防抖:任务触发的间隔超过指定的间隔才会被执行。

触发高频事件后,n秒后函数才会执行一次,如果n秒内高频事件再次被触发,则重新等待n秒。

function debounce(fn, delay, params) {

    let timer = null;

    return () => {

        if(timer) {
            clearTimeout(timer);
        }

        timer = setTimeout(() => {
            fn.apply(null, params);
        }, delay);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.3 节流与防抖

防抖和节流可视化比较:http://demo.nimius.net/debounce_throttle/

  • 在高频触发事件中
    • 节流就是第一个触发的事件说的算。当第一次触发事件,就开始计时,等待一定时间后执行,并且在这等待的时间再次触发的事件都会被屏蔽掉。
    • 防抖则是最后一个说的算。每次触发事件都会清除之前的定时器,再新建一个。

# 3.4 优化首屏体验-懒加载

// 获取所有的图片标签
const imgs = document.getElementsByTagName('img')
// 获取可视区域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight
// num用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
let num = 0
function lazyload(){
    for(let i=num; i<imgs.length; i++) {
        // 用可视区域高度减去元素顶部距离可视区域顶部的高度
        let distance = viewHeight - imgs[i].getBoundingClientRect().top
        // 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
        if(distance >= 0 ){
            // 给元素写入真实的src,展示图片
            imgs[i].src = imgs[i].getAttribute('data-src')
            // 前i张图片已经加载完毕,下次从第i+1张开始检查是否露出
            num = i + 1
        }
    }
}
// 监听Scroll事件
window.addEventListener('scroll', lazyload, false);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 3.4.1 用Throttle来优化Debounce

// fn是我们需要包装的事件回调, delay是时间间隔的阈值
function throttle(fn, delay) {
  // last为上一次触发回调的时间, timer是定时器
  let last = 0, timer = null
  // 将throttle处理结果当作函数返回
  
  return function () { 
    // 保留调用时的this上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments
    // 记录本次触发回调的时间
    let now = +new Date()
    
    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
    if (now - last < delay) {
    // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
       clearTimeout(timer)
       timer = setTimeout(function () {
          last = now
          fn.apply(context, args)
        }, delay)
    } else {
        // 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
        last = now
        fn.apply(context, args)
    }
  }
}

// 用新的throttle包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)

document.addEventListener('scroll', better_scroll)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34