mqyqingfeng/Blog

JavaScript专题之跟着underscore学防抖

mqyqingfeng opened this issue · 242 comments

前言

在前端开发中会遇到一些频繁的事件触发,比如:

  1. window 的 resize、scroll
  2. mousedown、mousemove
  3. keyup、keydown
    ……

为此,我们举个示例代码来了解事件如何频繁的触发:

我们写个 index.html 文件:

<!DOCTYPE html>
<html lang="zh-cmn-Hans">

<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1">
    <title>debounce</title>
    <style>
        #container{
            width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
        }
    </style>
</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;

我们来看看效果:

debounce

从左边滑到右边就触发了 165 次 getUserAction 函数!

因为这个例子很简单,所以浏览器完全反应的过来,可是如果是复杂的回调函数或是 ajax 请求呢?假设 1 秒触发了 60 次,每个回调就必须在 1000 / 60 = 16.67ms 内完成,否则就会有卡顿出现。

为了解决这个问题,一般有两种解决方案:

  1. debounce 防抖
  2. throttle 节流

防抖

今天重点讲讲防抖的实现。

防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行,真是任性呐!

第一版

根据这段表述,我们可以写第一版的代码:

// 第一版
function debounce(func, wait) {
    var timeout;
    return function () {
        clearTimeout(timeout)
        timeout = setTimeout(func, wait);
    }
}

如果我们要使用它,以最一开始的例子为例:

container.onmousemove = debounce(getUserAction, 1000);

现在随你怎么移动,反正你移动完 1000ms 内不再触发,我才执行事件。看看使用效果:

debounce 第一版

顿时就从 165 次降低成了 1 次!

棒棒哒,我们接着完善它。

this

如果我们在 getUserAction 函数中 console.log(this),在不使用 debounce 函数的时候,this 的值为:

<div id="container"></div>

但是如果使用我们的 debounce 函数,this 就会指向 Window 对象!

所以我们需要将 this 指向正确的对象。

我们修改下代码:

// 第二版
function debounce(func, wait) {
    var timeout;

    return function () {
        var context = this;

        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context)
        }, wait);
    }
}

现在 this 已经可以正确指向了。让我们看下个问题:

event 对象

JavaScript 在事件处理函数中会提供事件对象 event,我们修改下 getUserAction 函数:

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

如果我们不使用 debouce 函数,这里会打印 MouseEvent 对象,如图所示:

MouseEvent

但是在我们实现的 debounce 函数中,却只会打印 undefined!

所以我们再修改一下代码:

// 第三版
function debounce(func, wait) {
    var timeout;

    return function () {
        var context = this;
        var args = arguments;

        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}

到此为止,我们修复了两个小问题:

  1. this 指向
  2. event 对象

立刻执行

这个时候,代码已经很是完善了,但是为了让这个函数更加完善,我们接下来思考一个新的需求。

这个需求就是:

我不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。

想想这个需求也是很有道理的嘛,那我们加个 immediate 参数判断是否是立刻执行。

// 第四版
function debounce(func, wait, immediate) {

    var timeout;

    return function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}

再来看看使用效果:

debounce 第四版

返回值

此时注意一点,就是 getUserAction 函数可能是有返回值的,所以我们也要返回函数的执行结果,但是当 immediate 为 false 的时候,因为使用了 setTimeout ,我们将 func.apply(context, args) 的返回值赋给变量,最后再 return 的时候,值将会一直是 undefined,所以我们只在 immediate 为 true 的时候返回函数的执行结果。

// 第五版
function debounce(func, wait, immediate) {

    var timeout, result;

    return function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) result = func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
        return result;
    }
}

取消

最后我们再思考一个小需求,我希望能取消 debounce 函数,比如说我 debounce 的时间间隔是 10 秒钟,immediate 为 true,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后,取消防抖,这样我再去触发,就可以又立刻执行啦,是不是很开心?

为了这个需求,我们写最后一版的代码:

// 第六版
function debounce(func, wait, immediate) {

    var timeout, result;

    var debounced = function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) result = func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
        return result;
    };

    debounced.cancel = function() {
        clearTimeout(timeout);
        timeout = null;
    };

    return debounced;
}

