debounce实现 js_前端面试题——自己实现debounce

debounce实现 js_前端面试题——自己实现debounce前端面试,总会被问到这类问题:你知道debounce是什么么?你知道debounce什么时候用么?来来来,能给我实现一个debounce么?了解debounce以及实现方法,不仅会帮助我们面试,也是对我们技术的一次提升。废话不说,来不及了,我们一起学习debounce。什么是debounce?什么时候使用debounce?翻看Underscore的文档,它是这么描述debounce的:返回fun…

大家好,又见面了,我是你们的朋友全栈君。

前端面试,总会被问到这类问题:你知道debounce是什么么?

你知道debounce什么时候用么?

来来来,能给我实现一个debounce么?debounce实现 js_前端面试题——自己实现debounce

了解debounce以及实现方法,不仅会帮助我们面试,也是对我们技术的一次提升。废话不说,来不及了,我们一起学习debounce。

什么是debounce?什么时候使用debounce?

翻看Underscore的文档,它是这么描述debounce的:返回 function 函数的防反跳版本, 将延迟函数的执行(真正的执行)在函数最后一次调用时刻的 wait 毫秒之后. 对于必须在一些输入(多是一些用户操作)停止到达之后执行的行为有帮助。 例如: 渲染一个Markdown格式的评论预览, 当窗口停止改变大小之后重新计算布局, 等等.

这段话是什么意思呢?我们看一个例子:

HTML结构:

请滚动页面

滚动数目

0

CSS样式:

h1 {

height: 2000px;

}

.countWrapper {

position: fixed;

top: 100px;

left: 100px;

}

对应的JS代码:

var count = 0;

var updateCount = function(ev) {

console.log(ev)

count++;

document.getElementById(“count”).innerHTML = count;

}

window.onscroll = updateCount;

可以看到,随着鼠标滚动,updateCount这个事件不停地被触发。我们的例子当中,updateCount所做的事情比较简单,函数执行也比较快,但是,如果在复杂的系统当中,如Underscore文档中提到的,渲染一个Markdown格式的评论预览,如果我们每次都在window.onresize改变的时候重新计算布局,那么由于单位时间内,我们可能触发几十次resize事件,那么我们就要重新计算几十次布局,这给了系统很大的 压力,可能造成卡顿,而且,很明显,我们最关心的是窗口停止resize时候的评论预览,中间那么多次渲染是没有必要的。

怎么解决这个问题呢?

那就是,是延迟updateCount的执行,即只有在onscroll这个函数停止调用wait毫秒时间之后,再去执行updateCount。

基本实现

debounce本质上,是一个定时器setTimeout,在wait毫秒时间之后,执行传入的函数:

function debounce(func, wait, immediate) {

var timeout;

var debounced = function() {

if (timeout) clearTimeout(timeout);

timeout = setTimeout(func, wait);

}

return debounced;

}

调用方法:

window.onscroll = debounce(updateCount, 1000);

window.onscroll在每次滚动的时候,都会被调用debounce(updateCount, 1000),在debounce函数内部,如果之前已经存在定时器,那么就清除已有的定时器,重新开始计时。这样,如果滚动过于频繁地被触发,则之前滚动所开启的定时器都会被紧接而来的下一个滚动事件清除,只有最后一个滚动事件触发的定时器才会被保存,最终在1000ms之后执行updateCount函数。

完善程序

看过我之前文章的朋友们,应该会想到,一般模拟某个函数的时候,都需要处理一些事情:比如函数内部this的指向、参数的传递、原型链是否正确、是否能够正确处理返回值,等等。

我们先看this的指向。

如果直接调用window.onscroll = updateCount;的时候,updateCount内部的this是隐式绑定(如果不清楚什么是隐式绑定,请阅读我的《前端面试题——十分钟搞懂this》),那么this指向的是调用onscroll的元素,我的例子中是window,当然,你也可以在其他元素上调用onscroll,如:target.onscroll,那么这时的this就指向target。但是我们的模拟debounce当中,updateCount是在1000ms之后调用的,这个时候的调用环境是?恩?global?

