|
| 1 | +# JavaScript专题之跟着 underscore 学节流 |
| 2 | + |
| 3 | +## 前言 |
| 4 | + |
| 5 | +在[《JavaScript专题之跟着underscore学防抖》](https://github.com/mqyqingfeng/Blog/issues/22)中,我们了解了为什么要限制事件的频繁触发,以及如何做限制: |
| 6 | + |
| 7 | +1. debounce 防抖 |
| 8 | +2. throttle 节流 |
| 9 | + |
| 10 | +今天重点讲讲节流的实现。 |
| 11 | + |
| 12 | +## 节流 |
| 13 | + |
| 14 | +节流的原理很简单: |
| 15 | + |
| 16 | +如果你持续触发事件,每隔一段时间,只执行一次事件。 |
| 17 | + |
| 18 | +根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。 |
| 19 | +我们用 leading 代表首次是否执行,trailing 代表结束后是否再执行一次。 |
| 20 | + |
| 21 | +关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。 |
| 22 | + |
| 23 | +## 使用时间戳 |
| 24 | + |
| 25 | +让我们来看第一种方法:使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。 |
| 26 | + |
| 27 | +看了这个表述,是不是感觉已经可以写出代码了...... 让我们来写第一版的代码: |
| 28 | + |
| 29 | +```js |
| 30 | +// 第一版 |
| 31 | +function throttle(func, wait) { |
| 32 | + var context, args; |
| 33 | + var previous = 0; |
| 34 | + |
| 35 | + return function() { |
| 36 | + var now = +new Date(); |
| 37 | + context = this; |
| 38 | + args = arguments; |
| 39 | + if (now - previous > wait) { |
| 40 | + func.apply(context, args); |
| 41 | + previous = now; |
| 42 | + } |
| 43 | + } |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +例子依然是用讲 debounce 中的例子,如果你要使用: |
| 48 | + |
| 49 | +```js |
| 50 | +container.onmousemove = throttle(getUserAction, 1000); |
| 51 | +``` |
| 52 | + |
| 53 | +效果演示如下: |
| 54 | + |
| 55 | + |
| 56 | + |
| 57 | +我们可以看到:当鼠标移入的时候,事件立刻执行,每过 1s 会执行一次,如果在 4.2s 停止触发,以后不会再执行事件。 |
| 58 | + |
| 59 | +## 使用定时器 |
| 60 | + |
| 61 | +接下来,我们讲讲第二种实现方式,使用定时器。 |
| 62 | + |
| 63 | +当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。 |
| 64 | + |
| 65 | +```js |
| 66 | +// 第二版 |
| 67 | +function throttle(func, wait) { |
| 68 | + var timeout; |
| 69 | + var previous = 0; |
| 70 | + |
| 71 | + return function() { |
| 72 | + context = this; |
| 73 | + args = arguments; |
| 74 | + if (!timeout) { |
| 75 | + timeout = setTimeout(function(){ |
| 76 | + timeout = null; |
| 77 | + func.apply(context, args) |
| 78 | + }, wait) |
| 79 | + } |
| 80 | + |
| 81 | + } |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +为了让效果更加明显,我们设置 wait 的时间为 3s,效果演示如下: |
| 86 | + |
| 87 | + |
| 88 | + |
| 89 | +我们可以看到:当鼠标移入的时候,事件不会立刻执行,晃了 3s 后终于执行了一次,此后每 3s 执行一次,当数字显示为 3 的时候,立刻移出鼠标,相当于大约 9.2s 的时候停止触发,但是依然会在第 12s 的时候执行一次事件。 |
| 90 | + |
| 91 | +所以比较两个方法: |
| 92 | + |
| 93 | +1. 第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行 |
| 94 | +2. 第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件 |
| 95 | + |
| 96 | +## 双剑合璧 |
| 97 | + |
| 98 | +那我们想要一个什么样的呢? |
| 99 | + |
| 100 | +有人就说了:我想要一个有头有尾的!就是鼠标移入能立刻执行,停止触发的时候还能再执行一次! |
| 101 | + |
| 102 | +所以我们综合两者的优势,然后双剑合璧,写一版代码: |
| 103 | + |
| 104 | +```js |
| 105 | +// 第三版 |
| 106 | +function throttle(func, wait) { |
| 107 | + var timeout, context, args, result; |
| 108 | + var previous = 0; |
| 109 | + |
| 110 | + var later = function() { |
| 111 | + previous = +new Date(); |
| 112 | + timeout = null; |
| 113 | + func.apply(context, args) |
| 114 | + }; |
| 115 | + |
| 116 | + var throttled = function() { |
| 117 | + var now = +new Date(); |
| 118 | + //下次触发 func 剩余的时间 |
| 119 | + var remaining = wait - (now - previous); |
| 120 | + context = this; |
| 121 | + args = arguments; |
| 122 | + // 如果没有剩余的时间了或者你改了系统时间 |
| 123 | + if (remaining <= 0 || remaining > wait) { |
| 124 | + if (timeout) { |
| 125 | + clearTimeout(timeout); |
| 126 | + timeout = null; |
| 127 | + } |
| 128 | + previous = now; |
| 129 | + func.apply(context, args); |
| 130 | + } else if (!timeout) { |
| 131 | + timeout = setTimeout(later, remaining); |
| 132 | + } |
| 133 | + }; |
| 134 | + return throttled; |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +效果演示如下: |
| 139 | + |
| 140 | + |
| 141 | + |
| 142 | +我们可以看到:鼠标移入,事件立刻执行,晃了 3s,事件再一次执行,当数字变成 3 的时候,也就是 6s 后,我们立刻移出鼠标,停止触发事件,9s 的时候,依然会再执行一次事件。 |
| 143 | + |
| 144 | +## 优化 |
| 145 | + |
| 146 | +但是我有时也希望无头有尾,或者有头无尾,这个咋办? |
| 147 | + |
| 148 | +那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定: |
| 149 | + |
| 150 | +leading:false 表示禁用第一次执行 |
| 151 | +trailing: false 表示禁用停止触发的回调 |
| 152 | + |
| 153 | +我们来改一下代码: |
| 154 | + |
| 155 | +```js |
| 156 | +// 第四版 |
| 157 | +function throttle(func, wait, options) { |
| 158 | + var timeout, context, args, result; |
| 159 | + var previous = 0; |
| 160 | + if (!options) options = {}; |
| 161 | + |
| 162 | + var later = function() { |
| 163 | + previous = options.leading === false ? 0 : new Date().getTime(); |
| 164 | + timeout = null; |
| 165 | + func.apply(context, args); |
| 166 | + if (!timeout) context = args = null; |
| 167 | + }; |
| 168 | + |
| 169 | + var throttled = function() { |
| 170 | + var now = new Date().getTime(); |
| 171 | + if (!previous && options.leading === false) previous = now; |
| 172 | + var remaining = wait - (now - previous); |
| 173 | + context = this; |
| 174 | + args = arguments; |
| 175 | + if (remaining <= 0 || remaining > wait) { |
| 176 | + if (timeout) { |
| 177 | + clearTimeout(timeout); |
| 178 | + timeout = null; |
| 179 | + } |
| 180 | + previous = now; |
| 181 | + func.apply(context, args); |
| 182 | + if (!timeout) context = args = null; |
| 183 | + } else if (!timeout && options.trailing !== false) { |
| 184 | + timeout = setTimeout(later, remaining); |
| 185 | + } |
| 186 | + }; |
| 187 | + return throttled; |
| 188 | +} |
| 189 | +``` |
| 190 | + |
| 191 | +## 取消 |
| 192 | + |
| 193 | +在 debounce 的实现中,我们加了一个 cancel 方法,throttle 我们也加个 cancel 方法: |
| 194 | + |
| 195 | +```js |
| 196 | +// 第五版 非完整代码,完整代码请查看最后的演示代码链接 |
| 197 | +... |
| 198 | +throttled.cancel = function() { |
| 199 | + clearTimeout(timeout); |
| 200 | + previous = 0; |
| 201 | + timeout = null; |
| 202 | +} |
| 203 | +... |
| 204 | +``` |
| 205 | + |
| 206 | +## 注意 |
| 207 | + |
| 208 | +我们要注意 underscore 的实现中有这样一个问题: |
| 209 | + |
| 210 | +那就是 `leading:false` 和 `trailing: false` 不能同时设置。 |
| 211 | + |
| 212 | +如果同时设置的话,比如当你将鼠标移出的时候,因为 trailing 设置为 false,停止触发的时候不会设置定时器,所以只要再过了设置的时间,再移入的话,就会立刻执行,就违反了 leading: false,bug 就出来了,所以,这个 throttle 只有三种用法: |
| 213 | + |
| 214 | +```js |
| 215 | +container.onmousemove = throttle(getUserAction, 1000); |
| 216 | +container.onmousemove = throttle(getUserAction, 1000, { |
| 217 | + leading: false |
| 218 | +}); |
| 219 | +container.onmousemove = throttle(getUserAction, 1000, { |
| 220 | + trailing: false |
| 221 | +}); |
| 222 | +``` |
| 223 | + |
| 224 | +至此我们已经完整实现了一个 underscore 中的 throttle 函数,恭喜,撒花! |
| 225 | + |
| 226 | +## 演示代码 |
| 227 | + |
| 228 | +相关的代码可以在 [Github 博客仓库](https://github.com/mqyqingfeng/Blog/tree/master/demos/throttle) 中找到 |
| 229 | + |
| 230 | +## 专题系列 |
| 231 | + |
| 232 | +JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 |
| 233 | + |
| 234 | +JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 |
| 235 | + |
| 236 | +如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 |
0 commit comments