
「我正在参与掘金会员专属活动-源码共读第一期,点击参与」
前言
防抖相信大家都不陌生,面试中会经常会被问题或提起。比如会问一些前端优化、手写防抖节流函数等等,这里就跟着underscore 源码来学习一下。
定义
在规定时间后才执行,如果触发则重新计时
也就是说,防抖函数在n秒内,无论触发了多少次函数回调,我都只只在n秒后执行一次。比如我们设置一个等待时间为5秒的防抖函数,如果5秒内有触发,就需要重新计时,直到5秒内没有触发就调用执行。
使用场景
最近项目中有一个表单搜索场景,在输入文字的过程中会持续触发oninput
事件,而搜索接口只是在用户输入搜索文字后进行调用。如果是用户输入一个文字就搜索一次,不仅会频繁调用后台接口,前端显示效果也不好。
使用防抖的话,可以将接口调用设定在500ms
内没有触发oninput
事件后再调用接口,这样就可以解决问题。
还会在其他场景使用
- 一些频繁点击操作的按钮,比如登录、短信验证,避免用户短时间多次发送
- 调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖
- 鼠标移动
mousedown
计算等场景
实现原理
实现原理其实很简单,就是利用定时器,函数在最开始执行的时候就设定一个定时器,如果在n秒内有执行就吧定时器清空,重新设定一个新的定时器,当n秒内没有再调用后,定时器计时结束后就会触发回调。
第一版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
function debounce(fn, wait = 300) { let timer = null;
return function (...args) { if (timer) { clearTimeout(timer); } timer = setTimeout(() => { fn.apply(this, args); }, wait); }; }
|
我们再写一个输入框事件来测试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <input type="text" oninput="oninputHandler(event)" />
<script> const testFn = debounce((event) => { console.log('执行防抖', event.target.value); }, 1000);
function oninputHandler(event) { testFn(event); }
function oninputHandler(event) { console.log('input change value: ' + event.target.value); } </script>
|
这是没有执行防抖

开启防抖后

效果还是很明显的,从原来的输入一个值就触发,到现在1秒内没有输入才触发,至此,简单版防抖就已经实现了。
第二版
接下来再来对防抖做一下改造,在首次调用的时候立即执行函数,等到n秒内没有触发,才可以重新触发执行。
听起来有点绕,也就是说在oninput
事件第一次触发的时候就执行,后续的触发都不执行。等到1秒内没有执行后,再触发oninput
时又会执行第一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
function debounce(fn, wait = 300, immediate = false) { let timer = null;
return function (...args) { if (timer) { clearTimeout(timer); }
if (immediate) { const isExecute = !timer;
timer = setTimeout(() => { timer = null; }, wait);
isExecute && fn.apply(this, args); } else { timer = setTimeout(() => { fn.apply(this, args); }, wait); } }; }
|
underscore 源码
来看一下underscore里是如何实现的,先将核心代码复制出来,用上面的oninput
事件来调试,看一下它的一个具体步骤。
在debounced
方法内部打上一个断点,然后在输入框输入数据触发防抖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| function debounce(func, wait, immediate) { var timeout, previous, args, result, context;
var later = function () { var passed = now() - previous;
if (wait > passed) { timeout = setTimeout(later, wait - passed); } else { timeout = null;
if (!immediate) result = func.apply(context, args);
if (!timeout) args = context = null; } }; var debounced = restArguments(function (_args) { context = this; args = _args; previous = now(); if (!timeout) { timeout = setTimeout(later, wait);
if (immediate) result = func.apply(context, args); } return result; });
debounced.cancel = function () { clearTimeout(timeout); timeout = args = context = null; };
return debounced; }
|
源码还是有很多亮点的
- 增加了
cancel
方法,可以随时取消。 - 在执行回调的时候,吧函数结果当作返回值
return
出去,是为了避免回调中有返回数据。 - 通过记录每次执行时间差,来判断是否需要执行回调。