那么该如何使用这个 cancel 函数呢?依然是以上面的 demo 为例:

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

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

var setUseAction = debounce(getUserAction, 10000, true);

container.onmousemove = setUseAction;

document.getElementById("button").addEventListener('click', function(){
    setUseAction.cancel();
})

演示效果如下:

debounce-cancel

至此我们已经完整实现了一个 underscore 中的 debounce 函数,恭喜,撒花!

演示代码

相关的代码可以在 Github 博客仓库 中找到

专题系列

JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

建议将第四版和第五版换个顺序,在第四版中,由于func始终是异步执行的,return result返回一直是undefined,只有在第五版中immediate参数为true的情况下,result才会取到结果,所以建议换个顺序更加严谨一些。

@wcflmy 确实存在这个问题,非常感谢指出~ o( ̄▽ ̄)d

谢谢您的文章,最近想梳理自己的知识,但是却不知道从哪里入手好,跟着您的文章走真的是事半功倍啊,很多以前的疑点豁然开朗,再次谢谢!

@xxxgitone 我也在梳理自己的知识,与你共勉哈~

写得很好,在做动画时也经常用到这种方式,防止在一帧时间中(大概16ms)渲染多次。

function debounce(func) {
    var t;
    return function () {
        cancelAnimationFrame(t)
        t = requestAnimationFrame(func);
    }
}

@hujiulong requestAnimationFrame 确实是神器呐~

学习了 点赞, 一路学到这里 收获颇丰

第五版有一点不解,为什么要return result

 if (callNow) func.apply(context, args)

直接执行不可以么,望解答,谢谢

@YeaseonZhang 直接执行当然可以呀,之所以 return result ,是考虑到 func 这个函数,可能有返回值,尽管这个功能,我们在实际的开发中基本用不到……但是作为一个工具库,underscore 考虑得会更齐全一点~

跟着大神涨姿势了,我看了一两个小时才算看明白😂

大神厉害了,学习了

请问,您用的什么录屏软件啊,QuickTime吗?

jawil commented

@chenxiaochun 推荐一下我用的。mac 上,我在用Gifox, snagit。但其实,giphy 也不错
https://giphy.com/apps/giphycapture
App Store 直接下

http://recordit.co/ 这个也不错,简单粗暴,就是没有配置选项

还有这个也是免费的,而且 windows mac 都支持。。收费除了 SnagIt(支持 windows 和 mac),还有 Gifox(只支持 mac)

@jawil@mqyqingfeng 发现了另一款截屏录屏神器,推荐给两位啊。不仅功能强大,颜值也很高。http://jietu.qq.com/

@chenxiaochun 我用的是一个 mac 下的叫做 licecap 的录制 GIF 的小软件,免费而且不需要安装,直接打开就能用

用promise应该可以也可以返回setTimeout中回调函数的结果。

@stormqx 确实如此,用 promise 可以实现这个效果,不过 underscore 中没有实现 promise,所以这里也就没有使用 promise,不过说起来,ES6 系列中会讲到从零实现一个 promise ,欢迎关注哈~

写的很好 感谢赐教!

非常感谢分享,受教了!

good!

为什么不来讲解 lodash 呢

学以致用,感谢楼主!

/**
 * Created by Administrator on 2017/10/30.
 */
function setResult(tag, content, color) {
    if(tag && typeof tag == 'object') {
        tag.innerHTML = content;
        tag.style.color = color;
    }
}

var validateEmail = function (e) {
    // 邮箱正则
    var reg = /^[a-z0-9]+(\w|_)+@+([a-z0-9]){2,4}.[a-z]{2,4}$/;
    var currentValue = e.target.value;
    var resultTag = document.getElementById('resultEmail'),
        content = reg.test(currentValue) ? '邮箱正确' : '请输入正确的邮箱',
        color = reg.test(currentValue) ? 'green' : 'red';
    setResult(resultTag, content, color);
}

