【源码共读】跟着underscore学防抖

166 阅读3分钟

什么是防抖、节流

防抖(debounce): 在持续触发事件时,不会立即执行,会等待n秒再执行事件。在n秒内重复触发事件,只执行最后一次。如果在n秒到来之前,又触发了事件,则重新开启定时器,执行最后一次触发事件。

节流(throttle): 在持续触发事件时,在n秒只执行一次函数。如果在秒内,再次触发此事件,则直接忽略不执行。其主要目的就是减少一段时间的触发频率。

页面准备:

// index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

 <body>
    <div id="container"></div>
    <script src="debounce.js"></script>
  </body>
  
</html>

// debounce.js

var count = 1;
var container = document.getElementById('container');

function getUserAction() {
    container.innerHTML = count++;
};

container.onmousemove = getUserAction

限定时间内不可重复触发事件

function debounce(fn, wait){
    let timer;
    return function(){
        clearTimeout(timer)
        timer = setTimeout(fn, wait)
    }
}
// 1s 内不触发
container.onmousemove = debounce(getUserAction, 1000)
改变this

上面的 this 会默认指向 window, 我们需要将其改到触发函数的本身上 div #container。需要用 apply 改变指向

function debounce(fn, wait){
    let timer;
    return function(){
    var context = this
    clearTimeout(timer)
    timer = setTimeout(fn.apply(context), wait)
    }
}
argument

不使用 debounce 函数,getUserAction 传入 e,会打印出 mounseEvent 对象,但是使用了 debounce,e 的结果为 undefined

function debounce(fn, wait){
    let timer;
    return function(){
        var context = this;
        var args = argument
        clearTimeout(timer);
        timeout = setTimeout(fn.apply(context, args), wait)
    }
}
getUserAction 返回值

getUserAction 函数可能有返回值, 所以我们也要返回函数的执行结果。

function debounce(fn, wait){
    let timer;
    let result;
    return function(){
        var context = this;
        var args = argument
        clearTimeout(timer);
        timeout = setTimeout(()=>{
            result = fn.apply(context, args)
        }, wait)
        return result;
    }
}
immediate 立即执行
function debounce(fn, wait, immediate){
    let timer;
    let result;
    return function(){
        var context = this;
        var args = argument
        if(timer) clearTimeout(timer);
        // immediate 为 true,表示第一次触发后执行
        // timer 为 null, 表示首次触发
        // 在 wait = 5s 内, 不再执行
        // 在 5s 到了时,timer 设为 null,此时 callNow 为 true,又可以执行事件了
        if(immediate){
            let callNow = !timer;
            timer = setTimeout(function(){
                result = null;
            },wait)
            if(callNow) result = fn.apply(context, args)
        } else{
            timer = setTimeout(()=>{
                result = fn.apply(context, args)
            }, wait)
        }
        return result;
    }
}
取消防抖
function debounce(fn, wait, immediate){
    let timer;
    let result;
    var debounced = function(){
        var context = this;
        var args = argument
        if(timer) clearTimeout(timer);
        // immediate 为 true,表示第一次触发后执行
        // timer 为空 表示首次触发
        if(immediate){
            let callNow = !timer;
            timer = setTimeout(function(){
                result = null;
            },wait)
            if(callNow) {
                result = fn.apply(context, args)
            }
        } else{
            timer = setTimeout(()=>{
                result = fn.apply(context, args)
            }, wait)
        }
        return result;
    }
    debounced.cancel = function(){
        clearTimeout(timer)
        timer = null
    }
}
cancel 防抖运用:
const setUseAction = debounce(getUserAction, 1000, true)
container.onmousemove = setUseAction
document.getElementById('button').addEventListener('click', function() {
    setUseAction.cancel()
    })
underscore的防抖
// 获取当前时间戳
    function now(){
      return Date.now()|| new Date().getTime()
    }
    
/**
 * 
 * @param {*} func 回调函数
 * @param {*} wait 等待时间
 * @param {*} immediate 是否立即执行回调函数
 * @returns 
 */
 function debounce(func, wait, immediate) {
      var timeout, previous, args, result, context

      var later = function () {
        //计算是否超时
        // passed: 距离上次执行的时间
        // wait: 定时器时间
        var passed = now() - previous
        if (wait > passed) {
          // 例子: wait 定时器为10s,passed: 距离上次执行7sl
          // 所以 在 10 -7 = 3s后 执行 later 方法
          //距离上一次调用已经过了passed时间
          timeout = setTimeout(later, wait - passed)
        } else {
          // 超过定时器时间, 清空重新计时
          timeout = null
          if (!immediate) result = func.apply(context, args)
          // This check is needed because `func` can recursively invoke `debounced`.
          if (!timeout) args = context = null
        }
      }

      //处理回调函数的参数
      var debounced = restArguments(function (_args) {
        console.log('_args1', _args);
        // 将this指向的window 改为指向 当前 div
        context = this
        // _args: getUserAction 传入的 e, 会打印出 mouseEvent 对象
        args = _args
        previous = now();
        if (!timeout) {
          //第一次的时候,timeout为null。如果immediate = true,立即执行回调函数
          timeout = setTimeout(later, wait)

          if (immediate) result = func.apply(context, args)
        }
        return result
      })

      //取消防抖的定时器
      debounced.cancel = function () {
        clearTimeout(timeout)
        timeout = args = context = null
      }
      return debounced
    }

    function restArguments(func, startIndex) {
      startIndex = startIndex == null ? func.length - 1 : +startIndex
      return function () {
        var length = Math.max(arguments.length - startIndex, 0),
          rest = Array(length),
          index = 0
        for (; index < length; index++) {
          rest[index] = arguments[index + startIndex]
        }
        switch (startIndex) {
          case 0: return func.call(this, rest)
          case 1: return func.call(this, arguments[0], rest)
          case 2: return func.call(this, arguments[0], arguments[1], rest)
        }
        var args = Array(startIndex + 1)
        for (index = 0; index < startIndex; index++) {
          args[index] = arguments[index]
        }
        args[startIndex] = rest
        return func.apply(this, args)
      }
    }

总结:

  1. 调试 underscore 源码,了解了防抖的原理
  2. 防抖的使用场景: 适用于输入框远程查询事件,在线文档自动保存,浏览器视口大小改变
  3. 节流的使用场景: 适用于按钮提交事件,页面滚动事件的触发,搜索框联想功能