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

由underscore函数去抖看debounce的演进 #5

Open
zhengzheming opened this issue Jan 4, 2018 · 0 comments
Open

由underscore函数去抖看debounce的演进 #5

zhengzheming opened this issue Jan 4, 2018 · 0 comments

Comments

@zhengzheming
Copy link
Owner

zhengzheming commented Jan 4, 2018

debounce函数涉及的知识点: 闭包、作用域链、setTImeout、函数去抖

在《js高程》中有关于函数去抖的一段代码:

function debounce(method, context) {
  clearTimeout(method.tId);
  method.timer= setTimeout(function() {
    method.call(context);
  }, 1000);
}

function print() {
  console.log('hello world');
}

window.onscroll = function() {
  debounce(print);
};

该实现在最后一次滚动事件时调用method函数,但有两个可以优化的地方:
1、timer绑定到了method的属性上, 这要求method不能为匿名函数;
2、频繁交替调用clearTimeout、setTimeout, 对性能有一定的牺牲;

这两个缺点均能很好的解决。

先看第一处优化:

function debounce (method, context) {
  let timeout
  return function () {
    clearTimeout(timer)
    timer = setTimeout(method.call(context), 1000)
  }
};

function print() {
  console.log('hello world');
};

window.onscroll = _debounce(print)

第二处优化的方案:

function debounce(method, context) {
  let timeout, timestamp
  let wait = 1000
  function later(timestamp) {
    let last = new Date().getTime() - timestamp
    if (last < wait) {
      timeout = setTimeout(later, wait - last)
    } else {
      method.call(context)
      timeout = null
    }
  }
  return function innerDebunce() {
    timestamp = new Date().getTime()
    if (!timeout) {
      timeout = setTimeout(later, wait)
    }
  }
}

function print() {
  console.log('hello world');
};

window.onscroll = _debounce(print)

流程图
该方案在innerDebunce函数执行时,更新每一次执行的时间并记录到timestamp,当第一次timeout结束时,将结束时的时间与timestamp对比来决定是否立即执行method还是延迟执行method, 将重置定时器带来的性能损失降至最低。思路不可谓不巧妙。

underscore、lodash这些库均对debounce做出了相应的优化, 试看underscore源码:

// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
// 函数去抖(连续事件触发结束后只触发一次)
// sample 1: _.debounce(function(){}, 1000)
// 连续事件结束后的 1000ms 后触发
// sample 1: _.debounce(function(){}, 1000, true)
// 连续事件触发后立即触发(此时会忽略第二个参数)
_.debounce = function(func, wait, immediate) {
  var timeout, args, context, timestamp, result;

  var later = function() {
    // 定时器设置的回调 later 方法的触发时间,和连续事件触发的最后一次时间戳的间隔
    // 如果间隔为 wait(或者刚好大于 wait),则触发事件
    var last = _.now() - timestamp;

    // 时间间隔 last 在 [0, wait) 中
    // 还没到触发的点,则继续设置定时器
    // last 值应该不会小于 0 吧?
    if (last < wait && last >= 0) {
      timeout = setTimeout(later, wait - last);
    } else {
      // 到了可以触发的时间点
      timeout = null;
      // 可以触发了
      // 并且不是设置为立即触发的
      // 因为如果是立即触发(callNow),也会进入这个回调中
      // 主要是为了将 timeout 值置为空,使之不影响下次连续事件的触发
      // 如果不是立即执行,随即执行 func 方法
      if (!immediate) {
        // 执行 func 函数
        result = func.apply(context, args);
        // 这里的 timeout 一定是 null 了吧
        // 感觉这个判断多余了
        if (!timeout)
          context = args = null;
      }
    }
  };

  // 嗯,闭包返回的函数,是可以传入参数的
  return function() {
    // 可以指定 this 指向
    context = this;
    args = arguments;

    // 每次触发函数,更新时间戳
    // later 方法中取 last 值时用到该变量
    // 判断距离上次触发事件是否已经过了 wait seconds 了
    // 即我们需要距离最后一次触发事件 wait seconds 后触发这个回调方法
    timestamp = _.now();

    // 立即触发需要满足两个条件
    // immediate 参数为 true,并且 timeout 还没设置
    // immediate 参数为 true 是显而易见的
    // 如果去掉 !timeout 的条件,就会一直触发,而不是触发一次
    // 因为第一次触发后已经设置了 timeout,所以根据 timeout 是否为空可以判断是否是首次触发
    var callNow = immediate && !timeout;

    // 设置 wait seconds 后触发 later 方法
    // 无论是否 callNow(如果是 callNow,也进入 later 方法,去 later 方法中判断是否执行相应回调函数)
    // 在某一段的连续触发中,只会在第一次触发时进入这个 if 分支中
    if (!timeout)
      // 设置了 timeout,所以以后不会进入这个 if 分支了
      timeout = setTimeout(later, wait);

    // 如果是立即触发
    if (callNow) {
      // func 可能是有返回值的
      result = func.apply(context, args);
      // 解除引用
      context = args = null;
    }

    return result;
  };
};

可以看到, underscore中的debounce函数在优化了之前缺点后, 加上了对函数immediate调用的支持。
Refers:
underscore 函数去抖的实现

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