var validateMobile = function (e) {
    // 手机号正则
    var reg = /^1(3|4|5|7|8){1}[0-9]{9}$/
    var currentValue = e.target.value;
    var resultTag = document.getElementById('resultMobile'),
        content = reg.test(currentValue) ? '手机号正确' : '请输入正确的手机号',
        color = reg.test(currentValue) ? 'green' : 'red';
    setResult(resultTag, content, color);
}

// 防抖
function debounce(func, wait) {
    var timeOut;

    return function () {
        if(timeOut) {
            clearTimeout(timeOut);
        }
        // 保存this上下文,参数
        var that = this, args = arguments;
        timeOut = setTimeout(function () {
            func.apply(that, args);
        }, wait)
    }
}

document.getElementById('emailIpt').onkeyup = debounce(validateEmail, 1000);
document.getElementById('mobileIpt').onkeyup = debounce(validateMobile, 1000);

@lynn1824 感谢分享~ 这个例子非常适合使用防抖~

@dengnan123 因为 underscore 更适合我这种新手读呀~

第五版有一点不解,return result,为什么我得不到返回值?

@qhx123 只有当 immediate 为 true 的时候,才有可能获取返回值,我写个 demo,你可以参照这个例子获取返回值

<!DOCTYPE html>
<html lang="zh-cmn-Hans">

<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1">
    <title>debounce</title>
    <style>
    	#container{
            width: 100%;
            height: 200px;
            line-height: 200px;
            text-align: center;
            color: #fff;
            background-color: #444;
            font-size: 30px;
    	}
    </style>
</head>

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

</html>
// 第五版
function debounce(func, wait, immediate) {

    var timeout, result;

    return function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) result = func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
        return result;
    }
}

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

function getUserAction() {
    container.innerHTML = count++;
    // 函数有返回值
    return '111';
};

container.onmousemove = function(){
   // result 表示返回值
    var result = debounce(getUserAction, 1000, true)();
    console.log(result)
}

代码有问题,当设置了 immediate 为 true的时候,里面代码的逻辑会出现问题, 你这里的 setTimeout 把 timeout = null, 而不会触发最后一次滑动后间隔 一秒之后的那次逻辑。 导致进入执行一次打印 1,结果停下来并不会打印2,而是要再滑动的时候立马打印2,停下来也不会打印3。 跟原本逻辑相悖了。

@Derrys 这个效果跟逻辑是符合的呀,当 immediate 为 true 的时候,需求就是直接触发,只有等待 n 秒后不再触发,你再移动,才会触发的~

default

@Derrys 代码是对的,冴羽大神他是设置了时间,在这段时间之内触发事件是不执行的。我才反应过来。

ezewu commented

@mqyqingfeng 一楼的说:建议将第四版和第五版换个顺序,在第四版中,由于func始终是异步执行的,return result返回一直是undefined,只有在第五版中immediate参数为true的情况下,result才会取到结果,所以建议换个顺序更加严谨一些。现在文章是换过的吗?还是我没看懂,看不明白你们二个在说换什么?还有这个:立刻执行

这个时候,代码已经很是完善了,但是为了让这个函数更加完善,我们接下来思考一个新的需求。

这个需求就是:

我不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。

想想这个需求也是很有道理的嘛,那我们加个 immediate 参数判断是否是立刻执行。
这里我看您的图片是鼠标愰几下,停下来一会儿,数字就加1了,为什么原样复制代码,也没出现这效果,还是gif图有掉帧

@wuyouxin 文章中的内容已经换过了哈。第二个问题,并不是停下来一会,数字就加 1 了,其实是停下来一会后,超过了 n 秒后,你再移动,就执行了下一次的 immediate ,数字才会加 1

@wuyouxin 其实是我把鼠标移出区域这一个操作,导致触发了加 1

ezewu commented

@mqyqingfeng 谢谢楼主,昨天我反复看了一个多小时,后来看懂了,谢谢

@wuyouxin 真是抱歉哈,没有及时回复,让你看了这么久……

大家留下来的问题,不一定能保证及时回复,但是我一定是会回复的~

为什么我在vue单页面 import了这个debounce函数之后,在clearTimeout的时候timout变量一直为undefined,在正常页面用就没问题了

