この記事は最初に公開されました: https://github.com/bigo-frontend/blog/ フォローと再投稿を歓迎します。
js 関数の手ぶれ防止とスロットリング
手ぶれ補正スロットリング
コンセプト
-
debounce : 非常に頻繁にトリガーされるイベント (キーストロークなど) を 1 つの実行にマージします。
-
throttle : 200 ミリ秒ごとのスクロール位置のチェックや CSS アニメーションのトリガーなど、X ミリ秒ごとの一定数の実行を保証します。
-
requestAnimationFrame : スロットルの代わりに使用でき、関数が画面上の要素を再計算してレンダリングする必要がある場合、アニメーションや変更の滑らかさを保証するために使用できます。注: IE9 はサポートされていません。
アンチシェイクとスロットルの大きな違いは、スロットルは指定された時間間隔内でのみタスクを実行するのに対し、アンチシェイクはタスクを実行する前にタスク間の間隔が特定のしきい値を超えることを意味することです。
デバウンス
手ぶれ補正機能とdebounce
は、一定期間内の関数を指します。コールバックがどれだけトリガーされても、最後にのみ実行されます。つまり、頻繁に発生するイベントをトリガーするイベントが 1 回の実行にまとめられます (キーボード入力など)。 )。
実現原理は、タイマーを使用し、関数が初めて実行されるときにタイマーを設定し、呼び出されたときにタイマーが設定されていたことが判明したときに前のタイマーをクリアし、新しいタイマーをリセットすることです。クリアされたタイマーは、タイマーの期限が切れたときに関数の実行をトリガーします。
cosnt debounce = function(func, wait, immediate) {
let timer = null;
// 定时器计时结束后
// 1、清空计时器,使之不影响下次连续事件的触发
// 2、触发执行 func
let later = function(context, args) {
timer = null;
return func.apply(context, args);
}
let handler = function(...args) {
const context = this;
if (timer) {
clearTimeout(timer);
}
let result;
// immediate为true则首次立马执行函数,不需要等wait时间
// 根据 timer 是否为空可以判断是否是首次触发
if (immediate) {
let shouldCall = !timer;
timer = setTimeout(later.bind(null, context, args), wait);
if (shouldCall) {
result = func.apply(context, args);
}
} else {
timer = setTimeout(later.bind(null, context, args), wait);
}
return;
}
return handler;
}
// DEMO1
// 执行 debounce 函数返回新函数
const betterFn = debounce(() => console.log('fn 防抖执行了'), 1000)
// 停止滑动 1 秒后执行函数 () => console.log('fn 防抖执行了')
document.addEventListener('scroll', betterFn)
// DEMO2
// 执行 debounce 函数返回新函数
const betterFn = debounce(() => console.log('fn 防抖执行了'), 1000, true)
// 立即执行函数 () => console.log('fn 防抖执行了')
// 停止滑动 1 秒后再次执行函数 () => console.log('fn 防抖执行了')
document.addEventListener('scroll', betterFn)
スロットル
200 ミリ秒ごとのページ スクロールの処理など、X ミリ秒ごとに一定の実行回数を保証します。
実装オプションは 2 つあります。
- 1 つ目は、タイムスタンプを使用して実行時刻が到来したかどうかを判断し、最後の実行のタイムスタンプを記録し、イベントがトリガーされるたびにコールバックを実行し、現在のタイムスタンプとタイムスタンプの間の間隔がコールバック内で判断されることです。最後の実行タイムスタンプが時間差 (Xms) に達している場合は、それを実行し、最後の実行のタイムスタンプを更新します。
function throttle(func, threshold = 200) {
// 上一次执行fn的时间
let previous = 0;
return function(...args) {
// 获取当前时间
let now = +new Date();
// 将当前时间和上一次执行函数的时间进行对比
// 大于等待时间就把 previous 设置为当前时间并执行函数 fn
if (now - previous > threshold) {
previous = now;
func.apply(this, args);
}
}
}
- 2 番目の方法は、タイマーを使用することです。たとえば、
scroll
イベントがトリガーされたばかりのときに、hello worldを出力し、1000ms
タイマーを設定します。その後、イベントがトリガーされるたびにscroll
、コールバックがトリガーされます。タイマーがすでにトリガーされている場合は、存在する場合、コールバックはタイマーが設定されるまでメソッドを実行しません。タイマーがトリガーされ、handler
クリアされてからタイマーがリセットされます。
function throttle(func, threshold = 200) {
// 设置一个定时器
let timer = null;
return function(...args) {
// 如果定时器存在则返回
if (timer) return;
timer = setTimeout(() => {
// 执行函数并将定时器置为null
timer = null;
func.apply(this, args);
}, threshold);
}
}
上記 2 つのタイマーの応答時間を見てみましょう。最初のタイマーはすぐに実行され、2 番目のタイマーは実行前にしきい値を待つ必要があります。すぐに実行できるタイマーを実装することはできますか、またはしきい値を待つことはできますか?後で実行される関数、それともすぐに実行でき、最後に再度実行される関数ですか?
上記 2 つの方法を組み合わせて、先頭と末尾に判定パラメータを追加しました。
- イベント開始時のコールバックに応答するかどうかを設定します(
leading
パラメータ、false
無視) - イベント終了後のコールバックに応答するかどうかを設定します(
trailing
パラメータ、false
無視)
const throttle = function(func, wait, options) {
var timeout, context, args, result;
// 上一次执行回调的时间戳
var previous = 0;
// 无传入参数时,初始化 options 为空对象
if (!options) options = {
};
var later = function() {
// 当设置 { leading: false } 时
// 每次触发回调函数后设置 previous 为 0
// 不然为当前时间
previous = options.leading === false ? 0 : _.now();
// 防止内存泄漏,置为 null 便于后面根据 !timeout 设置新的 timeout
timeout = null;
// 执行函数
result = func.apply(context, args);
if (!timeout) context = args = null;
};
// 每次触发事件回调都执行这个函数
// 函数内判断是否执行 func
// func 才是我们业务层代码想要执行的函数
var throttled = function() {
// 记录当前时间
var now = _.now();
// 第一次执行时(此时 previous 为 0,之后为上一次时间戳)
// 并且设置了 { leading: false }(表示第一次回调不执行)
// 此时设置 previous 为当前值,表示刚执行过,本次就不执行了
if (!previous && options.leading === false) previous = now;
// 距离下次触发 func 还需要等待的时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 要么是到了间隔时间了,随即触发方法(remaining <= 0)
// 要么是没有传入 {leading: false},且第一次触发回调,即立即触发
// 此时 previous 为 0,wait - (now - previous) 也满足 <= 0
// 之后便会把 previous 值迅速置为 now
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
// clearTimeout(timeout) 并不会把 timeout 设为 null
// 手动设置,便于后续判断
timeout = null;
}
// 设置 previous 为当前时间
previous = now;
// 执行 func 函数
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 最后一次需要触发的情况
// 如果已经存在一个定时器,则不会进入该 if 分支
// 如果 {trailing: false},即最后一次不需要触发了,也不会进入这个分支
// 间隔 remaining milliseconds 后触发 later 方法
timeout = setTimeout(later, remaining);
}
return result;
};
return throttled;
};
// DEMO
// 执行 throttle 函数返回新函数
const betterFn = throttle(() => console.log('fn 函数执行了'), 1000)
// 每 10 毫秒执行一次 betterFn 函数,但是只有时间差大于 1000 时才会执行 fn
setInterval(betterFn, 10)
requestAnimationFrame
スロットルの代わりに使用でき、関数が画面上の要素を再計算してレンダリングする必要がある場合に、アニメーションや変更のスムーズさを確保するために使用できます。注: IE9 はサポートされていません。
let start = null;
const element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';
const step = function(timestamp) {
if (!start) start = timestamp;
let progress = timestamp - start;
element.style.left = Math.min(progress / 10, 200) + 'px';
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
議論するメッセージを残してくださる皆様を歓迎します。スムーズな仕事と幸せな生活をお祈りしています。
私は bigo のフロントエンドです。次号でお会いしましょう。