防抖和节流是面试常被问到的问题,因为里面包含了比如闭包,this指向,函数传参,箭头函数等知识点,那么防抖和节流就是防止用户一次性点击很多下,一直发请求,比如登录页面按钮,连点很多下就会发送很多没有意义的请求,所以就可以通过防抖与节流来优化。
- 防抖:在规定的时间内如果没有二次触发行为,则执行,否则放弃上一次的事件行为,从当前行为开始重新计时。
- 节流就是节省流量,人为的规定去每隔一秒发送一次请求,比如用户在10秒之内点击了100次的话,也只会发送10次请求。
手写防抖
手写防抖原理就是监听点击事件,给他设置一个定时器,让它在规定的时间内不会触发第二次,如果触发了第二次,则取消前一次的点击逻辑,就可以实现防抖效果了。如下;
<body>
<button id="btn">提交</button>
<script>
const btn = document.getElementById("btn");
function handle() {
console.log('向后端发请求');
// console.log(this); // 这里的this将指向btn元素
}
btn.addEventListener("click", debounce(handle, 1000))
function debounce(fn, wait) { // 防抖
let timer = null
return function () {
if (timer) clearTimeout(timer) // 如果定时器没执行完就清除原来的重新开始计时
timer = setTimeout(() => {
fn() // 函数handle执行 为为了不改变this指向 -> fn.call(this)
}, wait)
}
}
</script>
</body>
但是还需要注意的是这样就会修改handle里面的this指向,本来handle函数里面的this是通过隐式绑定(非独立调用)在btn这个dom对象上的,也就是指向 btn dom结构,但是如上所示,将handle()函数拿到 setTimeout 里面来调用,这时候函数为独立调用,所以默认指向全局的window对象。
为了解决这个问题,不能修改原函数的 this 指向,handle函数执行的时候可以将this指向原来的 btn,于是将fn() ---> fn.call(this),this 的指向就取决于 fn.call(this) 被调用时的上下文。在防抖函数 debounce 的场景中,this 指向触发事件的元素 btn。
但是如果函数里面有事件参数,也就是当函数被绑定在一个事件上执行时,就一定会具有一个形参,用来描述当前的事件详情。
<body>
<button id="btn">提交</button>
<script>
const btn = document.getElementById("btn");
function handle(e) {
console.log('向后端发请求');
console.log(e);
}
btn.addEventListener("click", handle)
</script>
</body>
所以后面要保证事件参数还是原来的事件参数,还需要注意的是如果 handle 原函数里面后面还有参数时,还需要考虑到传参数的问题。通过...args接收所有的参数变成一个数组,再将这个数组结构到fn也就是handle函数的形参里面。如下;
<body>
<button id="btn">提交</button>
<script>
const btn = document.getElementById("btn");
function handle(e, a, b) {
console.log('向后端发请求');
console.log(e);
}
btn.addEventListener("click", debounce(handle, 1000))
function debounce(fn, wait) { // 防抖
let timer = null
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.call(this, ...args)
}, wait)
}
}
</script>
</body>
这样就比较完美地实现了防抖效果。
手写节流
节流就是在规定的时间内只执行一次,比如,人为的规定去每隔一秒发送一次请求,比如用户在10秒之内点击了100次的话,也只会发送10次请求
主要思路就是点击一次后,记录下来此时的时间节点,点第二次的时候看是否与前一次间隔有没有1s,如果有1s就触发,否则不触发。
获取时间可以使用Date.now():
代码如下:
<body>
<button id="btn">提交</button>
<script>
let btn = document.getElementById('btn');
function handle() {
console.log('向后端发送请求');
}
btn.addEventListener('click', throttle(handle, 1000));
function throttle(fn, wait) {
let preTime = null
return function () {
let nowTime = Date.now()
if (nowTime - preTime >= wait) {
fn()
preTime = nowTime
}
}
}
</script>
</body>
然后考虑原函数的this指向,原函数的事件参数,完整代码如下。
<body>
<button id="btn">提交</button>
<script>
let btn = document.getElementById('btn');
function handle(e, a, b) {
console.log('向后端发送请求');
console.log(e);
console.log(this);
}
btn.addEventListener('click', throttle(handle, 1000));
function throttle(fn, wait) {
let preTime = null
return function (...args) {
let nowTime = Date.now()
if (nowTime - preTime >= wait) {
fn.call(this, ...args)
preTime = nowTime
}
}
}
</script>
</body>
自定义一个防抖指令
全局定义指令分为:
- 定义指令
- 注册指令 (vue 认可这个指令) 定义install函数,调用install函数,通过 app.directive(name, options) directive 会触发 options 中的钩子函数。
- 调用指令 入口文件中
Vue.use()调用,会调用 install 函数
举个例子:写一个节流的自定义指令
- 定义全局自定义指令:
// command/debounce.js
export default {
// el:该指令绑定在哪个元素上
// binding:指令的相关信息,包含该指令绑定的值
mounted(el, binding) {
let timer = null;
el.addEventListener('click', () => {
if (timer) clearTimeout(timer) // 如果还存在定时器,则清除它重新计时
timer = setTimeout(() => {
binding.value(); // 当计时器执行完后,再执行绑定的事件
timer = null;
}, 2000)
})
}
};
- 注册全局自定义指令:
// command/index.js
// 引入指令代码
import debounce from "./debounce.js";
const directive = { // 汇总指令
debounce,
}
export default {
install(app) { // app === vue 实例 已安装的方式插到app中
// 注册指令
Object.keys(directive).forEach(key => { // 找出 key
app.directive(key, directive[key]) // 注册指令 两个参数 (名字,指令代码)
})
}
}
- 引入指令并调用全局自定义指令
// mian.js
import { createApp } from 'vue'
import App from './App.vue'
import Directives from '@/command/index.js'
const app = createApp(App)
app.use(Directives) // 全局注册 // 注册指令调用了 install 函数
- 使用自定义指令
<template>
<button v-debounce="handle"></button>
</template>
<script setup>
const handle=> (){
console.log('xxxxxx');
}
</script>
好了,关于防抖与节流就介绍到这里了。