@zjp6049 我也判断不出这个问题所在……如果可以的话,可以写个 demo 一些讨论下~

看了你的博客将近一个多星期了,才知道这个字读冴(ya)

@jasonzhangdong 是第四声的呦~

<template>
  <div class="dashboard">
    <p @mousemove="handlerMouseMove" class="wrapper">{{ num }}</p>
  </div>
</template>

<script type="text/ecmascript-6">
function debounce(func, wait) {
    var timeout;
    return function () {
        clearTimeout(timeout)
        timeout = setTimeout(func, wait);
    }
}
export default {
  data() {
    return {
      num: 0,
    };
  },
  methods: {
    addNum() {
      this.num++
    },
    handlerMouseMove(e) {
      debounce(addNum, 1000)
//    debounce(addNum, 1000)()
    }
  }
};
</script>

<style lang="scss">
.dashboard {
  .wrapper {
    height: 300px;
    background: #ccc;
  }
}
</style>

@zjp6049 这个是 Vue 的示例?

hazxy commented

@mqyqingfeng 请问一下,对于第三版里那个var args = arguments,在arguments这里有点弄不懂了,这个arguments是哪个的?那args和getUserAction函数的参数的区别?我都有一点绕了

@mqyqingfeng 我之前自己混淆的,import进来的函数没有this作用域,还有vue绑定的事件跟原生你写的绑定方式不一样(在触发函数执行的区别:好像原生只绑定了指针,所以debuounce只触发了一次,vue每触发一次事件都重新执行一次新事件,执行了多次)不知道是否理解错了

@hazxy

// 第三版的代码
function debounce(func, wait) {
    var timeout;

    return function () {
        var context = this;
        var args = arguments;

        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}

arguments 在普通函数中就是指 arguments 所在的函数的参数,在这个例子中就是指 return 的这个函数的参数

如果我们这样使用 debounce:

var func = debounce(getUserAction, 1000);
container.onmousemove = func;

arguments 就是指 func 函数执行时的参数,在这里例子中,onmousemove 传给 func 一个参数,这个参数表示事件对象,一般我们用 event 表示。

args 和 getUserAction 函数的参数没有什么区别,因为我们只是把 onmousemove 传给 func 的参数又传递给 getUserAction 而已,所以两者是一样的。

如果有疑问,欢迎留言哈~

@zjp6049 应该是把 debounce 函数执行后的返回函数传递给 @mousemove,我不会 Vue,不知道是不是这样写的:

<template>
  <div class="dashboard">
    <p @mousemove="handlerMouseMove" class="wrapper">{{ num }}</p>
  </div>
</template>

<script type="text/ecmascript-6">
function debounce(func, wait) {
    var timeout;
    return function () {
        clearTimeout(timeout)
        timeout = setTimeout(func, wait);
    }
}

var handlerMouseMove = debounce(addNum, 1000)

export default {
  data() {
    return {
      num: 0,
    };
  },
  methods: {
    addNum() {
      this.num++
    },
    handlerMouseMove: handlerMouseMove
  }
};
</script>

<style lang="scss">
.dashboard {
  .wrapper {
    height: 300px;
    background: #ccc;
  }
}
</style>

@mqyqingfeng 不是的,你这样写拿不到addNum。关键还是要把debounce的执行结果给mousemove绑定,我最后是改成这样了。当然如果要用引入的方式的话,我就用了mixin(注释掉的代码是mixin的)。可能我用vue模仿才这样吧

<template>
  <div class="dashboard">
    <p @mousemove="result" class="wrapper">{{ num }}</p>
  </div>
</template>

<script type="text/ecmascript-6">
function debounce(func, wait) {
    var timeout
    return function () {
        clearTimeout(timeout)
        timeout = setTimeout(func, wait);
    }
}
//import { debounceMixin } from 'utils'
export default {
// mixins: [debounceMixin],
  data() {
    return {
      num: 0,
    };
  },
  created() {
    this.result = debounce(this.addNum, 1000, true)
//  this.result = this.debounce(this.addNum, 1000, true)
  },
  methods: {
    addNum() {
      this.num++
    }
  }
};
</script>

<style lang="scss">
.dashboard {
  .wrapper {
    height: 300px;
    background: #ccc;
  }
}
</style>

@zjp6049 哈哈,解决就好~

hazxy commented

@mqyqingfeng 懂了懂了,看一半一下子就反应过来了,谢谢解惑!

function debounce(func, wait) {
      var timeout;
      return function () {
        var context = this
        var args = arguments

        clearTimeout(timeout)
        timeout = setTimeout(function() {
          func.apply(context, args)
        }, wait)
      }
    }

为什么 debounce 内部不直接写代码,而是要 return function ,这样写的目的是什么,二者在调用的时候有什么不同呢?

@SuperRay3 非常抱歉,我之前没有看到,现在才回复~

先看看如果 return function 我们怎么使用:

var fn = debounce(getUserAction, 1000);
container.onmousemove = f;

debounce(getUserAction, 1000) 会返回一个函数,然后 container mouseover 的时候就会持续的触发这个返回的函数。也可以在 getUserAction 函数中直接写啦,但是直接写的话,改的 getUserAction 这个函数的代码,如果再有一个 getOtherUserAction 函数需要使用防抖,我们就还需要再修改 getOtherUserAction 函数,所以 return function 其实是为了将防抖的代码与原函数代码抽离出来,方便复用。

good job

第五版给的例子不生效的呢。

@Imlisten 感谢 PR 哈~ 第五版的例子,我在本地测试了一下,没有问题呀,当鼠标移动的时候,打印 111 ,就证明该例子生效了~

@mqyqingfeng 嗯,是打印了111, 但是同时防抖的效果也没了。

@Imlisten 哈哈,确实会这样哈,因为要展示返回的值,所以就无法将 debounce 返回的函数赋值给 onmouseover 函数,例子只是证明可以正确返回值~

@mqyqingfeng 不是很明白注释的这句话「// 如果已经执行过,不再执行」

...
     if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            //  不明白这个定时器   没有的话也是可以的呀
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
           
            if (callNow) func.apply(context, args)
        }
...

哈哈,明白了还是要给定时器赋值,在一直触发事件的时候让定时器都true值,定时器还一直排在任务队列的末尾,这样就能做到要等wait的这段时间了。

@baixiaoji 正是如此~

@mqyqingfeng 大神我又来了~ 之前我也有做过类似的防抖措施,是单纯使用定时器实现的,我贴出来代码你看看有哪些问题哈

let container = document.querySelector('#container'),
count = 1,
time,
log = console.log.bind(console);

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

// setTimeout(fn, 0) 表示立即插入队列而不是立即执行
function debounce() {
    count === 1 ? time = setTimeout(getUserAction, 0) : clearTimeout(time); 
    time = setTimeout(getUserAction, 1000)
}

// mousemove 移动完成后触发
container.addEventListener('mousemove', debounce);

@suminhohu 我测试了下效果,没有问题,效果是会第一次触发会立刻执行,然后不再触发 1 秒后会再执行一次,如果非要说些问题的话,就是 debounce 这个函数并没有完全的被抽离出来,如果用在其他的函数上面,我们还需要修改 debounce 函数内部的代码。我们可以一起修改一下:

第一步:deounce 函数内部的 count === 1 可以使用 !time 替代,可以避免与 count 的耦合

var time;
function debounce() {
    !time ? time = setTimeout(getUserAction, 0) : clearTimeout(time); 
    time = setTimeout(getUserAction, 1000)
}

第二步:提取需要做 deounce 的函数和时间作为变量,适应更多的情况:

var time;
function debounce(fn, timer) { 
 return function(){
    !time ? time = setTimeout(fn, 0) : clearTimeout(time); 
    time = setTimeout(fn, timer)
 }
}

第三步:避免 time 成为全局变量,防止意外修改:

    function debounce(fn, timer) {
        var time
        return function() {
            !time ? time = setTimeout(fn, 0) : clearTimeout(time);
            time = setTimeout(fn, timer)
        }
    }

其实跟这篇文章的第一版还蛮像的……

@mqyqingfeng 哇,感谢大神提供思路,我写代码的时候就很少考虑这么好!再次感谢~

TPORL commented

此时注意一点,就是 getUserAction 函数可能是有返回值的,所以我们也要返回函数的执行结果,但是当 immediate 为 false 的时候,因为使用了 setTimeout ,我们将 func.apply(context, args) 的返回值赋给变量,最后再 return 的时候,值将会一直是 undefined,所以我们只在 immediate 为 true 的时候返回函数的执行结果。

其实setTimeout里面把返回值赋给result,只是第一次是undefined(假设函数有返回值),之后每次有效触发,都会得到上一次函数func的返回值。
虽然感觉返回值没什么用...

请教下大神,我把第四版改成这样是否可行呢,里面有没有存在错误


function debounce(func, wait) {
          var timeout;       
          var flag;
          return function () {
              var context = this;
              var args = arguments;
              clearTimeout(timeout)  
              if(!flag) {
                  func.apply(context, args)
                  flag = true;
              } else {
                  timeout = setTimeout(function() {
                      func.apply(context, args)
                  }, wait);
              }         
              
          }
      }

大佬写的文章全是干货 太赞了

第五版定时器没有起到作用,函数里面的调用每次都会初始化time为undefined是不是应该把定时器放到外面或者将timer放到外面,就会好吧

 var result = debounce(getUserAction, 1000, true);
container.onmousemove = function(){
   result();
    console.log(result)
}

刚开始看的时候理解错了以为可以防抖事件。好吧事件是无法防抖的。。。感谢大佬写的系列文章。学习到很多

@TPORL 我也没有找到什么场景会用到返回值,但是 underscore 就这样写了,我也只能按照这个方式讲解了……

@Fromzero828 我去尝试了下这段代码,跟第四版的效果差异还是很大的。这段代码的思路是根据 flag 的值来判断第一次的时候是否立刻执行,然后下次就延时执行,这样的话,实际的效果就是是立刻改变值,然后停止触发 N 秒后又会执行一次,而这一次是没有必要的~ 你可以自己写个例子试验一下,相信你很快就会找到原因~

@dotequiet 不是很明白哈~ debounce 是一个高阶函数,执行后会返回一个函数,而事件触发的时候,反复执行的回调函数就是 debounce 返回的这个函数,所以 timeout 只有在执行 debounce(getUserAction, 1000, true) 才会初始化为 undefined~

此外

 var result = debounce(getUserAction, 1000, true);
container.onmousemove = function(){
   result();
    console.log(result)
}

不就相当于:

var result = debounce(getUserAction, 1000, true);
container.onmousemove = result;

@webjscss 正是如此,“事件是无法防抖的” 😀

@mqyqingfeng
第五版的demo,需要有返回值啦。。

/**
 * 添加函数返回值
 */

// 第五版
function debounce(func, wait, immediate) {

    var timeout, result;

    return function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) result = func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
        return result;
    }
}

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

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

container.onmousemove = function(){
    var result = debounce(getUserAction, 1000, true)();
    console.log(result)
}

@dotequiet 我明白了,确实是这样,感谢指出哈~ o( ̄▽ ̄)d

@mqyqingfeng 请问当getUserAction方法有参数传递时,应该如何写?

@happytoTom

var res = debounce(function(){
    getUserAction(在这里输入参数)
}, 1000);
container.onmousemove = res;

看了之后,在思考可不可以用到防止连点操作。
点击后立即执行,但是在返回之前不会再次触发点击。

楼主的文章写得很棒,收益很多,感谢!发现一个小问题,第四版声明的result应该是在对调第四、五版后留下的笔误吧,楼主有空可以看看。

大大我又来点赞学习了

@jDragonV 如果一定要等到返回数据的时候再次触发估计就需要一个变量来控制状态了

@goodluck2018 非常感谢呀~ 已经修改了,Good Luck 呀~

@imaxing 欢迎哦,希望能对你有所帮助~

