前言
- 什么是防抖?什么是节流?
- 有什么用途?
- 常规解决方案
- Vue2 高阶组件用法实现
- 拓展
防抖和节流定义
防抖可以简单的理解为再一定时间内,只触发一次操作事件,那么即便你再这段时间内疯狂的点击操作也是无效的
节流是在某一段时间内必然会触发一次操作,如果这段时间内频繁触发此动作是无效的,待时间重置后继续这一系列动作
用途
- 防抖:
- 比如按钮的频繁操作
- 输入框的动态频繁响应请求
- 节流:
- 滚动事件
- 防抖中定义的一般也可以实现
一般这两种方案都是可以起到优化页面交互的一种手段
简单实现这两种方案
一般面试的时候大多会让你手写
防抖
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.call(this, args);
}, delay);
};
}
节流
function throttle(fn, delay) {
let timer = null;
let startDate = Date.now();
return function () {
let args = Array.prototype.slice.call(arguments);
let endDate = Date.now();
if (endDate - startDate > delay) {
startDate = endDate;
if (timer) {
clearTimeout(timer);
}
fn.call(this, args);
} else {
clearTimeout(timer);
timer = setTimeout(() => {
fn.call(this, args);
}, delay);
}
};
}
补充一句更多的时候我们会借用第三方的库去实现,比如大名鼎鼎的lodash
, debounce
、throttle
这些API
都可以很好兼容处理
Vue@2
另类的实现方案
- 前段时间因为自己在思考vue中如何定义高阶组件,后来查阅了一些资料,想到了一些解决方案
- 借助这些思路可以去拓展一下
好了,我们直接开门见山吧
lodash
使用方案
- 基础版本
<script>
export default {
name: 'MyThrottle',
// 在组件配置项中添加一个abstract的选项设置为true,就可以让组件成为一个抽象组件,抽象组件它自身不会渲染一个DOM元素,也不会出现在组件的父组件链中。
// 在抽象组件的生命周期过程中,我们可以对包裹的子组件监听的事件进行拦截,也可以对子组件进行Dom 操作,从而可以对我们需要的功能进行封装,而不需要关心子组件的具体实现。
abstract: true,
props: {
time: Number,
events: {
deafult: '',
type: String
}
},
created() {
this.eventKeys = this.events.split(',');
this.originMap = {};
this.throttledMap = {};
},
methods: {
_throttle(fn, wait = 50, ctx) {
let lastCall = 0;
return function (...params) {
const now = new Date().getTime();
if (now - lastCall < wait) return;
lastCall = now;
fn.apply(ctx, params);
};
},
isOriginHtmlNode(vnode) {
return !vnode.tag.includes('vue-component');
}
},
// render函数直接返回slot的vnode,避免外层添加包裹元素
render() {
const vnode = this.$slots.default[0]; //遍历单个节点
const getTagetEvents = this.isOriginHtmlNode(vnode)
? vnode.data.on
: vnode.componentOptions.listeners;
this.eventKeys.forEach((key) => {
//获取当前vnode 自身的事件
const target = this.isOriginHtmlNode(vnode)
? vnode.data.on[key]
: vnode.componentOptions.listeners[key];
if (target === this.originMap[key] && this.throttledMap[key]) {
//如果存在则直接赋值
getTagetEvents[key] = this.throttledMap[key];
} else if (target) {
// 将原本的事件处理函数替换成throttle节流后的处理函数
this.originMap[key] = target;
this.throttledMap[key] = this._throttle(target, this.time, vnode);
//重写vnode 节点的绑定事件
getTagetEvents[key] = this.throttledMap[key];
}
});
return vnode;
}
};
</script>
使用
注意:
这种方式只能监听一个插槽的情况,默认只能为一个子元素包裹
通过传入
events
绑定监听元素事件,time
为时间线
扩展
- 需求:希望一次性监听多个子元素(多个插槽的情况),并且可能涵盖子元素的情况(深层遍历子元素)
- 希望可以自定义事件属性(那么希望可以通过参数配置)
<script>
export default {
name: 'MultiThrottle',
props: {
// eventKeys = 'click,blur'
// eventKeys = {
// click: {
// time: 2000,
// ...args
// },
// blur: {
// time: 5000,
// ...args
// }
// }
events: {
type: [Object, String],
default: ''
},
time: Number
},
created() {
this.throttledEventsMap = {};
},
computed: {
_eventKeys() {
let eventKeyNames = '';
if (typeof this.events === 'string') {
eventKeyNames = this.events;
} else {
eventKeyNames = Object.keys(this.events).join(',');
}
// console.log('eventKeyNames', eventKeyNames);
return eventKeyNames.split(',');
}
},
mounted() {
console.log('this.throttledEventsMap', this.throttledEventsMap);
},
methods: {
isOriginHtmlNode(vnode) {
return !vnode?.tag?.includes('vue-component');
},
// 对传入的属性进行赋值操作
getRestTime(key, time) {
if (typeof this.events === 'object') {
return this.events[key] || {time};
}
return {
time
};
},
_throttle(fn, wait = 50, ctx, key) {
const self = this;
let lastCall = 0;
return function (...params) {
const now = new Date().getTime();
if (now - lastCall < self.getRestTime(key, wait).time) return;
lastCall = now;
fn.apply(ctx, params);
};
}
},
// render函数直接返回slot的vnode,避免外层添加包裹元素
render() {
const _vnode = this.$slots.default; //遍历所有子节点
// console.log('_vnode', _vnode);
// console.log('vnode', vnode);
this._eventKeys?.forEach((key) => {
//获取当前vnode 自身的事件
const loopEvent = (vnode) => {
if (vnode && vnode.length) {
//如果节点存在
vnode.forEach((n) => {
if (n.children) {
//如果有子节点递归
// console.log('vnode.children', n.children);
loopEvent(n.children);
}
const getTagetEvents = this.isOriginHtmlNode(n)
? n.data?.on
: n.componentOptions?.listeners;
const target = this.isOriginHtmlNode(n)
? n.data?.on[key]
: n.componentOptions?.listeners[key];
if (this.throttledEventsMap[key]) {
// 默认存储一个表类型
// this.throttledEventsMap = {
// originEvent: target,
// transEvent: fn,
// }
const getTransEventObj = this.throttledEventsMap[key].find(
(fn) => fn.originEvent === target
);
if (getTransEventObj) {
getTagetEvents[key] = getTransEventObj.transEvent;
} else if (target) {
this.throttledEventsMap[key].push({
key,
originEvent: target,
transEvent: this._throttle(target, this.time, n, key)
});
getTagetEvents[key] = this.throttledEventsMap[key].find(
(item) => item.originEvent === target
).transEvent;
}
} else if (target) {
this.throttledEventsMap[key] = [
{
key,
originEvent: target,
transEvent: this._throttle(target, this.time, n, key)
}
];
getTagetEvents[key] = this.throttledEventsMap[key].find(
(item) => item.originEvent === target
).transEvent;
}
});
}
};
loopEvent(_vnode);
});
return <div>{_vnode}</div>;
}
};
</script>
使用:
config
配置(组件传入)
更加个性化的配置可以配置
config
,从而个性化设置参数