文章目录
手写防抖节流函数
在前面的学习过程中, 我们对于防抖和节流是非常清晰的了, 但是我们是使用的第三方库实现的防抖和节流
在实际开发中我们都是使用第三方库的防抖和节流, 但是我们手写防抖和节流会有助于阅读源码, 了解底层
接下来我们实现一下自己的防抖函数和节流函数
1. 手写防抖函数
我们按照如下思路来实现:
- 防抖基本功能实现:可以实现防抖效果(掌握重点)
- 优化一:优化参数和this指向(了解)
- 优化二:优化取消操作(增加取消功能)(了解)
- 优化三:优化立即执行效果(第一次立即执行)(了解)
1.1 基本实现
当我们准备实现一个工具函数时, 按照以下步骤:
- 需要接收什么参数
- 返回值是什么
- 内部实现
1.需要接收什么参数:
-
参数一: 回调的函数
-
参数二: 延迟时间
// 1.需要接收两个参数 function mydebounce(fn, delay) { }
2.返回值是什么
-
最终我们返回结果是要绑定对应的监听事件的, 所以返回值一定是一个新的函数
// 1.需要接收两个参数 function mydebounce(fn, delay) { const _debounce = () => {} // 2.返回一个新的函数 return _debounce }
3.内部实现
-
我们可以在_debounce函数中开启个定时器, 定时器的延迟时间就使用delay
-
并且每开启一个定时器, 我们都需要将上一次的定时器取消掉
function mydebounce(fn, delay) { // 1.创建一个变量, 用于记录上一次定时器 let timer = null // 2.触发事件时执行的函数 const _debounce = () => { // 2.1第一次timer为null, 所有我们需要判断timer有值时清除定时器 if (timer) clearTimeout(timer) // 2.2延迟执行传入的fn回调 timer = setTimeout(() => { fn() // 2.3函数执行完成后, 我们需要将timer重置 timer = null }, delay) } // 返回一个新的函数 return _debounce }
这样防抖的基本实现就已经实现完成, 已经可以实现防抖的效果, 我们可使用上一篇的输入框案例测试一下
-
测试代码帮大家拿过来
<input type="text" /> <!-- 引用本地js文件 --> <script src="./3.手写防抖.js"></script> <script> const inputEl = document.querySelector("input"); let counter = 1; function searchChange() { console.log(`第${counter++}次网络请求`, this.value); } // 使用自己封装的函数实现防抖 inputEl.oninput = mydebounce(searchChange, 3000); </script>
完成以上代码, 我们已经实现了防抖函数最核心的一部分代码, 在面试中能写出以上代码已经可以了, 接下来我们就是对防抖函数的功能进行完善和优化
1.2 优化this指向和参数
在上面测试中我们发现, value打印的值是undefined
- 这是因为我们在代码中直接调用了fn(), 我们知道独立函数调用this指向window, 而window中没有value这个值, 因此打印undefined
- 我们需要让this指向事件的调用者
优化this
-
我们可以使用显式绑定apply方法, 并且将_debonce的箭头函数改为普通函数
-
_debonce修改为普通函数, _debonce中的this会指向事件调用者
function mydebounce(fn, delay) { let timer = null // 1.箭头函数不绑定this, 修改为普通函数 const _debounce = function() { if (timer) clearTimeout(timer) timer = setTimeout(() => { // 2.使用显式绑定apply方法 fn.apply(this) timer = null }, delay) } // 返回一个新的函数 return _debounce }
优化参数
-
在事件监听的函数我们是有可能传入一些参数的, 例如event等
function mydebounce(fn, delay) { let timer = null // 1.接收可能传入的函数 const _debounce = function(...args) { if (timer) clearTimeout(timer) timer = setTimeout(() => { // 2.再将参数传给fn fn.apply(this, args) timer = null }, delay) } // 返回一个新的函数 return _debounce }
再进行测试
-
测试代码
<input type="text" /> <!-- 引用本地js文件 --> <script src="./3.手写防抖.js"></script> <script> const inputEl = document.querySelector("input"); let counter = 1; // 传入event参数并打印进行测试 function searchChange(event) { console.log(`第${counter++}次网络请求`, this.value, event); } // 使用自己封装的函数实现防抖 inputEl.oninput = mydebounce(searchChange, 3000); </script>
1.3 优化增加取消操作
什么是取消操作呢?
- 给大家解释一下, 例如用户在表单输入的过程中
- 返回了上一层页面或者关闭了页面, 就意味着我们这次延迟的网络请求没有必要继续发生了
- 我们应该提供这样一个可取消发送请求的功能
优化取消操作
-
给_debounce添加一个取消的方法
function mydebounce(fn, delay) { let timer = null const _debounce = function(...args) { if (timer) clearTimeout(timer) timer = setTimeout(() => { fn.apply(this, args) timer = null }, delay) } // 给_debounce添加一个取消函数 _debounce.cancel = function() { if (timer) clearTimeout(timer) } // 返回一个新的函数 return _debounce }
-
测试代码
<input type="text" /> <button>取消</button> <!-- 引用本地js文件 --> <script src="./3.手写防抖.js"></script> <script> const inputEl = document.querySelector("input"); const btnEl = document.querySelector("button"); let counter = 1; function searchChange() { console.log(`第${counter++}次网络请求`, this.value); } const debonceFn = mydebounce(searchChange, 3000); // 使用自己封装的函数实现防抖 inputEl.oninput = debonceFn; // 测试取消功能 btnEl.onclick = function () { debonceFn.cancel(); }; </script>
1.4 优化增加立即执行
增加立即执行功能:
- 有些场景需要第一次输入时, 立即执行, 后面的输入再使用防抖延迟执行
- 我们作为框架开发者, 需要提供这样一个功能, 而这个功能不是所有人都需要的
增加立即执行功能
-
我们可以提供第三个参数, 如果需要此功能, 那么传入第三个参数
// 1.设置第三个参数, 并且默认值为false function mydebounce(fn, delay, immediate = false) { let timer = null // 2.定义变量, 用于记录状态 let isInvoke = false const _debounce = function(...args) { if (timer) clearTimeout(timer) // 3.第一次执行不需要延迟 if (!isInvoke && immediate) { fn.apply(this, args) isInvoke = true return } timer = setTimeout(() => { fn.apply(this, args) timer = null // 4.重置isInvoke isInvoke = false }, delay) } _debounce.cancel = function() { if (timer) clearTimeout(timer) // 取消也需要重置 timer = null isInvoke = false } // 返回一个新的函数 return _debounce }
-
我们将第三个参数传为true测试一下吧
<input type="text" /> <button>取消</button> <!-- 引用本地js文件 --> <script src="./3.手写防抖.js"></script> <script> const inputEl = document.querySelector("input"); const btnEl = document.querySelector("button"); let counter = 1; function searchChange() { console.log(`第${counter++}次网络请求`, this.value); } // 传入第三个参数为true开启立即执行功能 const debonceFn = mydebounce(searchChange, 3000, true); inputEl.oninput = debonceFn; btnEl.onclick = function () { debonceFn.cancel(); }; </script>
2. 手写节流函数
我们前面有使用第三方库的节流函数
接下来手写实现一下自己的节流函数吧
我们按照如下思路来实现:
- 节流函数的基本实现:可以实现节流效果(掌握重点)
- 优化一:节流最后一次也可以执行(了解)
- 优化二:优化添加取消功能(了解)
- 优化三:优化返回值问题(了解)
2.1 基本实现
实现一个工具函数时, 应按照以下步骤:
- 需要接收什么参数
- 返回值是什么
- 内部实现
1.需要接收什么参数
需要接收两个参数:
-
参数一: 要执行的回调函数
-
参数二: 要执行的间隔时间
// 1.需要接收两个参数, 第一个参数要执行的回调, 第二个参数间隔时间 function mythrottle(fn, interval) { }
2.返回值是什么
-
最终我们返回结果是要绑定对应的监听事件的, 所以返回值一定是一个新的函数
// 1.需要传入两个参数, 第一个参数要执行的回调, 第二个参数间隔时间 function mythrottle(fn, interval) { const _throttle = function() { } // 2. 返回一个新的函数 return _throttle }
3.内部实现
思路: 实现节流函数, 我们使用定时器是不方便管理的, 实现节流函数我们采用另一个思路
-
我们获取一个当前时间nowTime, 我们使用new Date().gettime()方法获取, 在设定一个开始时间startTime, 等待时间waitTime
-
waitTime = interval - (nowTime - startTime), 当前的时间减去开始的时间得到结果, 再使用间隔时间减去这个结果, 就可以得到等待时间
-
得到等待时间我们在进行判断, 如果等待时间小于等于0, 那么就可以执行回调函数
-
开始时间startTime我们初始值为0就好, 当第一次执行时, nowTime获取的时间戳是一个非常大的值, 得到的结果waitTime是负值, 所以第一次执行节流函数, 一定会立即执行, 这也符合我们要封装的效果
function mythrottle(fn, interval) { // 1.定义变量保记录开始时间 let startTime = 0 const _throttle = function() { // 2. 获取当前时间 const nowTime = new Date().getTime() // 3.计算需要等待的时间 const waitTime = interval - (nowTime - startTime) // 4.当等待的时间小于等于0时, 执行回调函数 if (waitTime <= 0) { fn() // 并让开始时间等于现在时间 startTime = nowTime } } return _throttle }
节流函数的基本功能就实现了, 我们同样使用前的输入框案例测试一下
-
测试代码帮大家拿过来了
<input type="text" /> <!-- 引入自己的节流函数 --> <script src="./4.手写节流.js"></script> <script> const inputEl = document.querySelector("input"); let counter = 1; function searchChange() { console.log(`第${counter++}次网络请求`, this.value); } // 实现节流 inputEl.oninput = mythrottle(searchChange, 500); </script>
完成上面代码, 我们就已经掌握节流函数最核心的代码了, 在面试中写出上面代码即可, 重点掌握上面代码, 接下来是对节流函数的优化和功能的完善(了解了解即可)
2.2 优化this指向和参数
相信大家知道, 打印undefined是因为this指向问题, 刚刚手写防抖函数相信已经知道如何优化了
- 下面我们优化一下this指向
- 以及接收参数, 这里思路和防抖是一样的
function mythrottle(fn, interval) {
let startTime = 0
const _throttle = function(...args) {
const nowTime = new Date().getTime()
const waitTime = interval - (nowTime - startTime)
if (waitTime <= 0) {
// 只需要显式绑定this
fn.apply(this, arg)
startTime = nowTime
}
}
return _throttle
}
- 再进行测试
2.3 控制立即执行
前面我们有提到, 节流函数第一次会立即执行, 不用等待
- 但是我们第一次执行目前是不可控的
- 作为开发者, 我们需要考虑使用的人, 是否需要这个功能
- 因此我们需要对这个第一次的立即执行进行控制
-
我们可以给用户提供第三个参数, 用于控制是否立即执行
// 1.设置第三个参数控制是否默认执行, 默认为true function mythrottle(fn, interval, immedoate = true) { let startTime = 0 const _throttle = function() { const nowTime = new Date().getTime() // 2.控制是否立即执行 if (!immedoate && startTime === 0) { startTime = nowTime } const waitTime = interval - (nowTime - startTime) if (waitTime <= 0) { fn.apply(this) startTime = nowTime } } return _throttle }
-
测试代码
<input type="text" /> <!-- 引入自己的节流函数 --> <script src="./4.手写节流.js"></script> <script> const inputEl = document.querySelector("input"); let counter = 1; function searchChange() { console.log(`第${counter++}次网络请求`, this.value); } // 传入第三个参数false进行测试 inputEl.oninput = mythrottle(searchChange, 500, false); </script>