Эта бумага синхронизирует себяBlog
Некоторое время назад команда организовала тренировочный лагерь по коду, и все собрались вместе, чтобы достичьlodash
изthrottle
а такжеdebounce
, реализовать не хлопотно, но в итоге, по сравнению с официальной, я обнаружил, что пробел в реализации функции все-таки есть.Чтобы найти свою проблему, я еще раз прочитал официальный исходный код.Это статья представляет собой резюме после того, как я прочитал его.
Примечание: в этой статье будут перечислены только код и комментарии к основным частям. Если вас интересует весь исходный код, обращайтесь ко мне напрямую.repo:
Что такое дроссель и дебаунс
throttle
(также известный как дросселирование) иdebounce
(также известный как анти-встряска) на самом деле является контроллером частоты вызовов функций, здесь только краткое введение, если вы хотите узнать больше о деталях этих двух определений, вы можете увидеть изображение, приведенное ниже, или прочитатьЛодаш документация.
throttle
: Ограничение частоты вызова функции до определенного порога, например, функция не может вызываться дважды в течение 1 с.
debounce
: Когда функция вызывается в течение n секунд, действие будет выполнено. Если функция будет вызвана снова в течение этих n секунд, предыдущее будет отменено, а время выполнения будет пересчитано. Для простого примера нам нужно предложить на основе пользовательского ввода.Когда пользователь нажимает на клавиатуру, он может отменить предыдущий и заботиться только о времени последнего ввода.
lodash
К этим двум функциям добавляются некоторые параметры, в основном следующие три:
- ведущий, функция вызывается в начале каждой задержки ожидания
- трейлинг, функция вызывается в конце каждой задержки ожидания
- maxwait (только конфигурация debounce), максимальное время ожидания, потому что если
debounce
Время вызова функции не соответствует условиям и может никогда не сработать, поэтому эта конфигурация добавлена для того, чтобы функция могла выполняться один раз через определенный период времени.
Вот прямо спойлер, на самом деле
throttle
просто установитеmaxwait
изdebounce
, так что я только представлю здесьdebounce
Код, умные читатели могут сами подумать почему.
Разница между моей реализацией и Lodash
Моя собственная реализация кода размещена в моемrepoЗдесь вы можете посмотреть, если вам интересно. Сказал до этого, что моя реализация иlodash
Есть некоторые отличия, которые показаны ниже двумя картинками.
Вот моя реализация
Это реализация lodash
Смотрите здесь, мой код имеет две основные проблемы:
-
throttle
Последняя функция выполняется дважды, и она не является стабильным рекуррентом. -
throttle
Порядок выполнения функций неверный, хотя моя функция реализована, но для каждойwait
Я, например, все делаюleading
то время
Реализация Интерпретация lodash
Далее я рассмотрю эти вопросыlodasah
код.
Реализация официального кода не очень сложна.Здесь я выкладываю код ядра и комментарии после прочтения.Об общем процессе lodash расскажу позже:
function debounce(func, wait, options) {
let lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime
// 参数初始化
let lastInvokeTime = 0 // func 上一次执行的时间
let leading = false
let maxing = false
let trailing = true
// 基本的类型判断和处理
if (typeof func != 'function') {
throw new TypeError('Expected a function')
}
wait = +wait || 0
if (isObject(options)) {
// 对配置的一些初始化
}
function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time
// 为 trailing edge 触发函数调用设定定时器
timerId = setTimeout(timerExpired, wait)
// leading = true 执行函数
return leading ? invokeFunc(time) : result
}
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime // 距离上次debounced函数被调用的时间
const timeSinceLastInvoke = time - lastInvokeTime // 距离上次函数被执行的时间
const timeWaiting = wait - timeSinceLastCall // 用 wait 减去 timeSinceLastCall 计算出下一次trailing的位置
// 两种情况
// 有maxing:比较出下一次maxing和下一次trailing的最小值,作为下一次函数要执行的时间
// 无maxing:在下一次trailing时执行 timerExpired
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
// 根据时间判断 func 能否被执行
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
// 几种满足条件的情况
return (lastCallTime === undefined //首次
|| (timeSinceLastCall >= wait) // 距离上次被调用已经超过 wait
|| (timeSinceLastCall < 0) //系统时间倒退
|| (maxing && timeSinceLastInvoke >= maxWait)) //超过最大等待时间
}
function timerExpired() {
const time = Date.now()
// 在 trailing edge 且时间符合条件时,调用 trailingEdge函数,否则重启定时器
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// 重启定时器,保证下一次时延的末尾触发
timerId = setTimeout(timerExpired, remainingWait(time))
}
function trailingEdge(time) {
timerId = undefined
// 有lastArgs才执行,意味着只有 func 已经被 debounced 过一次以后才会在 trailing edge 执行
if (trailing && lastArgs) {
return invokeFunc(time)
}
// 每次 trailingEdge 都会清除 lastArgs 和 lastThis,目的是避免最后一次函数被执行了两次
// 举个例子:最后一次函数执行的时候,可能恰巧是前一次的 trailing edge,函数被调用,而这个函数又需要在自己时延的 trailing edge 触发,导致触发多次
lastArgs = lastThis = undefined
return result
}
function cancel() {}
function flush() {}
function pending() {}
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time) //是否满足时间条件
lastArgs = args
lastThis = this
lastCallTime = time //函数被调用的时间
if (isInvoking) {
if (timerId === undefined) { // 无timerId的情况有两种:1.首次调用 2.trailingEdge执行过函数
return leadingEdge(lastCallTime)
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = setTimeout(timerExpired, wait)
return invokeFunc(lastCallTime)
}
}
// 负责一种case:trailing 为 true 的情况下,在前一个 wait 的 trailingEdge 已经执行了函数;
// 而这次函数被调用时 shouldInvoke 不满足条件,因此要设置定时器,在本次的 trailingEdge 保证函数被执行
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait)
}
return result
}
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
return debounced
}
Здесь я кратко описываю процесс словами:
При первом входе в функцию из-за того, что lastCallTime === undefined и timerId === undefined, будет выполняться leadingEdge Если в это время leading истинно, то будет выполнена func. При этом здесь будет установлен таймер, и timerExpired будет выполняться после ожидания ожидания(ов), основная функция timerExpired – запускать трейлинг.
Если функция вызывается снова до достижения ожидания, lastCallTime будет обновлен, и, поскольку isInvoking не удовлетворяет условию в это время, на этот раз ничего не будет выполнено.
Когда время дойдет до ожидания, будет выполнен таймер timerExpired, который мы установили в начале.
В это время будет добавлен новый таймер, а следующее время выполнения — оставшееся время ожидания, которое будет различаться в зависимости от того, есть ли максимальное ожидание:
- Если maxwait отсутствует, время таймера — это время ожидания — timeSinceLastCall, чтобы обеспечить выполнение следующего трейлинга.
- Если есть максимизация, минимальное значение следующего максимизации и следующего трейлинга будет сравниваться как время выполнения следующей функции.
Наконец, если больше нет вызовов функций, trailingEdge выполняется по истечении времени таймера.
Где моя проблема?
Итак, вернемся к двум вопросам выше: что именно не так с моим кодом?
Почему диаграмма последовательности неверна?
После исследования lodash относительно стабильно запускает предыдущий вызов функции во время трейлинга, в то время как у меня следующий вызов запускается каждый раз, когда maxWait. Проблема заключается в управлении таймером.
Поскольку конфликт между timer и maxwait учитывается во время кодирования, каждый раз, когда вызывается функция, она будетclearTimeout(timer)
, Так что мойtrailing
Решение на самом деле действительно только в последний раз всего потока выполнения, а не в том, что сказал lodashtrailing
контроль – это функция в каждомwait
окончательное исполнение.
И lodash не очищает таймер, но каждый раз, когда генерируется новый таймер, он будет рассчитывать следующее время выполнения в соответствии с lastCallTime, что не только обеспечивает точность таймера, но и гарантирует, что каждый раз, когда генерируется новый таймерtrailing
контроль.
Почему он срабатывает дважды в конце
Посредством ведения журнала я обнаружил, что эта ситуация срабатывания дважды очень случайна. Когда была выполнена последняя функция, это произошло, чтобы соответствовать трейну предыдущей задержки, а затем был также запущен мой собственный таймер ожидания, поэтому я наконец-то вызвал на этот раз снова , Задержка трейлинга, поэтому он запускается дважды.
Теоретически у lodash тоже будет такая ситуация, но он будет удалять lastArgs и lastThis каждый раз при выполнении функции, а при следующем выполнении функции будет судить, существуют ли эти два параметра, поэтому такой ситуации можно избежать.
Суммировать
На самом деле, я знал это раньшеdebounce
а такжеthrottle
Цель и значение , но каждый раз, когда вы его используете, вы должны взглянуть на документацию. Благодаря этой реализации и чтению исходного кода я, наконец, познакомился с ним. Я также обнаружил, что мои способности к проектированию кода все еще несовершенны. Не очень хорошо подумал.
Если вы пишете код, вам все равно нужно больше писать и читать; медленно вы можете писать и читать и делиться со всеми.