題外話:下班騎車路上聽 陳鋼先生 的《人文通識》,講的是啓功先生,講到晚年獨居的那段,差點哭出來。。。
背景
開發中有些事件會頻繁觸發,例如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)
}
}
}
參考文章: