Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

事件节流 #22

Open
Jmingzi opened this issue Jan 5, 2018 · 0 comments
Open

事件节流 #22

Jmingzi opened this issue Jan 5, 2018 · 0 comments

Comments

@Jmingzi
Copy link
Owner

Jmingzi commented Jan 5, 2018

Throttle

概念

事件节流是指控制事件在一段时间内只执行一次,比如window的resize/scroll,还比如mousemovetouhchmove,还有输入框的input/change,当然输入框的更适合防抖debounce去做。

简单实现

throttle的实现方式

  1. 对比前后的时间戳,再去执行
  2. setTimeout定时器
// common callback
function cl(e) {
  console.log('resize callback', e)
}

// 方法1
function throttle_1 (fn, wait) {
  // 初始化start时间
  let previous = 0

  return function() {
    let now = new Date().getTime()
    
    if (previous === 0) {
      previous = now
      // 第一次调用
      // 可以用参数控制第一次是否执行
      fn.apply(this, arguments)
    }

    // 时间大于wait
    if (now - previous > wait) {
      fn.apply(this, arguments)
      // 记录执行结束的时间
      previous = now
    }
  }
}
window.addEventListener('resize', throttle_1(cl, 2000))

// 方案2
// 网上很多都说定时器可以实现,但其实是实现不了节流,可以实现防抖,
function throttle_2 (fn, wait) {
  let timer = null

  return function() {
    let context = this
    let args = arguments

    if (timer) {
      // 清除timer后,又得等待wait时间后才执行
      clearTimeout(timer)
      // clearTimeout后timer并不会清除,需手动置为null
      timer = null
    }

    // 第一次初始化
    // 可以选择是否直接执行,还是在wait后执行
    // 如果一直hold住,就始终不会回调,就和debounce需要的一毛一样
    timer = setTimeout(()=> {
      fn.apply(context, args)
    }, wait)
  }
}
window.addEventListener('resize', throttle_2(cl, 2000))

简单实现的问题

  • 方法一:初始化都会调一次,而结束的时候不会回调。除非结束的时间差刚好为你需要的wait值
  • 方法二: 初始化的时候不会调用,而结束的时候始终都会调一次,因为是定时器。

underscore实现方式

虽然定时器不能实现节流,但是可以配合方法1来更好的解决上述的问题。

underscore的节流可以控制头尾的回调是否执行,也就是第三个参数options

{
  // 首次不回调
  leading: false,
  // 最后不回调
  trailing: false
}

让我们来逐行分析它是如何结合2者的(以下为拷贝的underscore源码)

// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
_.throttle = function(func, wait, options) {

  // 初始化声明缓存变量,利用了闭包变量不销毁的原理,始终保持在内存中
  // 为了防止内存泄漏,执行完成后一要将变量置为null
  var timeout, context, args, result;
  var previous = 0;
  if (!options) options = {};

  var later = function() {
    // 初次回调不执行将previous = 0,也就是previous = now
    previous = options.leading === false ? 0 : _.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };

  var throttled = function() {
    // 事件回调时间
    var now = _.now();
    
    // 判断初次回调 是否关闭
    if (!previous && options.leading === false) previous = now;

    // 执行下次回调需要等待的时间
    // 1.如果初次不回调,则remaining === wait,也就是previous = now
    // 2.初次回调 remaining < 0
    // 3.手动调整客户端的时间后,remaining > wait
    var remaining = wait - (now - previous);

    context = this;
    args = arguments;

    if (remaining <= 0 || remaining > wait) {
      // 初次回调执行

      // 此处的timeout可能有值
      // 是上一次回调的timeout
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      
      // 记录回调执行的时间
      previous = now;
      result = func.apply(context, args);

      // 手动置为null
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      // 如果已经存在一个定时器,则不会进入该 if 分支
      // 初次回调不执行
      // 末尾回调执行
      timeout = setTimeout(later, remaining);
    }
    return result;
  };

  throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = context = args = null;
  };

  return throttled;
}
window.addEventListener('resize', _.throttle(cl, 2000))

总结

underscore的节流函数考虑到了很多方面:

  1. 首尾回调可传参控制

  2. 一定时间间隔内只会触发回调一次,且间隔的时间是动态计算的,也就是说定时器的时间是动态的。上面写的方法2中说的定时器实现不了的问题,hold住不放,定时器会从头计算等待的时间,再开始触发回调

  3. 计算时间差与定时器来解决的好处就是可以动态赋值定时器的remaining time,这也是二者结合的最根本。

参考

underscore 函数节流的实现
JS魔法堂:函数节流(throttle)与函数去抖(debounce)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant