在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。
通常这种情况下我们怎么去解决的呢?一般来讲,防抖和节流是比较好的解决方案。
让我们先来看看在事件持续触发的过程中频繁执行函数是怎样的一种情况。
html 文件中代码如下
<div id="content" style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div> <script> let num = 1; let content = document.getElementById('content'); function count() { content.innerHTML = num++; }; content.onmousemove = count; </script>
在上述代码中,div 元素绑定了 mousemove 事件,当鼠标在 div(灰色)区域中移动的时候会持续地去触发该事件导致频繁执行函数。效果如下
可以看到,在没有通过其它操作的情况下,函数被频繁地执行导致页面上数据变化特别快。所以,接下来让我们来看看防抖和节流是如何去解决这个问题的。
防抖(debounce)
所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
防抖函数分为非立即执行版和立即执行版。
非立即执行版:
function debounce(func, wait) { let timeout; return function () { let context = this; let args = arguments; if (timeout) clearTimeout(timeout); timeout = setTimeout(() => { func.apply(context, args) }, wait); } }
非立即执行版的意思是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
我们依旧使用上述绑定 mousemove 事件的例子,通过上面的防抖函数,我们可以这么使用
content.onmousemove = debounce(count,1000);
效果如下
可以看到,在触发事件后函数 1 秒后才执行,而如果我在触发事件后的 1 秒内又触发了事件,则会重新计算函数执行时间。
上述防抖函数的代码还需要注意的是 this 和 参数的传递
let context = this; let args = arguments;
防抖函数的代码使用这两行代码来获取 this 和 参数,是为了让 debounce 函数最终返回的函数 this 指向不变以及依旧能接受到 e 参数。
立即执行版:
function debounce(func,wait) { let timeout; return function () { let context = this; let args = arguments; if (timeout) clearTimeout(timeout); let callNow = !timeout; timeout = setTimeout(() => { timeout = null; }, wait) if (callNow) func.apply(context, args) } }
立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。
使用方法同上,效果如下
在开发过程中,我们需要根据不同的场景来决定我们需要使用哪一个版本的防抖函数,一般来讲上述的防抖函数都能满足大部分的场景需求。但我们也可以将非立即执行版和立即执行版的防抖函数结合起来,实现最终的双剑合璧版的防抖函数。
双剑合璧版:
/** * @desc 函数防抖 * @param func 函数 * @param wait 延迟执行毫秒数 * @param immediate true 表立即执行,false 表非立即执行 */ function debounce(func,wait,immediate) { let timeout; return function () { let context = this; let args = arguments; if (timeout) clearTimeout(timeout); if (immediate) { var callNow = !timeout; timeout = setTimeout(() => { timeout = null; }, wait) if (callNow) func.apply(context, args) } else { timeout = setTimeout(function(){ func.apply(context, args) }, wait); } } }
节流(throttle)
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。
对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。
时间戳版
function throttle(func, wait) { let previous = 0; return function() { let now = Date.now(); let context = this; let args = arguments; if (now - previous > wait) { func.apply(context, args); previous = now; } } }
使用方式如下
content.onmousemove = throttle(count,1000);
效果如下
可以看到,在持续触发事件的过程中,函数会立即执行,并且每 1s 执行一次。
定时器版:
function throttle(func, wait) { let timeout; return function() { let context = this; let args = arguments; if (!timeout) { timeout = setTimeout(() => { timeout = null; func.apply(context, args) }, wait) } } }
使用方式同上,效果如下
可以看到,在持续触发事件的过程中,函数不会立即执行,并且每 1s 执行一次,在停止触发事件后,函数还会再执行一次。
我们应该可以很容易的发现,其实时间戳版和定时器版的节流函数的区别就是,时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候。
同样地,我们也可以将时间戳版和定时器版的节流函数结合起来,实现双剑合璧版的节流函数。
双剑合璧版:
/** * @desc 函数节流 * @param func 函数 * @param wait 延迟执行毫秒数 * @param type 1 表时间戳版,2 表定时器版 */ function throttle(func, wait ,type) { if(type===1){ var previous = 0; }else if(type===2){ var timeout; } return function() { let context = this; let args = arguments; if(type===1){ let now = Date.now(); if (now - previous > wait) { func.apply(context, args); previous = now; } }else if(type===2){ if (!timeout) { timeout = setTimeout(() => { timeout = null; func.apply(context, args) }, wait) } } } }
---------------------------------------------------------------------------------------------------
函数防抖和函数节流都是老生常谈的问题了。这两种方式都能优化 js 的性能。有些人可能会搞混两个的概念。所以,我以自己的理解,来解释这两个概念的含义。并且列举在小程序中这两个方法的使用。
函数防抖: 英文 debounce 有防反跳的意思,大致就是指防止重复触发。
那么,函数防抖,真正的含义是:延迟函数执行。即不管debounce函数触发了多久,只在最后一次触发debounce函数时,才定义setTimeout,到达间隔时间再执行 需要防抖的函数。
用处:多用于 input 框 输入时,显示匹配的输入内容的情况。
函数节流: 英文 throttle 有节流阀的意思。大致意思也是 节约触发的频率
那么,函数节流,真正的含义是:单位时间n秒内,第一次触发函数并执行,以后 n秒内不管触发多少次,都不执行。直到下一个单位时间n秒,第一次触发函数并执行,这个n秒内不管函数多少次都不执行。
用处:多用于页面scroll滚动,或者窗口resize,或者防止按钮重复点击等情况
其实如果只根据 控制函数触发的频率是不好区分这两个概念的。我认为两个函数都能达到防止重复触发的功能。但是函数防抖是 n秒后延迟执行;而函数节流是立马执行,n秒后再立马执行。
在小程序中,函数防抖、函数节流的使用方式:
一般都会把这两种方法封装在公用的 js 中:
tool.js
/*函数节流*/ function throttle(fn, interval) { var enterTime = 0;//触发的时间 var gapTime = interval || 300 ;//间隔时间,如果interval不传,则默认300ms return function() { var context = this; var backTime = new Date();//第一次函数return即触发的时间 if (backTime - enterTime > gapTime) { fn.call(context,arguments); enterTime = backTime;//赋值给第一次触发的时间,这样就保存了第二次触发的时间 } }; } /*函数防抖*/ function debounce(fn, interval) { var timer; var gapTime = interval || 1000;//间隔时间,如果interval不传,则默认1000ms return function() { clearTimeout(timer); var context = this; var args = arguments;//保存此处的arguments,因为setTimeout是全局的,arguments不是防抖函数需要的。 timer = setTimeout(function() { fn.call(context,args); }, gapTime); }; } export default { throttle, debounce };
使用:
import tool from "../../static/js/tool.js"; Page({ data:{ win_scrollTop:0 }, onPageScroll: tool.throttle(function(msg){ this.setData({ win_scrollTop: msg[0].scrollTop }); }), gotoUnlock: tool.debounce(function() { this.saveUserInfo(); }), saveUserInfo:function(){ console.log(111) } })
上面的两种方式只是精简版的,可能还有某些情况没考虑到,以后遇到了再优化。
函数节流的说明:
(1) 第一次执行时,是一定能执行函数的。
(2) 然后 n秒内第二次触发的时候,当第一次与第二次间隔不足 设置的间隔时间时,就不会执行。之后第三、第四次触发还是不执行。
(3) 直到 n秒之后 有且仅有一次,并且是第一次再次触发函数。
函数防抖的说明:
(1) 第一次触发函数时,定义了一个定时器。在 n秒后执行。
(2) 然后 函数第二次触发的时候,由于闭包的特性,这时候的 timer已经是第一次触发时的 定时器的标识了。然后直接清除第一次的setTimeout,这时候第一次的setTimeout里面的内容就不会执行了。然后再定义第二次的setTimeout。
(3) 然后重复第二个步骤,一直清除,又一直设置。直到函数最后一次触发,定义了最后的一个定时器,并且间隔 n秒 执行。
(4) 如果在 最后一个定时器没执行时,函数又触发了,那么又重复第三步。相当于 设置的间隔时间,只是延迟函数执行的时间,而不是间隔多少秒再执行。
到这里,这两个方式的区别就很明显了。函数节流是减少函数的触发频率,而函数防抖则是延迟函数执行,并且不管触发多少次都只执行最后一次。
-----------------------------------------------------------------------------------
参考文章:
转载:https://www.jianshu.com/p/c8b86b09daf0
https://www.cnblogs.com/zjjDaily/p/10840276.html