怎么办呢?

我们把debounce调用时候的this保存给context,然后使用apply调用内部的func,并指定context为func的this:

function debounce(func, wait, immediate) {

var timeout;

var debounced = function() {

var context = this;

if (timeout) clearTimeout(timeout);

timeout = setTimeout(function() {

func.apply(context);

}, wait);

}

return debounced;

}

然后再看看参数的传递。

这个就很容易了,把参数保存为args,然后传入apply作为第二个参数。直接看代码:

function debounce(func, wait, immediate) {

var timeout;

var debounced = function() {

var context = this;

var args = arguments;

if (timeout) clearTimeout(timeout);

timeout = setTimeout(function() {

func.apply(context, args);

}, wait);

}

return debounced;

}

最后处理返回值。

function debounce(func, wait, immediate) {

var timeout, result;

var debounced = function() {

var context = this;

var args = arguments;

if (timeout) clearTimeout(timeout);

timeout = setTimeout(function() {

result = func.apply(context, args);

}, wait);

return result;

}

return debounced;

}

由于result是在setTimeout内部更新的,所以,其实return result会返回undefined,因此这个result并不是func函数执行之后真正的返回值。不过result接下来会用到,所以我们先放在那里吧。

立即执行

到这里我们的debounce就写完了么?对,已经写完了。不过underscore中的debounce还有第三个参数:immediate。这个参数是做什么用的呢?传参 immediate 为 true, debounce会在 wait 时间间隔的开始调用这个函数 。(注:并且在 wait 的时间之内,不会再次调用。)在类似不小心点了提交按钮两下而提交了两次的情况下很有用。

把true传递给immediate参数,会让debounce在wait时间开始计算之前就触发函数(也就是没有任何延时就触发函数),而不是过了wait时间才触发函数,而且在wait时间内也不会触发(相当于把func的执行锁住)。 如果不小心点了两次提交按钮,第二次提交就会不会执行。

那我们根据immediate的值来决定如何执行func。如果是immediate的情况下,我们立即执行func,并在wait时间内锁住func的执行,wait时间之后再触发,才会重新执行func,以此类推。

function debounce(func, wait, immediate) {

var timeout, result;

var debounced = 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(this, args);

} else {

timeout = setTimeout(function() {

result = func.apply(context, args);

}, wait);

}

return result;

}

return debounced;

}

如果使用window.onscroll = debounce(updateCount, 1000, true);调用函数,那么会进入if (immediate) 这种情况。我们分为首次调用,调用后wait结束之前再次调用和调用后wait结束之后再次调用三种情况讨论。

首次调用:如果是第一次调用的话,timeout是undefined,那么 callNow就是true。而timeout会被更新为定时器返回的ID,然后调用result = func.apply(this, args);。另外,result值在这里能被正确更新,并正确返回。

调用后wait结束之前再次调用:这个时候,timeout还是等于定时器ID(clearTimeout并不会删掉timeout中保存的ID),那么callNow就是false,就不会执行func.apply(this, args);

调用后wait结束之后再次调用:由于定时器的wait时间已过,timeout被更新为null,那么callNow就是true,又可以执行func.apply(this, args);了,同时锁住timeout,以此类推。

程序可以简单改一下:

function debounce(func, wait, immediate) {

var timeout, result;

var debounced = function() {

var context = this;

var args = arguments;

if (timeout) clearTimeout(timeout);

var later = function() {

timeout = null;

if (!immediate) result = func.apply(context, args);

};

if (immediate) {

var callNow = !timeout;

timeout = setTimeout(later, wait);

if (callNow) result = func.apply(this, args);

} else {

timeout = setTimeout(later, wait);

}

return result;

}

return debounced;

}

使用later来保存func函数的调用情况。这么改的原因么?可能是为了和underscore本身的debounce长得像一点吧。

其实,上面的代码还可以优化一下,让代码更简洁:

function debounce(func, wait, immediate) {

var timeout, result;

var debounced = function() {

var context = this;

var args = arguments;

if (timeout) clearTimeout(timeout);

var later = function() {

timeout = null;

if (!immediate) result = func.apply(context, args);

};

var callNow = immediate && !timeout;

timeout = setTimeout(later, wait);

if (callNow) result = func.apply(this, args);

return result;

}

return debounced;

}

