面试题
- 谈谈防抖节流
- 手写防抖节流
防抖与节流是很相似(但不同)的概念,简单来说就是一个能控制一段时间某个函数的执行次数的方案。用来优化计算机或网络资源。下面我们分别看下这两个概念。
防抖 debounce
简单来说就是把多次执行组合成一次执行。
比如我们设置了一个时间间隔 5 秒,当事件触发的间隔超过 5 秒,(回调)函数才会执行,如果在 5 秒内,事件又被触发,则刷新这个 5 秒,至少5秒后事件没被触发才执行函数。
实现原理
//---------------------------------测试用例--------------------------------
// 用户高频率执行的函数(需要防抖的函数),但可能是个异步请求列表,成本比较高需要优化
function userHighRequencyAction(e, content) {
console.log(e, content);
}
// 给这个高频的方法,加防抖方案输出一个防抖的function
var userDebounceAction = debounce(userHighRequencyAction, 1000);
// 如何触发那个高频函数 绑定一个onmousemove事件,来模拟高频触发 $(1)事件监听$
document.onmousemove = function (e) {
userDebounceAction(e, 'test debounce'); // 给防抖函数传个参
}
//---------------------------------实现-----------------------------------
function debounce(func, wait) { // -----> $(2)作用域和闭包$
let timer;
return function () {
let context = this;
let args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
func.apply(context, args); // -----> $(3)this$
// 其实就是 context.func(args)
}, wait);
};
}
看了上面的实现即使有解释,我想你也是云里雾里,为什么要这么做?有什么好处?其实是你需要理解清楚一些其他必要的概念。和那些必要概念与之的必要关联。看不懂没关系,多看两遍,多思考关联,知识是一张网,继续下去,你就能把知识融汇贯通。
关联概念
- 事件监听
- 作用域
- 闭包
- this
理解了这些概念后,再来简单说明下这实现思路。你会觉得,下面我这段话异常清晰直接,如果觉得模糊,再多在关联概念里游会,回来后你会变得更健壮。
简单说明
其实这其中逻辑没什么好说的,就是利用 setTimeout 这个WebApi的延迟效果,设置一个定时器,当没有到达延迟时间时重复执行就清除定时器(clearTimeout),并建立一个新的定时器,继续等延迟时间,如此循环。
- 为什么要返回函数或者说为什么要用 闭包【关联概念】。其实debounce函数只调用了一次,后面调用的全是闭包函数,其实了解闭包都知道这样 timer 定时器的变量不被gc回收,这样下次执行时仍然指向的是上一次设置的定时器。
- 为什么要绑定 this, 因为fn 执行的时候this指向全局对象(浏览器中是window),根据词法作用域,可以在外层用个变量保存下 this, 再用 apply 进行显示绑定。
- 为什么要有 arguments 因为 JavaScript 在事件处理函数中会提供事件对象 event, 所以我们得把参数一并传入, 而apply/call是可以传参的(具体自查MDN的api)。
这样理解是不是轻松多了,所以对概念之间关联的理解跟清晰的概念本身同样重要。
**
类比
乘公交车,一直有人陆陆续续上车(事件触发),司机心想30s(时间间隔) 内没人继续上,再开车(函数执行)。提高了公交的资源利用。
乘电梯,程序设定电梯在没有人20s(时间间隔) 内按开门按钮(事件触发)上下电梯,再关门启动(函数执行)。提高了电梯的资源利用。
我们开发中可能遇见的场景
- 搜索输入框(Autocomplete),当不再输入后的几百毫秒再去发送请求,减少服务器压力。
- 注册框(即时判断是否重复用户名),或需要后台校验的文本输入框同理。
- 提交按钮的点击,有的人就是会疯狂的点,有啥办法呢。
- 不停改变浏览器窗口大小会触发多次 resize 事件,引起浏览器的重排【关联概念(弱)】,消耗性能。
节流 throttle
简单来说 就是在指定的时间间隔内,只允许我们的函数执行一次。
比如一个事件在被疯狂触发,本来每秒执行几百次(回调)函数,而你使用函数节流设了个时间间隔 1s,那么这个函数在1s 内只会执行一次。
与防抖之间的主要区别
节流至少在每时间间隔内 保证有规律地执行该功能。
实现原理
function throttle(func, wait) {
var timer,
context,
args;
return function () {
context = this;
args = arguments;
if (!timer) {
timer = setTimeout(function() {
// 执行后置定时器变量为null
timer = null;
func.apply(context, args);
}, wait);
}
};
}
简单来说,当触发事件的时候,设置一个 timer, 再次触发事件的时候 timer 存在(不为null),则不执行,直到函数执行了,把timer置空,并启动设置下一个定时器。这也就保证了 wait时间内函数只会执行一次。
我们开发中可能遇见的场景
- 无限滚动列表。用户向下滚动列表时,您需要时刻检查用户屏幕离底部有多远。如果用户接近底部,我们应该请求下一页内容并将其附加到页面上。为什么不用 debounce 因为它仅在用户停止滚动时才会触发。我们需要在用户到达底部之前就开始获取内容。throttle可以保证我们一直在检查距底部的距离。但不用频率过高的执行函数(scroll 事件触发的回调)。
- 高频点击提交按钮,比如你抢票的时候,用debounce你就会因为太想抢而不停地点确越抢不到。
- 监听鼠标 mousemove 计算一个div跟随鼠标移动而移动的函数等等, 用debounce就很不连贯
怎么用防抖与节流?
讲道理这个我建议直接用lodash (省的自己写,而且有些变体用法满足多种需求比如 debounce 的前置执行还是后置执行参数)
requestAnimationFrame(rAF)
限制函数执行速率的另一种方法。 根据经验,如果你的JavaScript函数是“绘画”或直接对动画属性进行实时处理或重新计算元素位置使用requestAnimationFrame会是好选择。
debounce 手写简单拓展
虽然lodash 现成 api 但可能面试会问就简单列下这种举一反三的问题,不过你当时想想其实问题不大。例子就用别人现成的了。
需求: 不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
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;
}
}