前言
该系列是笔者整理的前端需要掌握的手写功能集合,这些功能的手撕需要具备一定的前端基础功底,在面试中也会高频的出现。笔者会将每个手写功能单独呈现为一篇,尽可能整理的细致,同时也不会让文章篇幅太长,内容太过杂乱,该篇为前端开发中经常使用的工具函数防抖和节流函数
防抖函数
什么是防抖函数
防抖函数的功能是实现,被触发的函数可以延迟执行,同时在一定时间内多次触发会更新延迟时间,最终实现的效果是,在一定时间内多次触发被防抖的函数,最终该函数只会执行最后被触发的那一次,若以游戏做比喻,那么防抖可以理解为给技能增加施法前摇动作,若前摇被打断则需要重新施法
防抖函数的使用场景
在什么场景下,需要使用防抖函数呢?下面简单的列举几个基础的列子,思考这些使用场景特点,以便日后自己合理的运用防抖函数
- 输入验证或动态搜索
在input输入框中无论是将用户输入的内容进行验证,或者说是根据用户输入关键词,查询后进行内容推荐,我们一般都是需要监听用户的输入,如果用户输入每一个字符后,我们都验证一下或发送一下请求,那么将极大的消耗资源
- 下拉刷新
用户下拉时进行数据刷新操作,假设用户无意义频繁的快速下拉(想一想你有没有在网络不好,或者无聊抽风的时候频繁下拉刷新页面),如果每次都监听下拉动作,然后进行数据请求也是会极大的消耗资源
- 按钮提交
用户在进行按钮提交时,如果误触连点,多次提交表单,也会多消耗资源,如果是非幂等操作,配合处理不当还会出现其他意想不到的问题
- 监听页面滚动
有时候我们想要页面在滚动到某一位置时执行某项操作,如请求指定数据或是加载特殊进入动画,如果是直接监听页面滚动,那么在每次进行滚动时,页面将会持续的进行监听函数的回调,这也是有损性能的
以上就是笔者提供的一些防抖函数的基础使用场景,当然防抖不是这些场景下的唯一解,有些情况下需要多种手段配合处理,但是不能否认防抖函数在这些场景下使用起来简单高效
防抖的实现
接下来我们将一步步的从基础思路开始,最终实现一个防抖函数
业务场景:监听用户输入的关键词,以此查询相关推荐数据,然后进行结果词条展示,这是一个比较常见的场景,一般在列表搜索页面,根据用户输入的关键词,进行动态的结果词条展示
一:首先我们完成基础的功能,监听用户输入然后动态获取数据
<input type="text" oninput="handleInput(this.value)">
<script>
//监听用户输入
function handleInput(value) {
mockRequestFn(value).then(res => {
console.log(`关键词“${value}”请求到的数据为`, res.data)
})
}
//模拟一个请求函数
const mockRequestFn = function (value) {
//这里我们直接使用Promise和setTimeout模拟了请求
return new Promise((resolve, reject) => {
setTimeout(() => {
const result = {
status: 'ok',
code: 200,
data: [{ name: 'someGoods', price: 110 }]
}
resolve(result)
}, 500)
})
}
</script>
复制代码
二: 防抖的第一个特点,能让被触发的函数延迟执行。延迟执行你一定想到了setTimeout,那么我们就使用setTimeout来完成这一步的操作
function handleInput(value) {
setTimeout(() => {
mockRequestFn(value).then(res => {
console.log(`关键词“${value}”请求到的数据为`, res.data)
})
}, 1000);//假设延迟1秒,我们时间故意放长点可以体验一下
}
复制代码
三: 现在是延迟执行了,但是事件处理函数依然是被频繁的触发,现在就要处理多次触发时重置延迟时间的问题了,所谓重置延迟时间,其实就是在再次触发函数时,如果上一次的触发还没执行,取消上一次的执行,在这里每一次的函数触发执行都是用setTimeout完成的,那就可以使用清除定时器,来结束上一个函数
let timer = null
function handleInput(value) {
clearTimeout(timer) // 在函数执行时清除上一个计时器,达到重置时间的效果
timer = setTimeout(() => {
mockRequestFn(value).then(res => {
console.log(`关键词“${value}”请求到的数据为`, res.data)
})
}, 1000)
}
复制代码
防抖的基本思路就是这样的,但是现在的实现方法是远远不够的,存在以下几个问题:
-
首先这样的书写方式,无法多次复用,其他地方需要使用,还需再写一遍,造成代码重复,我们需要将其封装为函数使用
-
为了实现该功能,直接在全局定义变量timer,然后在函数中使用定义的全局变量,这样函数执行结果既不可控(受到外界timer的影响),同时timer也会污染全局变量
-
有些情况下,防抖处理的函数是需要被立刻执行的,先执行处理函数再进行防抖操作,比如当防抖函数运用在表单提交操作上,用户在首次点击提交时,我们不能将请求延迟,否则就会极大的影响用户体验(真实的防抖时间不会像我们例子中这么长,但即使在防抖时间很短情况下,延迟了防抖的时间再去请求,也会影响用户体验,具体是立即执行还是延迟执行,这个需要根据具体场景选择),例子中我们需要的效果是,用户持续输入时,进行防抖不请求,一但用户停止输入(过了防抖时间无操作),立刻去请求数据,这就是属于延迟执行的操作。
-
如果被防抖处理的函数具有返回值,需要将其返回值返回出来
发现以上问题后我们来将防抖操作封装成函数,进一步处理一下
防抖函数
/**
* 防抖函数
* @param {Function} fn 需要防抖处理的函数
* @param {Number} time 防抖时间
* @param {Boolean} triggleNow 是否立即触发
* @returns {*} fn执行结果
*/
function debounce(fn, time, triggleNow) {
let timer = null;
return function () {
let _self = this,
args = arguments,
result = null;
if (timer) {
clearTimeout(timer);
}
if (triggleNow) {
/**
* 1. 如果第一次进来timer为null,直接执行回调
* 2. 然后给timer赋值,过了防抖时间将timer再次置null
* 3. 如果在防抖时间内再次触发了函数,clearTimeout会取消上一个将timer置null的操作,timer就有值了
* 4. 只有timer为null时才会执行函数fn
*/
const exec = !timer;
timer = setTimeout(() => {
timer = null;
}, time);
if (exec) {
result = fn.apply(_self, args);
}
} else {
timer = setTimeout(() => {
result = fn.apply(_self, args);
}, time);
}
return result;
}
}
复制代码
节流函数
节流函数其实和防抖函数的一部分都是想通的,下面我们简单介绍一下
什么是节流函数
在我们介绍防抖函数的时候,曾今打过一个比喻,防抖函数就好像是技能前摇,那么节流函数就可以理解为技能冷却。因为节流函数主要针对的是,在函数被多次触发时,在固定时间内只会执行一次
再次注意下防抖和节流两者的区别:
防抖: 设置间隔时间,在间隔时间内多次触发函数, 刷新间隔时间,只执行最后触发那一次(如果是立刻触发的形式,只触发最开始那次)
节流: 设置间隔时间,在间隔时间内多次触发函数,触发函数只会执行一次,到下个间隔时间后,才会再次执行一次,以此类推
节流使用场景
虽然节流和防抖的功能不一样,但是有些场景下,两者其实都可以使用,关键取决于你想实现什么样的效果,就比如表单提交时用户连点问题,防抖节流都能在该场景下使用
下面举几个节流使用比较合适的场景
- 场景缩放
对页面(场景)缩放时,有些地方我们需要去监控浏览器resize,scroll等实现操作,如果一直监听,然后进行业务处理就会很消耗性能,这里就可以用节流实现性能优化
- 用户鼠标持续点击操作
这个场景很宽泛,只要是允许用户持续点击,但是又想只在一定时间内只触发一次处理函数的场景都可以使用
- 页面触底加载更多
页面触底后请求数据加载更多,但在请求回来列表再次渲染前,用户可能会多次触底发送请求,这时就可以使用节流
节流的实现
因为有防抖的实现思路,节流就不叙述那么详细了,节流的实现在于,用设置的节流时间、事件处理函数上次触发时间、当前事件处理函数触发时间进行计算比较,如果事件处理函数两次的触发间隔时间大于节流时间,就可以执行函数,否则继续等待
节流函数
节流函数在实现时,有一个点需要注意,就是是否需要继续执行最后一次触发事件,有些时候我们在进行节流操作的时候,如果在节流时间内再次触发事件处理函数,虽然函数在该时间段内不会再次执行,但是会记录到下一个节流周期执行,而有些时候我们又不希望执行这样的操作,因此针对这种情况需要进行参数判断。
/**
* 节流函数
* @param {Function} fn 需要节流处理的函数
* @param {Number} delay 节流时间
* @param {Boolean} needLast 是否执行最后一次触发
* @returns fn执行结果
*/
function throttle (fn, delay, needLast) {
let timer = null,
beginTime = new Date().getTime();
return function () {
let _self = this,
args = arguments,
curTime = new Date().getTime(),
result = null;
clearTimeout(timer)
//判断两次间隔如果大于设置时间执行函数
if (curTime - beginTime >= delay) {
result = fn.apply(_self, args);
beginTime = curTime;
} else {
//如果设置需要执行最后一次触发,那么就利用setTimeout延迟后执行
if (needLast) {
timer = setTimeout(() => {
result = fn.apply(_self, args);
}, delay);
}
}
return result
}
}
复制代码
结语
防抖函数和节流函数,本身并不涉及任何设计模式相关知识,仅仅是技能型工具函数,适用于前端某些业务场景,虽然代码并不多实现起来也相对简单,但是这也需要前端开发者对JS的基础知识掌握的足够扎实,因为其中也涉及到了闭包、apply(this指向相关)、arguments、clearTimeout和指示器等JS语言基础知识,由思路的推演还涉及到了函数式编程范式、纯函数的使用相关知识,这其中任何一点都值得再深入的了解,以此扎实自己的前端基础,因此下一篇将整理JS中this指向及call、apply、bind的基础实现,整理好后会附在本文最后。
上一篇手写文章地址:前端需要掌握的20个手写功能—Event Emitter/发布订阅模式