题外话:下班骑车路上听 陈钢先生 的《人文通识》,讲的是启功先生,讲到晚年独居的那段,差点哭出来。。。
背景
开发中有些事件会频繁触发,例如window的resize、scroll,光标行为mousedown、mousemove,键盘行为keyup、keydown等。频繁触发可看下例:https://jsbin.com/lifideyufu/edit?html,output
// 代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<style>
#container{
width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
var count = 1;
var container = document.getElementById('container');
function getUserAction(){
container.innerHTML = count++;
}
container.onmousemove = getUserAction;
</script>
</body>
</html>
光标在盒子里稍微一移动,事件就触发了几十次,那我们试想,如果这个触发事件是调用后端接口的话,那就很尴尬了。
为此,防抖和节流是两种比较好的解决方案。一般我们项目中,对于某些方法的调用,例如按钮点击事件、input的keyup事件等都会加上防护。
释义
防抖(debounce)
触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
思路:每次触发事件时都取消之前的延时调用方法,重新计时。
不想看详解的直接上代码
let debounce_timer: number = 0;
export function debounce(fn: Function, delay: number): Function {
return function(this: Function) {
let args = arguments;
clearTimeout(debounce_timer);
debounce_timer = window.setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
节流(throttle)
连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。
思路:每次触发事件时都判断当前是否有等待执行的延时函数。
不想看详解的直接上代码
const t_config: { timer: number; remaining: number; previous: Date } = {
timer: 0,
remaining: 0,
previous: new Date()
};
export function throttle(fn: Function, delay: number): Function {
return function(this: Function) {
let now = new Date(),
args = arguments;
t_config.remaining = +now - +t_config.previous;
if (t_config.remaining >= delay) {
if (t_config.timer) {
clearTimeout(t_config.timer);
}
fn.apply(this, args);
t_config.previous = now;
} else {
if (!t_config.timer) {
t_config.timer = window.setTimeout(() => {
fn.apply(this, args);
t_config.previous = new Date();
t_config.timer = 0;
}, delay - t_config.remaining);
}
}
};
}
详解
防抖(debounce)
// 第一版
function debounce(func, wait) {
var timeout;
return function () {
clearTimeout(timeout)
timeout = setTimeout(func, wait);
}
}
//调用
container.onmousemove = debounce(getUserAction, 1000);
第一版 问题:调用debounce之后this的指向被改变成Window对象。
// 第二版
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context)
}, wait);
}
}
第二版 问题:调用debounce之后event对象为undefined了。
// 第三版
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
第三版 前三版都是停止触发后才执行,就是我们把wait设置为10s,鼠标移动入盒子,等你不再移动后的10s,count才会变成1。
测试效果:https://jsbin.com/loqogucuwu/5/edit?html,output
也就是说函数不会立即执行。下面第四版添加一个参数,可变成立即执行。移入就执行函数,wait过后再移动,也立即执行。
// 第四版
function(func, wait, immediate) {
var timeout, result;
return function() {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(()=>{
timeout = null
}, wait);
if (callNow) result = func.apply(context, args);
} else {
timeout = setTimeout(()=>{
func.apply(context, args);
}, wait);
}
return result;
}
}
测试效果:https://jsbin.com/vaqowasise/edit?html,output 至此。防抖结束。
节流(throttle)
实现节流的方式有两种:一种是用时间戳,一种是用定时器。
两种方法区别:时间戳方法触发会立即执行,停止触发就不再执行。
定时器方法触发之后N秒才执行,停止触发会再执行一次。
时间戳:记录一个时间戳(第一次时间戳为0),获取当前时间,与记下的时间戳作比较,大于wait,才执行方法。
// 时间戳
function throttle(func, wait){
var context, args;
var tag = 0;
return function(){
var now = new Date().getTime();
context = this;
args = arguments;
if(now - tag > wait){
func.apply(context, args);
tag = now;
}
}
}
测试效果:https://jsbin.com/hapijivutu/1/edit?html,output。
定时器:触发事件时设置一个定时器,再次触发的时候,先判断是否存在定时器,存在则不执行,等已存在的定时器执行完毕,清空定时器,则可以再设置在执行。
// 定时器
function throttle(func,wait){
var context, args, timeout;
var tag = 0;
return function(){
context = this;
args = arguments;
if(!timeout){
timeout = setTimeout(()=>{
timeout = null;
func.apply(context, args);
}, wait)
}
}
}
参考文章: