1. Почему необходимо регулировать функции?
Есть следующий код
let n = 1
window.onmousemove = () => {
console.log(`第${n}次触发回调`)
n++
}
Когда мы перемещаем мышь по странице ПК, в секунду может запускаться около 60 событий. Вы также можете посетить приведенный ниже онлайн-пример, чтобы протестировать его.
Посмотрите онлайн-пример:Дросселирование функции - проверка запуска триггеров движения мыши by Logan (@logan70) on CodePen.
Функция обратного вызова здесь просто печатает строку.Если функция обратного вызова более сложна, вполне возможно, что нагрузка на браузер будет очень высокой, что может снизить удобство работы пользователя.
resize
,scroll
илиmousemove
Обратный вызов слушателя события будет срабатывать часто, поэтому нам нужно ограничить его.
Во-вторых, реализация идеи
Функция дросселирования простоДля последовательных вызовов функций выполняйте их только один раз за каждый временной интервал.. Есть две первоначальные идеи реализации:
1. Используйте временные метки
Установите метку времени сравнения. При возникновении события используйте текущую метку времени, чтобы вычесть метку времени сравнения. Если разница больше установленного интервала, выполните функцию и замените метку времени сравнения текущей меткой времени, если разница меньше установленный интервал интервала, функция не выполняется.
function throttle(method, wait) {
// 对比时间戳,初始化为0则首次触发立即执行,初始化为当前时间戳则wait毫秒后触发才会执行
let previous = 0
return function(...args) {
let context = this
let now = new Date().getTime()
// 间隔大于wait则执行method并更新对比时间戳
if (now - previous > wait) {
method.apply(context, args)
previous = now
}
}
}
Посмотрите онлайн-пример:Регулирование функции — временная метка начальной реализации by Logan (@logan70) on CodePen.
2. Используйте таймер
Когда событие запускается в первый раз, установите таймер, выполните функцию после ожидания в миллисекундах и установите таймер наnull
, когда событие сработает позже, если таймер существует, оно не будет выполнено, а если таймер не существует, таймер будет установлен снова.
function throttle(method, wait) {
let timeout
return function(...args) {
let context = this
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
method.apply(context, args)
}, wait)
}
}
}
Посмотрите онлайн-пример:Функция throttling — начальная реализация таймера by Logan (@logan70) on CodePen.
3. Сравнение двух методов
- первый триггер: при реализации с отметкой времени он будет выполнен немедленно (когда предыдущее значение равно 0); при реализации с таймером таймер будет установлен и выполнен после ожидания в миллисекундах.
- перестать запускать: При реализации с меткой времени не будет выполняться после остановки триггера, при реализации с таймером, благодаря наличию таймера, будет выполняться один раз после остановки триггера.
3. Сценарий приложения дроссельной заслонки функции
- Реализация функции перетаскивания элемента DOM (
mousemove
) - стрелялка
mousedown/keydown
Событие (в единицу времени может быть выпущена только одна пуля) - Вычислите расстояние, на которое перемещается мышь (
mousemove
) - Холст имитирует функцию монтажной области (
mousemove
) - Поиск Леново (
keyup
) - Слушайте события прокрутки, чтобы определить, следует ли автоматически загружать больше внизу страницы:
scroll
Добавленdebounce
После этого, только после того, как пользователь перестанет прокручивать страницу, будет оцениваться, достиг ли он нижней части страницы;throttle
Если это так, пока страница прокручивается, она будет оцениваться каждый раз
В-четвертых, финальная версия функции дросселирования
Код говорит, если есть ошибка, укажите
function throttle(method, wait, {leading = true, trailing = true} = {}) {
// result 记录method的执行返回值
let timeout, result
// 记录上次原函数执行的时间(非每次更新)
let methodPrevious = 0
// 记录上次回调触发时间(每次都更新)
let throttledPrevious = 0
let throttled = function(...args) {
let context = this
// 使用Promise,可以在触发回调时拿到原函数执行的返回值
return new Promise(resolve => {
let now = new Date().getTime()
// 两次相邻触发的间隔
let interval = now - throttledPrevious
// 更新本次触发时间供下次使用
throttledPrevious = now
// 重置methodPrevious为now,remaining = wait > 0,假装刚执行过,实现禁止立即执行
// 统一条件:leading为false
// 加上以下条件之一
// 1. 首次触发(此时methodPrevious为0)
// 2. trailing为true时,停止触发时间超过wait,定时器内函数执行(methodPrevious被置为0),然后再次触发
// 3. trailing为false时(不设定时器,methodPrevious不会被置为0),停止触发时间超过wait后再次触发(interval > wait)
if (leading === false && (!methodPrevious || interval > wait)) {
methodPrevious = now
// 保险起见,清除定时器并置为null
// 假装刚执行过要假装的彻底XD
if (timeout) {
clearTimeout(timeout)
timeout = null
}
}
// 距离下次执行原函数的间隔
let remaining = wait - (now - methodPrevious)
// 1. leading为true时,首次触发就立即执行
// 2. 到达下次执行原函数时间
// 3. 修改了系统时间
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
// 更新对比时间戳,执行函数并记录返回值,传给resolve
methodPrevious = now
result = method.apply(context, args)
resolve(result)
// 解除引用,防止内存泄漏
if (!timeout) context = args = null
} else if (!timeout && trailing !== false) {
timeout = setTimeout(() => {
// leading为false时将methodPrevious设为0的目的在于
// 若不将methodPrevious设为0,如果定时器触发后很长时间没有触发回调
// 下次触发时的remaining为负,原函数会立即执行,违反了leading为false的设定
methodPrevious = leading === false ? 0 : new Date().getTime()
timeout = null
result = method.apply(context, args)
resolve(result)
// 解除引用,防止内存泄漏
if (!timeout) context = args = null
}, remaining)
}
})
}
// 加入取消功能,使用方法如下
// let throttledFn = throttle(otherFn)
// throttledFn.cancel()
throttled.cancel = function() {
clearTimeout(timeout)
previous = 0
timeout = null
}
return throttled
}
Внешняя функция, которая вызывает дросселированную функцию, также должна использовать синтаксис Async/Await для ожидания возврата результата выполнения.
См. код для использования:
function square(num) {
return Math.pow(num, 2)
}
// let throttledFn = throttle(square, 1000)
// let throttledFn = throttle(square, 1000, {leading: false})
// let throttledFn = throttle(square, 1000, {trailing: false})
let throttledFn = throttle(square, 1000, {leading: false, trailing: false})
window.onmousemove = async () => {
try {
let val = await throttledFn(4)
// 原函数不执行时val为undefined
if (typeof val !== 'undefined') {
console.log(`原函数返回值为${val}`)
}
} catch (err) {
console.error(err)
}
}
// 鼠标移动时,每间隔1S输出:
// 原函数的返回值为:16
Посмотрите онлайн-пример:Регулирование функций — окончательная версия by Logan (@logan70) on CodePen.
Конкретные этапы реализации см. ниже.
Пять, конкретные этапы реализации функции дросселирования Throttle
1. Оптимизация первой версии: Слияние двух реализаций
Результатом этого является то, что первый триггер выполняется немедленно, и он будет выполняться снова после остановки триггера.
function throttle(method, wait) {
let timeout
let previous = 0
return function(...args) {
let context = this
let now = new Date().getTime()
// 距离下次函数执行的剩余时间
let remaining = wait - (now - previous)
// 如果无剩余时间或系统时间被修改
if (remaining <= 0 || remaining > wait) {
// 如果定时器还存在则清除并置为null
if (timeout) {
clearTimeout(timeout)
timeout = null
}
// 更新对比时间戳并执行函数
previous = now
method.apply(context, args)
} else if (!timeout) {
// 如果有剩余时间但定时器不存在,则设置定时器
// remaining毫秒后执行函数、更新对比时间戳
// 并将定时器置为null
timeout = setTimeout(() => {
previous = new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
}
Давайте посмотрим, предположив, что обратный вызов запускается постоянно:
- Первый триггер: временная метка сравнения равна 0, оставшееся время отрицательное, функция выполняется немедленно, и временная метка сравнения обновляется.
- Второй триггер: оставшееся время положительное, таймер не существует, установите таймер
- После триггера: оставшееся время положительное, таймер есть, других действий не совершать
- Пока оставшееся время не станет меньше или равно 0 или функции таймера (из-за интервала срабатывания обратного вызова, а settimeout имеет ошибку, которая срабатывает и неопределенна)
- Если функция в таймере выполняется, обновите временную метку сравнения и установите для таймера значение null, а затем продолжите установку таймера для следующего триггера.
- Если функция в таймере не выполняется, но оставшееся время меньше или равно 0, очистите таймер и установите его в ноль, немедленно выполните функцию, обновите метку времени и продолжите установку таймера для следующего триггера. .
- После остановки триггера: если триггер не остановлен в два специальных момента времени, упомянутых выше, будет таймер, и исходная функция будет выполнена один раз.
Посмотрите онлайн-пример:Функция Throttling-Optimization 1st Edition: объединение двух реализаций by Logan (@logan70) on CodePen.
2. Оптимизируйте вторую версию: предоставьте элемент конфигурации, следует ли выполнять немедленно при первом запуске.
// leading为控制首次触发时是否立即执行函数的配置项
function throttle(method, wait, leading = true) {
let timeout
let previous = 0
return function(...args) {
let context = this
let now = new Date().getTime()
// !previous代表首次触发或定时器触发后的首次触发,若不需要立即执行则将previous更新为now
// 这样remaining = wait > 0,则不会立即执行,而是设定定时器
if (!previous && leading === false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
method.apply(context, args)
} else if (!timeout) {
timeout = setTimeout(() => {
// 如果leading为false,则将previous设为0,
// 下次触发时会与下次触发时的now同步,达到首次触发(对于用户来说)不立即执行
// 如果直接设为当前时间戳,若停止触发一段时间,下次触发时的remaining为负值,会立即执行
previous = leading === false ? 0 : new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
}
Посмотрите онлайн-пример:Версия 2 оптимизации регулирования функции: предоставляет элемент конфигурации, который выполняется немедленно при первом запуске. by Logan (@logan70) on CodePen.
3. Оптимизировать третью версию: предоставить элемент конфигурации, следует ли выполнять один раз после остановки триггера.
// trailing为控制停止触发后是否还执行一次的配置项
function throttle(method, wait, {leading = true, trailing = true} = {}) {
let timeout
let previous = 0
return function(...args) {
let context = this
let now = new Date().getTime()
if (!previous && leading === false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
method.apply(context, args)
} else if (!timeout && trailing !== false) {
// 如果有剩余时间但定时器不存在,且trailing不为false,则设置定时器
// trailing为false时等同于只使用时间戳来实现节流
timeout = setTimeout(() => {
previous = leading === false ? 0 : new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
}
Посмотрите онлайн-пример:Версия 3 оптимизации регулирования функции: укажите элемент конфигурации, указывающий, следует ли выполняться один раз после остановки триггера. by Logan (@logan70) on CodePen.
4. Оптимизируйте четвертую версию: предоставьте функцию отмены
Иногда нам нужно иметь возможность вручную отменить троттлинг в нетриггерный период, код реализован следующим образом:
function throttle(method, wait, {leading = true, trailing = true} = {}) {
let timeout
let previous = 0
// 将返回的匿名函数赋值给throttled,以便在其上添加取消方法
let throttled = function(...args) {
let context = this
let now = new Date().getTime()
if (!previous && leading === false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
method.apply(context, args)
} else if (!timeout && trailing !== false) {
timeout = setTimeout(() => {
previous = leading === false ? 0 : new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
// 加入取消功能,使用方法如下
// let throttledFn = throttle(otherFn)
// throttledFn.cancel()
throttled.cancel = function() {
clearTimeout(timeout)
previous = 0
timeout = null
}
// 将节流后函数返回
return throttled
}
Посмотрите онлайн-пример:Регулирование функций — оптимизация, 4-е издание: обеспечивает отмену by Logan (@logan70) on CodePen.
5. Оптимизация, пятое издание: обработка возвращаемого значения исходной функции
Функция, которую нужно регулировать, может иметь возвращаемое значение, нам нужно разобраться с этой ситуацией,underscore
Метод обработки заключается в возврате возвращаемого функцией значения в возвращаемомdebounced
Функция возвращается снова, но это на самом деле проблема. Если исходная функция выполняется вsetTimeout
Внутри возвращаемое значение нельзя получить синхронно, мы используем Promise для обработки возвращаемого значения исходной функции.
function throttle(method, wait, {leading = true, trailing = true} = {}) {
// result记录原函数执行结果
let timeout, result
let previous = 0
let throttled = function(...args) {
let context = this
// 返回一个Promise,以便可以使用then或者Async/Await语法拿到原函数返回值
return new Promise(resolve => {
let now = new Date().getTime()
if (!previous && leading === false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
result = method.apply(context, args)
// 将函数执行返回值传给resolve
resolve(result)
} else if (!timeout && trailing !== false) {
timeout = setTimeout(() => {
previous = leading === false ? 0 : new Date().getTime()
timeout = null
result = method.apply(context, args)
// 将函数执行返回值传给resolve
resolve(result)
}, remaining)
}
})
}
throttled.cancel = function() {
clearTimeout(timeout)
previous = 0
timeout = null
}
return throttled
}
Как использовать один: при вызове функции дросселирования используйтеthen
Получить возвращаемое значение исходной функции
function square(num) {
return Math.pow(num, 2)
}
let throttledFn = throttle(square, 1000, false)
window.onmousemove = () => {
throttledFn(4).then(val => {
console.log(`原函数的返回值为:${val}`)
})
}
// 鼠标移动时,每间隔1S后输出:
// 原函数的返回值为:16
Используйте метод два: внешняя функция, вызывающая регулируемую функцию, использует синтаксис Async/Await для ожидания возврата результата выполнения.
См. код для использования:
function square(num) {
return Math.pow(num, 2)
}
let throttledFn = throttle(square, 1000)
window.onmousemove = async () => {
try {
let val = await throttledFn(4)
// 原函数不执行时val为undefined
if (typeof val !== 'undefined') {
console.log(`原函数返回值为${val}`)
}
} catch (err) {
console.error(err)
}
}
// 鼠标移动时,每间隔1S输出:
// 原函数的返回值为:16
Посмотрите онлайн-пример:Регулирование функции — оптимизация, пятое издание: обработка возвращаемого значения исходной функции by Logan (@logan70) on CodePen.
6. Оптимизированная версия 6: Немедленное выполнение и последующее выполнение могут быть отключены одновременно.
подражатьunderscore
Реализованная функция дросселирования имеет ложку дегтя, т.е.leading:false
а такжеtrailing: false
нельзя установить одновременно.
Если установлено одновременно, например, когда вы перемещаете мышь, потому чтоtrailing
Установить какfalse
, таймер не будет установлен, когда триггер остановлен, поэтому, пока проходит установленное время, а затем двигаться,remaining
Если это отрицательное число, оно будет выполнено немедленно, что нарушаетleading: false
, здесь оптимизируем идею следующим образом:
Вычислить временной интервал между двумя последовательными обратными вызовами триггера. Если он больше установленного значения интервала, сбросить временную метку сравнения на текущую временную метку, что эквивалентно возврату к первому триггеру, достигая эффекта запрета первого триггера (псевдо) немедленное выполнение, код выглядит следующим образом, пожалуйста, укажите на любые ошибки:
function throttle(method, wait, {leading = true, trailing = true} = {}) {
let timeout, result
let methodPrevious = 0
// 记录上次回调触发时间(每次都更新)
let throttledPrevious = 0
let throttled = function(...args) {
let context = this
return new Promise(resolve => {
let now = new Date().getTime()
// 两次触发的间隔
let interval = now - throttledPrevious
// 更新本次触发时间供下次使用
throttledPrevious = now
// 更改条件,两次间隔时间大于wait且leading为false时也重置methodPrevious,实现禁止立即执行
if (leading === false && (!methodPrevious || interval > wait)) {
methodPrevious = now
}
let remaining = wait - (now - methodPrevious)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
methodPrevious = now
result = method.apply(context, args)
resolve(result)
// 解除引用,防止内存泄漏
if (!timeout) context = args = null
} else if (!timeout && trailing !== false) {
timeout = setTimeout(() => {
methodPrevious = leading === false ? 0 : new Date().getTime()
timeout = null
result = method.apply(context, args)
resolve(result)
// 解除引用,防止内存泄漏
if (!timeout) context = args = null
}, remaining)
}
})
}
throttled.cancel = function() {
clearTimeout(timeout)
methodPrevious = 0
timeout = null
}
return throttled
}
Посмотрите онлайн-пример:Регулирование функций — оптимизация 6-го издания: немедленное выполнение и последующее выполнение могут быть отключены одновременно. by Logan (@logan70) on CodePen.
6. Справочные статьи
Тема JavaScript: научитесь сокращать количество потоков с помощью подчеркивания
Реализация дросселирования функции подчеркивания
Если есть какие-либо ошибки или неточности, пожалуйста, поправьте меня, большое спасибо.