开发中你可能会遇到下面的情况:
- 监听 Window 对象的 resize,scroll 事件
- 拖拽时监听 mousemove
- 文字输入时,对输入字符串进行处理,比如要把 markdwon 转换成 html
- 监听文件变化,重启服务
第一种和第三种情况,事件短时间内被频繁出发,如果在事件中有大量的计算,频繁操作 DOM,资源加载等重行为,可能会导致 UI 卡顿,严重点甚至让浏览器挂掉。对于第四种情况,有的开发者保存编辑好的文件喜欢按多次 Ctrl+S,若是快速的重启服务还能 Hold 住,但是要是重启一个应用,就可能多次不必要的重启。
函数节流
// 节流throttle代码(时间戳):
// 函数按照一个周期执行,当高频事件触发时,第一次会立即执行(给 scroll 事件绑定函数与真正触发事件的间隔一般大于 delay),而后再怎么频繁地触发事件,也都是每 delay 时间才执行一次。
var throttle = function (func, delay) {
var prev = Date.now();
return function () {
var context = this;
var args = arguments;
var now = Date.now();
if (now - prev >= delay) {
func.apply(context, args);
prev = Date.now();
}
};
};
function handle() {
console.log(Math.random());
}
window.addEventListener("scroll", throttle(handle, 1000));
// 节流throttle代码(定时器):
// 当触发事件的时候,我们设置一个定时器,再次触发事件的时候,如果定时器存在,就不执行,直到delay时间后,定时器执行执行函数,并且清空定时器,这样就可以设置下个定时器。当第一次触发事件时,不会立即执行函数,而是在delay秒后才执行。而后再怎么频繁触发事件,也都是每delay时间才执行一次。当最后一次停止触发后,由于定时器的delay延迟,可能还会执行一次函数。
var throttle = function (func, delay) {
var timer = null;
return function () {
var context = this;
var args = arguments;
if (!timer) {
timer = setTimeout(function () {
func.apply(context, args);
timer = null;
}, delay);
}
};
};
function handle() {
console.log(Math.random());
}
window.addEventListener("scroll", throttle(handle, 1000));
函数去抖
当事件触发之后,必须等待某一个时间(N)之后,回调函数才会执行,假若再等待的时间内,事件又触发了则重新再等待时间 N,直到事件 N 内事件不被触发,那么最后一次触发过了事件 N 后,执行函数。
还是窗口 resize,如果一直改变窗口大小,则不会打印 1,只有停止改变窗口大小并等待一段时间后,才会打印 1。
//函数去抖简单实现:
/**
* @param func {Function} 实际要执行的函数
* @param delay {Number} 延迟时间,单位是毫秒(ms)
* @return {Function}
*/
function debounce(fn, delay = 1000) {
let timer;
// 返回一个函数,这个函数会在一个时间区间结束后的 delay 毫秒时执行 func 函数
return function () {
// 保存函数调用时的上下文和参数,传递给func
var context = this;
var args = arguments;
// 函数被调用,清除定时器
clearTimeout(timer);
// 当返回的函数被最后一次调用后(也就是用户停止了某个连续的操作),
// 再过 delay 毫秒就执行 func
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}
//应用场景,监听文件变化,重启应用:
const debounce = require("./debounce");
watcher.on(
"change",
debounce(() => {
const child = spawn("npm", ["run", "dev:electron"], {
cwd,
detached: true,
stdio: "inherit",
});
child.unref();
electron.app.quit();
}, delay)
);
去抖应用在防止按钮重复点击的场合,会在连续点击结束后才运行,我们希望第一次点击就运行,可进行下面优化
// isImmeDiate: 为true时立即执行
const debounce = function (fn, delay, isImmeDiate = false) {
let timeout = null;
const debounced = function () {
const context = this;
const args = arguments;
if (timeout) clearTimeout(timeout);
if (isImmeDiate) {
// 判断是否已经执行过,不要重复执行
// setTimeout也是一直在更新的
let callNow = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, delay);
if (callNow) result = fn.apply(context, args);
} else {
timeout = setTimeout(() => {
fn.apply(context, args);
}, delay);
}
return result;
};
// 增加取消函数
debounced.prototype.cancle = function () {
clearTimeout(timeout);
timeout = null;
};
return debounced;
};
注意
上面所有方法,在使用时不可直接在 onclick 方法后调用,以 jsx 语法为例
<button onClick={debounce(fn)}>
这样不会生效,正确的逻辑是先调用一次,返回内部一个 debounced 的 function,之后每次调用时使用的是该内部 function
react 内的正确用法
class addProduct extends React.Component {
constructor(props) {
super(props);
// 先调用一次,拿到内部的function
// 后面使用的是内部的function
this.handleSubmit2 = debounce(this.handleSubmit)
}
handleSubmit(e) {
// ...
}
return (
<div>
<Button
onClick={e => this.handleSubmit2(e)}
>
提交
</Button>
</div>
);
}
export default addProduct;
总结
函数防抖:将几次操作合并为一此操作进行。原理是维护一个计时器,规定在 delay 时间后触发函数,但是在 delay 时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。
区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
林秀栋的技术博客