前两天阿里面试线上答题考到了防抖函数,我没看过这篇文章,遂挂。。。自己的技术积累还是不够啊!

underscore上的定义:

debounce_.debounce(function, wait, [immediate])
返回 function 函数的防反跳版本, 将延迟函数的执行(真正的执行)在函数最后一次调用时刻的 wait 毫秒之后. 对于必须在一些输入(多是一些用户操作)停止到达之后执行的行为有帮助。 例如: 渲染一个Markdown格式的评论预览, 当窗口停止改变大小之后重新计算布局, 等等.

传参 immediate 为 true, debounce会在 wait 时间间隔的开始调用这个函数 。(注:并且在 waite 的时间之内,不会再次调用。)在类似不小心点了提交按钮两下而提交了两次的情况下很有用。

wbcs commented
function debounce(fn, wait, immediate) {
  let timeout;
  let callback;

  const action = function(...args) {   //节流核心代码
    clearTimeout(timeout);
    timeout = setTimeout(fn.bind(this), wait, ...args);  //解决fn中this、event参数问题
  };

  callback = immediate ?  //第一次是否立即执行
    function(...args) { 
      //解决返回值问题
      return typeof timeout === 'undefined' ?  // timeout是undefined,表示是第一次执行
        (timeout = 0, fn.call(this, ...args)) :  //timeout设为0, 调用fn(this、event问题)
        action.call(this, ...args); //后续调用
    } :
    action;  //否
  
  callback.cancel = function() {
    clearTimeout(timeout);
    timeout = undefined;
  };

  return callback;
}

这样实现是不是能更好一点, 把immediate的判断放在debounce中,这样就不需要连续触发的时候还判断了。

qdDog commented
if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            //  这里我可以这么理解吗。第一次开始执行的时候.timeout是undefined。置反之后所以是true。
           //  可以正常执行到callNow的判断里面。立即执行了。
          // timeout 这个定时器,要过一秒之后才会返回。那之后在一秒之类执行还是会执行到callNow里面的判断才对呀。。。
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
           
            if (callNow) func.apply(context, args)
        }

在 vue里面使用的时候 apply 好像把context丢掉了, 因为 func.apply(context, args) 的 args 并没有 context

change: helper.debounce(item => {
            console.log(item);
            this.$emit('cartChange', item);  // $emit is undefined,because this is undefined.
        }, 1000, false)

我有一个疑问,下面这种方法的简单实现,是防抖吗,或者说有什么不好?因为我看到好多是用定时器实现的,所以又这个疑问。

function debounce(fn, wait) {
            var time = 0;
            return function() {
                var context = this;
                var args = [].slice.call(arguments);
                var nowTime = Date.now();
                if(nowTime - time >= wait) {
                    fn.apply(context, args);
                }
                time = Date.now();
            }
        }

第四版中的定时器里,timout = null 上面少了一句 func.apply(context, args),不然不会执行最后一次的触发

@LeeChar 加上这句,如果不是最后一次触发,会出现快速接连两次触发的情况

您好,我想问一下,不知是我自己的问题,还是设计本就是这样,在立即执行这个功能上,第一次触发之后继续触发就没有了后续的执行。是不是因为不同需求的原因,根据不同的需求去选择合适的功能。比如在鼠标划动上可以添加上立即触发功能而放弃最后一次的触发,在搜索框上面则不需要立即触发功能,用最后一次的触发来实现防抖功能???

您好,我想问一下,不知是我自己的问题,还是设计本就是这样,在立即执行这个功能上,第一次触发之后继续触发就没有了后续的执行。是不是因为不同需求的原因,根据不同的需求去选择合适的功能。比如在鼠标划动上可以添加上立即触发功能而放弃最后一次的触发,在搜索框上面则不需要立即触发功能,用最后一次的触发来实现防抖功能???

刚看到这个邮箱,有点疑惑。。。我github没玩转,好像是恢复我的;是这样的,我跟着例子写代码的时候,看见动图显示最后一次触发了;可是我照着写的例子却并没有触发最后一次,在那上面加了一个执行语句最后一次就执行了;可能有点误解吧,或者也许我没有看进去。而且。。。。我没看懂你想表达的意思,尴尬额

