在日常开发中,经常会有一个按钮防止二次点击操作的需求。比如支付订单,创建商品,点赞收藏之类的。虽然不知道用户为什么要在0.5秒内连续点击两次甚至三次
这些需求都可以通过函数防抖来实现。
函数防抖简单来说就是,触发高频事件后n秒内只执行一次,如果在时间内再次触发,那重新计算时间
function debounce(fn) { let timer = null return function() { clearTimeout(timer) timer = setTimeout(() => { fn.apply(this, arguments) }, 1000) } } function sayHi(params) { console.log('Hi'); } let ObjDIV = document.getElementById('div') ObjDIV.addEventListener('click', debounce(sayHi))
上面的代码在1s内连续点击的话,只会触发一次 sayHi ,而且1s的等待时间会从最后一次点击重置。
但是上面的代码有个缺陷, sayHi 只能在等待时间结束后调用,实际上一些需求是不可能这样做的。比如点赞,不可能要等1s之后才有反馈。所以我们将防抖函数进行下优化。
function debounce(func, wait, immediate) { let timeout, args, context, timestamp, result const later = function() { // 据上一次触发时间间隔 const last = +new Date() - timestamp // 上次被包装函数被调用时间间隔last小于设定时间间隔wait if (last < wait && last > 0) { timeout = setTimeout(later, wait - last) } else { timeout = null // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用 if (!immediate) { result = func.apply(context, args) if (!timeout) context = args = null } } } return function(...args) { context = this timestamp = +new Date() const callNow = immediate && !timeout // 如果延时不存在,重新设定延时 if (!timeout) timeout = setTimeout(later, wait) if (callNow) { result = func.apply(context, args) context = args = null } return result } }
这样函数就会立即执行,并且在接下来的 wait 里不会重复执行,直到 wait 时间之后再次点击才可以执行下一次。
不带立即执行的应用场景
在搜索引擎搜索问题的时候,我们希望用户输入完最后一个字才调用查询接口,而不是在输入每个字都调用接口。当然这个可以使用blur或者按钮事件触发,具体使用哪种方法还是根据需求来定。
节流
在这个 wait 时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。
节流的一些应用场景:
mousemove的拖动
resize事件的触发
scroll的事件监听
/** * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait * * @param {function} func 回调函数 * @param {number} wait 表示时间窗口的间隔 * @param {object} options 如果想忽略开始函数的的调用,传入{leading: false}。 * 如果想忽略结尾函数的调用,传入{trailing: false} * 两者不能共存,否则函数不能执行 * @return {function} 返回客户调用函数 */ _.throttle = function(func, wait, options) { var context, args, result; var timeout = null; // 之前的时间戳 var previous = 0; // 如果 options 没传则设为空对象 if (!options) options = {}; // 定时器回调函数 var later = function() { // 如果设置了 leading,就将 previous 设为 0 // 用于下面函数的第一个 if 判断 previous = options.leading === false ? 0 : _.now(); // 置空一是为了防止内存泄漏,二是为了下面的定时器判断 timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function() { // 获得当前时间戳 var now = _.now(); // 首次进入前者肯定为 true // 如果需要第一次不执行函数 // 就将上次时间戳设为当前的 // 这样在接下来计算 remaining 的值时会大于0 if (!previous && options.leading === false) previous = now; // 计算剩余时间 var remaining = wait - (now - previous); context = this; args = arguments; // 如果当前调用已经大于上次调用时间 + wait // 或者用户手动调了时间 // 如果设置了 trailing,只会进入这个条件 // 如果没有设置 leading,那么第一次会进入这个条件 // 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了 // 其实还是会进入的,因为定时器的延时 // 并不是准确的时间,很可能你设置了2秒 // 但是他需要2.2秒才触发,这时候就会进入这个条件 if (remaining <= 0 || remaining > wait) { // 如果存在定时器就清理掉否则会调用二次回调 if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { // 判断是否设置了定时器和 trailing // 没有的话就开启一个定时器 // 并且不能不能同时设置 leading 和 trailing timeout = setTimeout(later, remaining); } return result; }; };
在vue里怎么写?
import { debounce } from "@/utils/index"; onCreateSubmit: debounce( function() { createIntegralGoods(this.form).then(res => { this.$message.success("商品创建成功"); this.$router.push({ path: "/campaign/active/integral-mall/index" }); }); }, 1000, true
————————————————
智一面|前端面试必备练习题