取消debounce

Underscore中的debounce还有一个功能:如果需要取消预定的 debounce ,可以在 debounce 函数上调用 .cancel()。

这个就把setTimeout事件清除掉,并把timeout直接更新为null就可以:

debounced.cancel = function() {

clearTimeout(timeout);

timeout = null;

};

debounce我们就写完了,完整代码如下所示:

function debounce(func, wait, immediate) {

var timeout, result;

var debounced = function() {

var context = this;

var args = arguments;

if (timeout) clearTimeout(timeout);

var later = function() {

timeout = null;

if (!immediate) result = func.apply(context, args);

};

var callNow = immediate && !timeout;

timeout = setTimeout(later, wait);

if (callNow) result = func.apply(this, args);

return result;

}

debounced.cancel = function() {

clearTimeout(timeout);

timeout = null;

};

return debounced;

}

希望看这篇文章,你不仅知道什么时候使用debounce,也可以写出自己的debounce,顺顺利利通过面试。祝大家前端学习一切顺利!

关注我的公众号:前端三剑客。分享前端面试与算法面试的分析与总结!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/151567.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 《JavaScript 模式》读书笔记(6)— 代码复用模式3

    我们之前聊了聊基本的继承的概念,也聊了很多在JavaScript中模拟类的方法。这篇文章,我们主要来学习一下现代继承的一些方法。九、原型继承下面我们开始讨论一种称之为原型继承(prototype

    2022年3月25日
    47
  • linux vim复制粘贴命令_在Linux如何复制

    linux vim复制粘贴命令_在Linux如何复制将光标移动到复制的起始位置,按一下大写V或小写v,(大写V是整行,小写是光标处),然后上下左右将光标移动到复制的末尾,然后按下y,移动到要粘贴的位置,按下大写P或小写p(大写P:光标之前粘贴,小写p光标之后粘贴);总结:光标处起始处——按V/v——移动到复制的末尾处——按y——光标移到想粘贴的地方——按P/p;想要剪切的话,把y换成dd;…

    2022年9月22日
    3
  • android轮播图实现_ajax异步加载

    android轮播图实现_ajax异步加载这个图片异步加载并缓存的类已经被很多开发者所使用,是最常用的几个开源库之一,主流的应用,随便反编译几个火的项目,都可以见到它的身影。    可是有的人并不知道如何去使用这库如何进行配置,网上查到的信息对于刚接触的人来说可能太少了,下面我就把我使用过程中所知道的写了下来,希望可以帮助自己和别人更深入了解这个库的使用和配置。     GITHUB上的下载路径为:https:/

    2025年7月11日
    3
  • webstorm激活码2021【注册码】[通俗易懂]

    webstorm激活码2021【注册码】,https://javaforall.net/100143.html。详细ieda激活码不妨到全栈程序员必看教程网一起来了解一下吧!

    2022年3月19日
    74
  • 提问艺术「建议收藏」

    提问艺术「建议收藏」提问的艺术相信大部分老鸟当年都看过这篇经典的文章。在这里在转一次,以帮助大家能更好地问问题,以便获得更好的回答。先贴结论吧最后,不管是谁,来这里回答问题都是凭一腔热忱,凭兴趣和心情,如果版面充斥让人没有兴趣回答的问题,我想,对大家都不是好消息。自力更生真的很重要,不管你水平如何遇到什么样的困难,能自己解决多少就解决多少,然后再来求助,说需要什么什么帮助,多做一些努力只有好处

    2022年6月23日
    27
  • java 键盘输入多种方法

    java 键盘输入多种方法一、java不像C中拥有scanf这样功能强大的函数,大多是通过定义输入输出流对象。常用的类有BufferedReader,Scanner。实例程序:1、利用Scanner实现从键盘读入integer或float型数据//importjava.io.*;importjava.util.*;publicclassInputTest{publicstaticv

    2022年7月8日
    19

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号