web

优化事件处理函数

debounce throttle requestAnimationFrame

Posted by Lorry on October 7, 2018

文章字数:3238, 阅读全文大约需要:9 分钟

概述

浏览器的性能是不同的, 如果事件触发时间较短, 频率较高, 必然会导致浏览器跟不上事件的频率, 导致掉帧,卡顿等问题, 用户体验不佳, 而且事件频繁触发会消耗更多的 cpu 资源以及电量, 对移动端来说是非常严苛的.

所以就需要对事件的处理进行优化.主要有三个方法,

  • debounce, 函数防抖
  • throttle, 函数节流
  • requestAnimationFrame, 浏览器的新方法

先来看看最新且名字最长的** requestAnimationFrame **, 他是在 window 上的方法, 不用引入任何的第三方库, 且会根据重绘所需的时间来进行调用, 所以理论上来说不会造成任何的卡顿, 只是执行的次数会变少.

下面是一个例子, 模拟了一个进度条.

See the Pen dgONLL by Jiang Lirui (@JiangLiruii) on CodePen.

js 代码为:

var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';

// 在 requestAnimationFrame 中接受一个时间戳的参数, 代表了执行函数时的当前时间
function step(timestamp) {
  if (!start) start = timestamp;
  var progress = timestamp - start;
  element.style['padding-left'] = Math.min(progress / 10, 500) + 'px';
  // 指定一个终点
  if (progress < 5000) {
    // 迭代调用该方法
    window.requestAnimationFrame(step);
  }
}

window.requestAnimationFrame(step);

上述方法是递归调用, 他的厉害之处还不在这里, 比如 scroll 事件的调用

See the Pen ReoVaZ by Jiang Lirui (@JiangLiruii) on CodePen.

js 源码为:

// 设置标志位
var ticking = false;
var outter = document.getElementById('outter')
outter.addEventListener('scroll', () => { 
    if(!ticking) {
        // 当重绘完成以后调用
        window.requestAnimationFrame(() =>{
        console.log('with', new Date());
        // 重置标志位
        ticking = false;
        })
    }
    // 将标志位设为 true, 阻止继续requestAnimationFrame的调用
    ticking = true;
})
// 参考不使用 requestAnimation 的情况
outter.addEventListener('scroll', () => {
  console.log('without', new Date());
})

使用 rAF 的好处是原生 API, 容易维护执行, 且精度较高, 基本上可以视为16ms 的 throttle, 当然缺点也比较明显, 只能在前端使用, 不支持后端, 太频繁的 rAF 调用仍需要通过资质 throttle 进行调节

再来一起看看函数节流和函数防抖, 节流就是从源头上就不让其发生, 防抖是让其发生但会忽略掉之前的一部分.

比如:

See the Pen WaojaB by Jiang Lirui (@JiangLiruii) on CodePen.

js 源码为

// 全局 timeout 变量
var t = null
var outter = document.getElementById('outter')
function debounce(func, timeout=1000) {
    if(!t) {
        // 设置异步执行
        t = setTimeout(() => {
            // 调用回调函数
            func()
            // 清除
            t = null
        }, timeout)
    } else {
        // 清除
        t = clearTimeout(t)
    }
}
outter.addEventListener('scroll', () => {
  debounce(() => {
      console.log('debounce')
  })
})
outter.addEventListener('scroll', () => {
  console.log('without debounce')
})

下面是 throttle, 其他代码都一样, 唯一不同的是针对已有定时器的处理方式, 节流就是不让后面的再发生了, 要让之前的定时器完成了之后才”开流”

function throttle(func, timeout=1000) {
    if(!t) {
        t = setTimeout(() => {
            func();
            t = null;
        }, timeout)
    } else {
        // 这里是最主要的区别
        return
    }
}

以上就是最基础的版本了, 当然传入的函数还有其他的参数, 甚至 this 的绑定问题, 那么可以进行高阶函数的抽象

function throttle(func, timeout=1000, ...args) {
    var t;
    // 返回一个函数
    return function() {
        if(!t) {
            t = setTimeout(() => {
                // 这里将 this 和参数一并传入
                func.apply(this,args);
                t = null;
            }, timeout)
        } else {
            return
        }
    }
}
outter.addEventListener('scroll', throttle(() => {
      console.log('throttle')
  }))
outter.addEventListener('scroll', () => {
  console.log('without throttle')
})

还有更进一步的需求, 比如 underscore 的 throttle,debounce

  • throttle 默认会在第一次时立即调用函数, 用 option 来设置 trail 和 leading.
  • debounce有 immediate 参数来区分 trail 还是 leading 模式, 如果是 true 为 leading 模式, 即先触发函数, 之后同理, 等待 timeout, 在期间如果被打断, timeout 就会刷新, 重新计时.