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,顺顺利利通过面试。祝大家前端学习一切顺利!

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

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

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

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • SAGA GIS_GOSAT卫星数据下载

    SAGA GIS_GOSAT卫星数据下载官网:SAGA-SystemforAutomatedGeoscientificAnalyseshttps://saga-gis.sourceforge.io/en/index.html点击下载按钮点击后等几秒即可下载下载时间过长

  • vs 安装包_vs离线安装包

    vs 安装包_vs离线安装包VS安装包注册com组件VS安装包注册com组件1.把你的com组件加入到打包程序。 2.在打包程序中找到该com组件,点击属性。在属性中有Register项,把值选择为vsdrfCOM即可。

    2022年10月13日
  • 推荐几个基于.net的cms系统

    推荐几个基于.net的cms系统 建站必备啊,.net的不如PHP的那么多那么好用,这几个也还可以了。

  • QueryInterface详解 COM

    QueryInterface详解 COMQueryInterface接口查询IUnknown:      所有的COM接口均需要继承IUnknown接口。因此,若某个用户拥有一个IUnknown接口指针,它并不需要知道它所拥有的接口指针到底是什么类型的,而只需要通过此接口就可以用来查询其他接口就行了。      由于所有的COM接口都继承了IUnknown,每个接口的vbtl的前三项都是QueryInterface,A

  • Makefile missing separator. Stop.怎么解决「建议收藏」

    Makefile missing separator. Stop.怎么解决「建议收藏」现象:在makefile中写入:all:cleancompclean:./clean.cshcomp:./run_tc命令行的背景显示为红色。运行ma

  • NHibernate教程

    NHibernate教程 一、NHibernate简介在今日的企业环境中,把面向对象的软件和关系数据库一起使用可能是相当麻烦、浪费时间的。NHibernate是一个面向.Net环境的对象/关系数据库映射工具。对象/关系数据库映射(object/relationalmapping(ORM))这个术语表示一种技术,用来把对象模型表示的对象映射到基于SQL的关系模型数据结构中去。NHibernate除了能将一张表映射为

发表回复

您的电子邮箱地址不会被公开。

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