个人笔记
需求:如何按按钮时频繁地发送请求?
最简单的处理:定义一个标识,来表示是否结束发送请求,结束了才可以重新请求
let box = document.querySelector('.box');
// 模拟获取数据
function queryData(callback) {
setTimeout(() => {
typeof callback === "function" ? callback('OK') : null;
}, 1000);
}
let isRun = false;
box.onclick = function () {
if (isRun) return;
isRun = true;
queryData(result => {
console.log(result);
isRun = false;
});
};
缺点:当项目中有很多按钮的时候,需要定义的标识太多了,并且按钮触发频率还是不可控,即使等请求结束再发送请求,仍然会产生性能上的损耗。
防抖和节流的概念
防抖:在行为频繁的触发下,只识别一次。(可以控制识别第一次或最后一次)。我们自己可以规定频发触发的条件。例如,规定300ms内,只要触发多次就算是频繁触发。当频繁触发时,两次中间只要间隔小于300ms,最终只触发一次,触发间隔超过300ms,就算开始了第二轮执行。
节流:节流也是降低触发的频率,把传入的函数按照一定的频率执行,即限制一个函数在一定时间内只能执行一次,以此降低触发的频率。浏览器有自己的最快反应时间,谷歌是5-7ms,IE是10-17ms。在我们的频繁操作下,例如频繁点击按钮,谷歌浏览器的频率是5ms执行一次。意思就是,即使触发的速度再快,例如5ms内触发了两次,最终仍然算触发一次。节流就是降低这个频率,例如我们设定频率是300ms,我们在频繁触发的过程中限制设置为300ms,那么不管触发几次,300ms内只会执行一次。
- 防抖举例:一般点击事件的优化都以为多,时间间隔内多次点击只触发一次。
- 节流举例:键盘输入事件或者滚动条滚动事件都是以节流为主。例如输入框的自动搜索, 在输入时可限制发请求的次数。滚动条滚动时,防止坚挺的滚动时间触发次数太多。又或者轮播图的点击切换可以使用节流限制,比如300ms内点击多次只切换一次,防止轮播图滚动过快。
onresize,scroll,mousemove,mousehover等这些事件的触发频率很高。如果我们不处理,浏览器会帮我们每隔大概5ms触发一次,会很消耗性能。除此之外,重复的ajax调用不仅可能会造成请求数据的混乱,还会使网络拥塞,占用服务器带宽,增加服务器压力
防抖
思路:
let box = document.querySelector('.box');
function debounce() {
return function proxy() {
};
}
function fn() {
console.log('OK');
}
box.onclick = debounce(fn, 300, true);
box.onclick=proxy : 疯狂点击box,疯狂触发proxy,但是我们最终想执行的是fn,所以需要我们在proxy中,基于一些逻辑的处理,让fn只执行一次即可
简易版(边界触发):不关心是第一次就触发,还是最后一次触发,默认为最后一次触发
function debounce(func, wait) {
if (typeof func !== "function") throw new TypeError('func must be required and be an function!');
if (typeof wait !== "number") wait = 300;
var timer = null
return function proxy() {
var params = [].slice.call(arguments),
self = this;
if (timer) clearTimeout(timer); //timer=null 省略,因为下面直接给timer赋值了 // 如果之前的300毫秒还没有执行完,那就清除之前的
timer = setTimeout(function () {
if (timer) { //当最新的结束后,把没用的这个定时器也清掉「良好的习惯」
clearTimeout(timer);
timer = null;
}
func.apply(self, params)
}, wait);
};
}
可以借助一些es6语法
const debounce = function (fn, wait) {
if (typeof fn !== 'function') throw new TypeError('fn must be function')
if (typeof wait !== 'number') wait = 300//默认是300
let timer = null
return function proxy(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
clearTimeout(timer)
timer = null
fn.apply(this, args)
}, wait)
}
}
完整版:传入参数immediate控制是否第一次触发
/*
* debounce:函数防抖
* @params
* func「function,required」:最后要执行的函数
* wait「number」:设定的频发触发的频率时间,默认值是300
* immediate「boolean」:设置是否是开始边界触发(是否立即执行,true就在第一次触发的时候执行,不传就在最后一次执行),默认值是false
* @return
* func执行的返回结果
*/
function debounce(func, wait, immediate) {
if (typeof func !== "function") throw new TypeError('func must be required and be an function!');
if (typeof wait === "boolean") {//兼容这种情况debounce(fn,true)
immediate = wait;
wait = 300;
}
if (typeof wait !== "number") wait = 300;//初始化参数值,不管什么情况,都初始化为300,比如兼容这种情况debounce(fn),增强健壮性
if (typeof immediate !== "boolean") immediate = false;//初始化参数值
var timer = null,
result;
return function proxy() {
var runNow = !timer && immediate,//一个是否立即执行的标记,没有timer(代表完全新打开页面第一次点击和 点击完,执行过以后被清除,即完全重新开始 的情况),并且是传入参数是true,就立即执行
params = [].slice.call(arguments),//将伪数组集合变为数组
self = this;
if (timer) clearTimeout(timer); //干掉之前的
timer = setTimeout(function () {
if (timer) { //当最新的结束后,把没用的这个定时器也干掉「良好的习惯」
clearTimeout(timer);//所有的定时器都没有了,一切从头开始
timer = null;
};
!immediate ? result = func.apply(self, params) : null;//这里判断是否边界执行(最后那次执行)
}, wait);
runNow ? result = func.apply(self, params) : null;//里判断是否立即执行(触发之后立即执行)
return result;//有可能有的地方会用到返回值
};
}
function fn(ev) {
console.log('OK', ev, this);
}
box.onclick = debounce(fn, 300, true);
在疯狂点击缶,只触发了一次,并且函数的参数和this都正确的传到里面去了
注:这里apply传入伪数组作为参数其实也是可以的
小tip:
为什么[].slice.call(arguments) 会将伪数组集合变为数组?
MDN slice-polyfill
polyfill里面原理是用了for...i循环+push,所以[].slice.call(arguments)会返回真的数组,实际上只是做了for循环+push而已
节流
function fn() {
console.log('OK');
}
window.onscroll =fn
基础方案(两种方式)
- 当前执行的时间减去上次执行的时间,看是否大于wait
// 时间戳方案
function throttle(fn,wait){
var pre = Date.now();
return function(){
var context = this;
var args = arguments;
var now = Date.now();
if( now - pre >= wait){
fn.apply(context,args);
pre = Date.now();
}
}
}
function handle(){
console.log(Math.random());
}
window.addEventListener("mousemove",throttle(handle,1000));
const throttle = function (fn, wait) {
if (typeof fn !== 'function') throw new TypeError('fn must be function')
if (typeof wait !== 'number') wait = 300//默认是300
let pre = Date.now();//当前执行的时间戳
return function proxy(...args) {
let now = Date.now()
if (now - pre >= wait) {
fn.apply(this, args)
pre = Date.now()
}
}
}
- 定时器执行完成后再执行下一次
// 定时器方案
function throttle(fn,wait){
var timer = null;
return function(){
var context = this;
var args = arguments;
if(!timer){
timer = setTimeout(function(){
fn.apply(context,args);
timer = null;
},wait)
}
}
}
function handle(){
console.log(Math.random());
}
window.addEventListener("mousemove",throttle(handle,1000));
const throttle2 = function (fn, wait){
if (typeof fn !== 'function') throw new TypeError('fn must be function')
if (typeof wait !== 'number') wait = 300//默认是300
let timer = null
return function proxy(...args) {
if(!timer){
timer = setTimeout(()=>{
clearTimeout(timer)
timer = null
fn.apply(this, args)
}, wait)
}
}
}
underscore库的源码版
function throttle(func, wait) {
if (typeof func !== "function") throw new TypeError('func must be required and be an function!');
if (typeof wait !== "number") wait = 300;
var timer = null,
previous = 0,//记录上次执行完的时间,为了保证第一次可以立即执行一次,初始化为0
result;
return function proxy() {
var now = +new Date(),
remaining = wait - (now - previous),//remaining代表这一次到上一次之间,还差多长时间到500毫秒
self = this,
params = [].slice.call(arguments);
if (remaining <= 0) {//第一次肯定小于零,立即执行一次,如果以后不再500ms的间隔之内,已经超过了500ms,那么不需要等待了,立即执行,并且重定previous
// 立即执行即可
if (timer) {//清上一个已经运行完的定时器,良好的习惯
clearTimeout(timer);
timer = null;
}
result = func.apply(self, params);
previous = +new Date();//更新上次执行完的时间
} else if (!timer) {
// 没有达到间隔时间,而且之前也没有设置过定时器(或者定时器被清除),此时我们设置定时器,等到remaining后执行一次
timer = setTimeout(function () {
if (timer) {
clearTimeout(timer);
timer = null;
}
result = func.apply(self, params);
previous = +new Date();
}, remaining);
}
return result;
};
}
以上方法第一次可以立即触发,并且以后在500ms之内触发了,就要等到500ms的间隔到了之后才会触发