{"action":"created","issue":{"url":"https://api.github.com/repos/mqyqingfeng/Blog/issues/22","repository_url":"https://api.github.com/repos/mqyqingfeng/Blog","labels_url":"https://api.github.com/repos/mqyqingfeng/Blog/issues/22/labels{/name}","comments_url":"https://api.github.com/repos/mqyqingfeng/Blog/issues/22/comments","events_url":"https://api.github.com/repos/mqyqingfeng/Blog/issues/22/events","html_url":"https://github.com/mqyqingfeng/Blog/issues/22","id":233163700,"number":22,"title":"JavaScript专题之跟着underscore学防抖","user":{"login":"mqyqingfeng","id":11458263,"avatar_url":"https://avatars0.githubusercontent.com/u/11458263?v=3","gravatar_id":"","url":"https://api.github.com/users/mqyqingfeng","html_url":"https://github.com/mqyqingfeng","followers_url":"https://api.github.com/users/mqyqingfeng/followers","following_url":"https://api.github.com/users/mqyqingfeng/following{/other_user}","gists_url":"https://api.github.com/users/mqyqingfeng/gists{/gist_id}","starred_url":"https://api.github.com/users/mqyqingfeng/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mqyqingfeng/subscriptions","organizations_url":"https://api.github.com/users/mqyqingfeng/orgs","repos_url":"https://api.github.com/users/mqyqingfeng/repos","events_url":"https://api.github.com/users/mqyqingfeng/events{/privacy}","received_events_url":"https://api.github.com/users/mqyqingfeng/received_events","type":"User","site_admin":false},"labels":[{"id":618327137,"url":"https://api.github.com/repos/mqyqingfeng/Blog/labels/%E4%B8%93%E9%A2%98%E7%B3%BB%E5%88%97","name":"专题系列","color":"0071b5","default":false}],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":4,"created_at":"2017-06-02T12:11:09Z","updated_at":"2017-06-20T01:55:47Z","closed_at":null,"body":"## 前言\r\n\r\n在前端开发中会遇到一些频繁的事件触发，比如：\r\n\r\n1. window 的 resize、scroll\r\n2. mousedown、mousemove\r\n3. keyup、keydown\r\n……\r\n\r\n为此，我们举个示例代码来了解事件如何频繁的触发：\r\n\r\n我们写个 `index.html` 文件：\r\n\r\n```html\r\n<!DOCTYPE html>\r\n<html lang=\"zh-cmn-Hans\">\r\n\r\n<head>\r\n    <meta charset=\"utf-8\">\r\n    <meta http-equiv=\"x-ua-compatible\" content=\"IE=edge, chrome=1\">\r\n    <title>debounce</title>\r\n    <style>\r\n        #container{\r\n            width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;\r\n        }\r\n    </style>\r\n</head>\r\n\r\n<body>\r\n    <div id=\"container\"></div>\r\n    <script src=\"debounce.js\"></script>\r\n</body>\r\n\r\n</html>\r\n```\r\n\r\n`debounce.js` 文件的代码如下：\r\n\r\n```js\r\nvar count = 1;\r\nvar container = document.getElementById('container');\r\n\r\nfunction getUserAction() {\r\n    container.innerHTML = count++;\r\n};\r\n\r\ncontainer.onmousemove = getUserAction;\r\n```\r\n\r\n我们来看看效果：\r\n\r\n![debounce](https://github.com/mqyqingfeng/Blog/raw/master/Images/debounce/debounce.gif)\r\n\r\n从左边滑到右边就触发了 165 次 getUserAction 函数！\r\n\r\n因为这个例子很简单，所以浏览器完全反应的过来，可是如果是复杂的回调函数或是 ajax 请求呢？假设 1 秒触发了 60 次，每个回调就必须在 1000 / 60 = 16.67ms 内完成，否则就会有卡顿出现。\r\n\r\n为了解决这个问题，一般有两种解决方案：\r\n\r\n1. debounce 防抖\r\n2. throttle 节流\r\n\r\n## 防抖\r\n\r\n今天重点讲讲防抖的实现。\r\n\r\n防抖的原理就是：你尽管触发事件，但是我一定在事件触发 n 秒后才执行，如果你在一个事件触发的 n 秒内又触发了这个事件，那我就以新的事件的时间为准，n 秒后才执行，总之，就是要等你触发完事件 n 秒内不再触发事件，我才执行，真是任性呐!\r\n\r\n## 第一版\r\n\r\n根据这段表述，我们可以写第一版的代码：\r\n\r\n```js\r\n// 第一版\r\nfunction debounce(func, wait) {\r\n    var timeout;\r\n    return function () {\r\n        clearTimeout(timeout)\r\n        timeout = setTimeout(func, wait);\r\n    }\r\n}\r\n```\r\n\r\n如果我们要使用它，以最一开始的例子为例：\r\n\r\n```js\r\ncontainer.onmousemove = debounce(getUserAction, 1000);\r\n```\r\n\r\n现在随你怎么移动，反正你移动完 1000ms 内不再触发，我才执行事件。看看使用效果：\r\n\r\n![debounce 第一版](https://github.com/mqyqingfeng/Blog/raw/master/Images/debounce/debounce-1.gif)\r\n\r\n顿时就从 165 次降低成了 1 次!\r\n\r\n棒棒哒，我们接着完善它。\r\n\r\n## this\r\n\r\n如果我们在 `getUserAction` 函数中 `console.log(this)`，在不使用 `debounce` 函数的时候，`this` 的值为：\r\n\r\n```html\r\n<div id=\"container\"></div>\r\n```\r\n\r\n但是如果使用我们的 debounce 函数，this 就会指向 Window 对象！\r\n\r\n所以我们需要将 this 指向正确的对象。\r\n\r\n我们修改下代码：\r\n\r\n```js\r\n// 第二版\r\nfunction debounce(func, wait) {\r\n    var timeout;\r\n\r\n    return function () {\r\n        var context = this;\r\n\r\n        clearTimeout(timeout)\r\n        timeout = setTimeout(function(){\r\n            func.apply(context)\r\n        }, wait);\r\n    }\r\n}\r\n```\r\n\r\n现在 this 已经可以正确指向了。让我们看下个问题：\r\n\r\n## event 对象\r\n\r\nJavaScript 在事件处理函数中会提供事件对象 event，我们修改下 getUserAction 函数：\r\n\r\n```js\r\nfunction getUserAction(e) {\r\n    console.log(e);\r\n    container.innerHTML = count++;\r\n};\r\n```\r\n\r\n如果我们不使用 debouce 函数，这里会打印 MouseEvent 对象，如图所示：\r\n\r\n![MouseEvent](https://github.com/mqyqingfeng/Blog/raw/master/Images/debounce/event.png)\r\n\r\n但是在我们实现的 debounce 函数中，却只会打印 undefined!\r\n\r\n所以我们再修改一下代码：\r\n\r\n```js\r\n// 第三版\r\nfunction debounce(func, wait) {\r\n    var timeout;\r\n\r\n    return function () {\r\n        var context = this;\r\n        var args = arguments;\r\n\r\n        clearTimeout(timeout)\r\n        timeout = setTimeout(function(){\r\n            func.apply(context, args)\r\n        }, wait);\r\n    }\r\n}\r\n```\r\n\r\n到此为止，我们修复了两个小问题：\r\n\r\n1. this 指向\r\n2. event 对象\r\n\r\n## 立刻执行\r\n\r\n这个时候，代码已经很是完善了，但是为了让这个函数更加完善，我们接下来思考一个新的需求。\r\n\r\n这个需求就是：\r\n\r\n我不希望非要等到事件停止触发后才执行，我希望立刻执行函数，然后等到停止触发 n 秒后，才可以重新触发执行。\r\n\r\n想想这个需求也是很有道理的嘛，那我们加个 immediate 参数判断是否是立刻执行。\r\n\r\n```js\r\n// 第四版\r\nfunction debounce(func, wait, immediate) {\r\n\r\n    var timeout, result;\r\n\r\n    return function () {\r\n        var context = this;\r\n        var args = arguments;\r\n\r\n        if (timeout) clearTimeout(timeout);\r\n        if (immediate) {\r\n            // 如果已经执行过，不再执行\r\n            var callNow = !timeout;\r\n            timeout = setTimeout(function(){\r\n                timeout = null;\r\n            }, wait)\r\n            if (callNow) func.apply(context, args)\r\n        }\r\n        else {\r\n            timeout = setTimeout(function(){\r\n                func.apply(context, args)\r\n            }, wait);\r\n        }\r\n    }\r\n}\r\n```\r\n\r\n再来看看使用效果：\r\n\r\n![debounce 第四版](https://github.com/mqyqingfeng/Blog/raw/master/Images/debounce/debounce-4.gif)\r\n\r\n## 返回值\r\n\r\n此时注意一点，就是 getUserAction 函数可能是有返回值的，所以我们也要返回函数的执行结果，但是当 immediate 为 false 的时候，因为使用了 setTimeout ，我们将 func.apply(context, args) 的返回值赋给变量，最后再 return 的时候，值将会一直是 undefined，所以我们只在 immediate 为 true 的时候返回函数的执行结果。 \r\n\r\n```js\r\n// 第五版\r\nfunction debounce(func, wait, immediate) {\r\n\r\n    var timeout, result;\r\n\r\n    return function () {\r\n        var context = this;\r\n        var args = arguments;\r\n\r\n        if (timeout) clearTimeout(timeout);\r\n        if (immediate) {\r\n            // 如果已经执行过，不再执行\r\n            var callNow = !timeout;\r\n            timeout = setTimeout(function(){\r\n                timeout = null;\r\n            }, wait)\r\n            if (callNow) result = func.apply(context, args)\r\n        }\r\n        else {\r\n            timeout = setTimeout(function(){\r\n                func.apply(context, args)\r\n            }, wait);\r\n        }\r\n        return result;\r\n    }\r\n}\r\n```\r\n\r\n## 取消\r\n\r\n最后我们再思考一个小需求，我希望能取消 debounce 函数，比如说我 debounce 的时间间隔是 10 秒钟，immediate 为 true，这样的话，我只有等 10 秒后才能重新触发事件，现在我希望有一个按钮，点击后，取消防抖，这样我再去触发，就可以又立刻执行啦，是不是很开心？\r\n\r\n为了这个需求，我们写最后一版的代码：\r\n\r\n```js\r\n// 第六版\r\nfunction debounce(func, wait, immediate) {\r\n\r\n    var timeout, result;\r\n\r\n    var debounced = function () {\r\n        var context = this;\r\n        var args = arguments;\r\n\r\n        if (timeout) clearTimeout(timeout);\r\n        if (immediate) {\r\n            // 如果已经执行过，不再执行\r\n            var callNow = !timeout;\r\n            timeout = setTimeout(function(){\r\n                timeout = null;\r\n            }, wait)\r\n            if (callNow) result = func.apply(context, args)\r\n        }\r\n        else {\r\n            timeout = setTimeout(function(){\r\n                func.apply(context, args)\r\n            }, wait);\r\n        }\r\n        return result;\r\n    };\r\n\r\n    debounced.cancel = function() {\r\n        clearTimeout(timeout);\r\n        timeout = null;\r\n    };\r\n\r\n    return debounced;\r\n}\r\n```\r\n\r\n那么该如何使用这个 cancel 函数呢？依然是以上面的 demo 为例：\r\n\r\n```js\r\nvar count = 1;\r\nvar container = document.getElementById('container');\r\n\r\nfunction getUserAction(e) {\r\n    container.innerHTML = count++;\r\n};\r\n\r\nvar setUseAction = debounce(getUserAction, 10000, true);\r\n\r\ncontainer.onmousemove = setUseAction;\r\n\r\ndocument.getElementById(\"button\").addEventListener('click', function(){\r\n    setUseAction.cancel();\r\n})\r\n```\r\n\r\n演示效果如下：\r\n\r\n![debounce-cancel](https://raw.githubusercontent.com/mqyqingfeng/Blog/master/Images/debounce/debounce-cancel.gif)\r\n\r\n至此我们已经完整实现了一个 underscore 中的 debounce 函数，恭喜，撒花！\r\n\r\n## 演示代码\r\n\r\n相关的代码可以在 [Github 博客仓库](https://github.com/mqyqingfeng/Blog/tree/master/demos/debounce) 中找到\r\n\r\n## 专题系列\r\n\r\nJavaScript专题系列目录地址：[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。\r\n\r\nJavaScript专题系列预计写二十篇左右，主要研究日常开发中一些功能点的实现，比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等，特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。\r\n\r\n如果有错误或者不严谨的地方，请务必给予指正，十分感谢。如果喜欢或者有所启发，欢迎 star，对作者也是一种鼓励。\r\n"},"comment":{"url":"https://api.github.com/repos/mqyqingfeng/Blog/issues/comments/309622241","html_url":"https://github.com/mqyqingfeng/Blog/issues/22#issuecomment-309622241","issue_url":"https://api.github.com/repos/mqyqingfeng/Blog/issues/22","id":309622241,"user":{"login":"hujiulong","id":13267437,"avatar_url":"https://avatars2.githubusercontent.com/u/13267437?v=3","gravatar_id":"","url":"https://api.github.com/users/hujiulong","html_url":"https://github.com/hujiulong","followers_url":"https://api.github.com/users/hujiulong/followers","following_url":"https://api.github.com/users/hujiulong/following{/other_user}","gists_url":"https://api.github.com/users/hujiulong/gists{/gist_id}","starred_url":"https://api.github.com/users/hujiulong/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hujiulong/subscriptions","organizations_url":"https://api.github.com/users/hujiulong/orgs","repos_url":"https://api.github.com/users/hujiulong/repos","events_url":"https://api.github.com/users/hujiulong/events{/privacy}","received_events_url":"https://api.github.com/users/hujiulong/received_events","type":"User","site_admin":false},"created_at":"2017-06-20T01:55:47Z","updated_at":"2017-06-20T01:55:47Z","body":"写得很好，在做动画时也经常用到这种方式，防止在一帧时间中（大概16ms）渲染多次。\r\n```javascript\r\nfunction debounce(func) {\r\n    var t;\r\n    return function () {\r\n        cancelAnimationFrame(t)\r\n        t = requestAnimationFrame(func);\r\n    }\r\n}\r\n```"}}
{"actor":{"display_login":"hujiulong"}}