前端学习

前端学习

@色少7年前

05/11
19:17
前端技能

函数节流(throttle)与函数去抖(debounce)

项目优化种发现很多时候需要输入之后验证,因此用得频率比较高的2个函数debounce和throttle。还有以下场景往往由于事件频繁被触发,因而频繁执行DOM操作、资源加载等重行为,导致UI停顿甚至浏览器崩溃。

1. window对象的resize、scroll事件

2. 拖拽时的mousemove事件

3. 射击游戏中的mousedown、keydown事件

4. 文字输入、自动完成的keyup事件

实际上对于window的resize事件,实际需求大多为停止改变大小n毫秒后执行后续处理;而其他事件大多的需求是以一定的频率执行后续处理。针对这两种需求就出现了debounce和throttle两种解决办法。

debounce

debounce的关注点是空闲的间隔时间

 // 函数去抖(连续事件触发结束后只触发一次)
 // sample 1: _.debounce(function(){}, 1000)
 // 连续事件结束后的 1000ms 后触发
 // sample 1: _.debounce(function(){}, 1000, true)
 // 连续事件触发后立即触发(此时会忽略第二个参数)

_.debounce = function(func, wait, immediate) {
    // immediate默认为false
    var timeout, args, context, timestamp, result;

    var later = function() {
      // 当wait指定的时间间隔期间多次调用_.debounce返回的函数,则会不断更新timestamp的值,导致last < wait && last >= 0一直为true,从而不断启动新的计时器延时执行func
      var last = _.now() - timestamp;

      if (last < wait && last >= 0) {
        timeout = setTimeout(later, wait - last);
      } else {
        timeout = null;
        if (!immediate) {
          result = func.apply(context, args);
          if (!timeout) context = args = null;
        }
      }
    };

    return function() {
      context = this;
      args = arguments;
      timestamp = _.now();
      // 第一次调用该方法时,且immediate为true,则调用func函数
      var callNow = immediate && !timeout;
      // 在wait指定的时间间隔内首次调用该方法,则启动计时器定时调用func函数
      if (!timeout) timeout = setTimeout(later, wait);
      if (callNow) {
        result = func.apply(context, args);
        context = args = null;
      }

      return result;
    };
  };

Throttle

throttle 的关注点是连续的执行间隔时间

// 函数节流(如果有连续事件响应,则每间隔一定时间段触发) 
// 每间隔 wait(Number) milliseconds 触发一次 func 方法
// 如果 options 参数传入 {leading: false} 
// 那么不会马上触发(等待 wait milliseconds 后第一次触发 func) // 如果 options 参数传入 {trailing: false}
// 那么最后一次回调不会被触发
// **Notice: options 不能同时设置 leading 和 trailing 为 false** 
// 示例: 
// var throttled = _.throttle(updatePosition, 100);
// $(window).scroll(throttled); 
// 调用方式(注意看 A 和 B console.log 打印的位置):
// _.throttle(function, wait, [options]) 
// sample 1: _.throttle(function(){}, 1000)
// print: A, B, B, B ... 
// sample 2: _.throttle(function(){}, 1000, {leading: false}) 
// print: B, B, B, B ... 
// sample 3: _.throttle(function(){}, 1000, {trailing: false}) 
// print: A, A, A, A ...
 // ----------------------------------------- //


_.throttle = function(func, wait, options) { 

/* options的默认值 * 表示首次调用返回值方法时,会马上调用func;否则仅会记录当前时刻,当第二次调用的时间间隔超过wait时,才调用func。
 * options.leading = true; 
 * 表示当调用方法时,未到达wait指定的时间间隔,则启动计时器延迟调用func函数,若后续在既未达到wait指定的时间间隔和func函数又未被调用的情况下调用返回值方法,则被调用请求将被丢弃。
 * options.trailing = true; 
 * 注意:当options.trailing = false时,效果与上面的简单实现效果相同 
*/

   var context, args, result;
    var timeout = null;
    var previous = 0;
    if (!options) options = {};
    var later = function() {
      previous = options.leading === false ? 0 : _.now();
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      var now = _.now();
      if (!previous && options.leading === false) previous = now;
      // 计算剩余时间
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 当到达wait指定的时间间隔,则调用func函数
      // 精彩之处:按理来说remaining <= 0已经足够证明已经到达wait的时间间隔,但这里还考虑到假如客户端修改了系统时间则马上执行func函数。
      if (remaining <= 0 || remaining > wait) {
        // 由于setTimeout存在最小时间精度问题,因此会存在到达wait的时间间隔,但之前设置的setTimeout操作还没被执行,因此为保险起见,这里先清理setTimeout操作
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // options.trailing=true时,延时执行func函数
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };

throttle 应用场景

  • DOM 元素的拖拽功能实现(mousemove)
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
  • 计算鼠标移动的距离(mousemove)
  • Canvas 模拟画板功能(mousemove)
  • 搜索联想(keyup)
  • 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次

debounce 应用场景
函数去抖有哪些应用场景?哪些时候对于连续的事件响应我们只需要执行一次回调?

  • 每次 resize/scroll 触发统计事件
  • 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)

函数节流(throttle)与函数去抖(debounce)

@色少7年前

05/11
13:35
前端技能

深入剖析 JavaScript 的深复制

http://www.cnblogs.com/charling/p/3452677.html
jQuery.extend()源码解读深拷贝或者浅拷贝
http://jerryzou.com/posts/dive-into-deep-clone-in-javascript/
深入剖析JavaScript 的深复制

记录如下:
1、jQuery.clone()——dom对象的复制
在 jQuery 中也有这么一个叫

1
$.clone()

的方法,可是它并不是用于一般的 JS 对象的深复制,而是用于 DOM 对象。
2、jQuery.extend()——深复制(递归extend)
3、lodash —— _.clone() / _.cloneDeep()

1
_.clone(obj, true)

等价于

1
_.cloneDeep(obj)

比较各个深复制方法

特性 jQuery lodash JSON.parse 所谓“拥抱未来的深复制实现”
浏览器兼容性 IE6+ (1.x) & IE9+ (2.x) IE6+ (Compatibility) & IE9+ (Modern) IE8+ IE9+
能够深复制存在环的对象 抛出异常 RangeError: Maximum call stack size exceeded 支持 抛出异常 TypeError: Converting circular structure to JSON 支持
对 Date, RegExp 的深复制支持 × 支持 × 支持
对 ES6 新引入的标准对象的深复制支持 × 支持 × ×
复制数组的属性 × 仅支持RegExp#exec返回的数组结果 × 支持
是否保留非源生对象的类型 × × × 支持
复制不可枚举元素 × × × ×
复制函数 × × × ×

深入剖析 JavaScript 的深复制