@wb421768544
你的这段代码和楼主的代码功能上还是有一点区别。
这段代码核心是调用 action 函数

const action = function(...args) {   //节流核心代码
  clearTimeout(timeout);
  timeout = setTimeout(fn.bind(this), wait, ...args);  //解决fn中this、event参数问题
};

action 函数中 setTimeout 超时则会执行fn.bind(this)

举个例子,debounce(fn, 5000, true) 第一次触发时立即执行 fn 函数。第二次触发时,会注册一个定时器,定时器会在 5s 后执行 fn 函数。第三次触发时,如果上一个定时器还未超时,则取消上一个定时器,重新注册一个 5s 的定时器。

简单来说,如果快速地触发该函数两次,第一次立即执行,第二次会延迟执行

而在楼主的代码中,当 immediate 为true时,会执行下面这段代码

timeout = setTimeout(function(){
  timeout = null;
}, wait);

同样举个例子,debounce(fn, 5000, true) 第一次触发时立即执行 fn 函数。并且每次触发时都会取消上一个定时器并再注册一个定时器,定时器会在 5s 后执行 timeout = null。从下面的代码可以看出,停止触发5秒后,才可以重新触发执行 fn 函数

var callNow = !timeout;
...
...
if (callNow) func.apply(context, args)

简单来说,如果快速地触发该函数两次,第一次立即执行,第二次不会执行也不会延迟执行

@minghu0902
你的这段代码是 throttle 节流。

// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait);你好,请问这 timeout = null;是不是不写也可以,毕竟timeout 只想要一个返回值

function debounce(func, wait) {
  let timeout
  return function() {
    let context = arguments
    clearTimeout(timeout)
    timeout = setTimeout(() => {func.apply(this, context)}, wait)
  }
}

为什么这里要把arguments传到func里,我直接这样把e传到func里不行,请指教

function debounce(func, wait) {
  let timeout
  return function(e) {
    clearTimeout(timeout)
    timeout = setTimeout(() => {func.apply(this, e)}, wait)
  }
}

我又来学习这块的知识啦,确实很不错,之前看过有点囫囵吞枣,还是应该彻底弄懂理解加以应用方能掌握

请问楼主function debounce(func, wait) {
var timeout;
return function () {
clearTimeout(timeout)
timeout = setTimeout(func, wait);
}
}为什么要return function,去掉return function,直接执行clearTimeout和setTimeout可以吗

// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait);你好,请问这 timeout = null;是不是不写也可以,毕竟timeout 只想要一个返回值

不可以 clearTimeout 不会让timeout =null ,时间到了 callNow 不会为true

请问楼主function debounce(func, wait) {
var timeout;
return function () {
clearTimeout(timeout)
timeout = setTimeout(func, wait);
}
}为什么要return function,去掉return function,直接执行clearTimeout和setTimeout可以吗

debounce 是一开始就执行的,每次移动事件促发执行的是function 啊,像你这样写timeout 声明 就只能放debounce 外面了,这样就不算封装了吧,timeout 是所有事件促发共用的变量。

最后一个取消代码 有点问题

  1. 在 第一次进入的时候 immediate 没有设置成false 这样的话 只会执行一次.
  2. 取消方法里面没有将 immediate设置成false 出发取消事件后 并不能立即执行方法.
function debounce(func, wait) {
  let timeout
  return function() {
    let context = arguments
    clearTimeout(timeout)
    timeout = setTimeout(() => {func.apply(this, context)}, wait)
  }
}

为什么这里要把arguments传到func里,我直接这样把e传到func里不行,请指教

function debounce(func, wait) {
  let timeout
  return function(e) {
    clearTimeout(timeout)
    timeout = setTimeout(() => {func.apply(this, e)}, wait)
  }
}

@yin-shu 我一开始还蒙了,不过看清楚后,那个settimeout里面的函数是定义的,就是说参数是形式参数,而不是真正运行时候的参数,你把那个形式参数去掉之后又可以打印出来了e,这个时候才是闭包.不知道你听得懂不?