Без сомнения, вы, вероятно, уже работали с обратными вызовами в своем путешествии по программированию, а это значит, что вы знаете, каким кошмаром и насколько непредсказуемыми могут быть асинхронные обратные вызовы. В этой статье мы рассмотрим на примере проблемы, с которыми мы сталкиваемся, и некоторые из самых простых решений, которые вы можете реализовать.
Во-первых, давайте оценим этот код
function search(criteria) { // search by this criteria doSomething( // do something with the criteria criteria, function onSuccess(data) { updateTheUI(data); } ); }
Если вы думаете, что этот код работает и хорош, вы провалили свой первый тест!
Давайте перейдем к делу, что произойдет, если функция поиска будет вызвана одновременно? Вы не контролируете, когда doSomething
вызовет ваш обратный вызов успеха, который обновит ваш пользовательский интерфейс... Что, если последующий вызов завершит свой вызов до первого?
Когда первый ответит (или разрешится), ваш обратный вызов успеха обновит пользовательский интерфейс с самым ранним результатом критерия поиска, и, таким образом, ваш пользовательский интерфейс будет не синхронизирован с тем, что должно выводить действие пользователя.
Если у вас до сих пор нет четкого представления о проблеме, предлагаю вам внимательно изучить эту песочницу кода. Откройте песочницу, откройте инструменты разработчика, а затем напишите 123 в поисковом вводе. Вы сразу заметите 3 запроса на вкладке сети ваших инструментов разработки:
Но позже, после того, как они закончат работу и будут вызваны обратные вызовы, у вас будет этот пользовательский интерфейс (я знаю, нет ничего уродливее этого!) с другим случайным цветом*:
Теперь вам не нужно иметь компилятор в голове, чтобы знать, что мы облажались и показываем результаты первого в истории поиска: когда мы набрали 1.
Возможно, вы думаете о дебаунсе, дросселировании, takeLatest, takeLeading и многих других дешевых решениях, с которыми вы могли столкнуться в своем путешествии. Но решают ли они проблему? Нет! не случайно:
debounce
иthrottle
: их легко сломать, просто подождите, пока вы не убедитесь, что задержка debounce/throttle пройдена, затем повторите поиск, и тогда вы вернетесь к исходной точке!takeLatest
иtakeLeading
, с другой стороны, кажется, решают проблему, но они не следуют, если вам нужно вызвать функцию очистки прерванной операции. Вот упражнение: заменитеtakeLatest
наtakeEvery
и посмотрите, как ведет себя ваш код.
Не волнуйтесь, мы не пессимисты, мы просто хотим решить проблему правильно, раз и навсегда. Несмотря на то, что приведенные выше методы могут частично решить проблему, лучше, если ваш разум знает другие (правильные) решения той же проблемы.
Теперь мы рассмотрим два простых решения **, одно с использованием замыканий, а другое с использованием обратных вызовов (да, обратных вызовов для решения обратных вызовов).
1. Предотвратить выполнение обратного вызова
Первое решение состоит в предотвращении выполнения старых обратных вызовов, например:
const index = 0; function executeSideEffectWithCallback(value) { index++; const callIndex = index; function callback(data) { if (callIndex === index) { printData(data); } else { console.log('prevented an old callback!'); } } invokeSideEffect(value, callback); }
Хитрость заключается в том, что каждый раз, когда вы вызываете свою функцию, вы увеличиваете целочисленную переменную и фиксируете ее значение. Позже в функции обратного вызова вы сравниваете захваченное значение (через замыкание) с исходным значением. Если между нашим первоначальным вызовом и обратным вызовом был другой вызов, callIndex
и index
не будут иметь одинаковое значение, и тогда мы можем принять решение не выполнять часть работы.
Конечно, вам нужно будет адаптировать этот код к вашим потребностям, например, используя реакцию и во время рендеринга, объявление index=0
всегда будет давать вам ноль, и решение не будет работать, вам понадобится своего рода изменяемый объект, который живет с компонент (ссылка). Давайте решим предыдущую ошибку, которая у нас была, используя эту технику в этой песочнице. Опять же, откройте инструменты разработчика и посмотрите, что происходит, поймите проблему вместе с решением.
Обратите внимание, что запросы на выборку не прерываются, они завершают свою работу в обычном режиме, а затем вызывают ваш обратный вызов. Как только ваш обратный вызов вызывается, он проверяет, относится ли он к последнему зарегистрированному вызову, если да, он вызывается.
2. Зарегистрируйте обратный вызов для очистки
Этот метод позволяет вам определить, как избавиться от того, что делает ваш исполнитель побочного эффекта: отменить выборку, очистить тайм-аут/интервал, прервать генератор (или еще более интересные вещи).
Хотя это требует, чтобы вы передали регистратор прерывания исполнителю побочного эффекта. Вот пример:
function delayedData(data, onSuccess, onAbort) { const timeoutId = setTimeout(() => { onSuccess(data); }, delay); onAbort(() => clearTimeout(timeoutId)); } //... let currentAbort = null; function caller() { invokeIfPresent(currentAbort); currentAbort = null; delayedData( search, function onSuccess(data) { printData(data); }, function registerAbort(cb) { currentAbort = cb; } }
Такой дизайн кода гарантирует, что для каждого побочного эффекта, который вы выполняете, вы поддерживаете его очистку и способ регистрации этого обратного вызова для очистки*** (назовем его двусторонним прерыванием-привязкой!). Это гарантирует, что у вас всегда будет один живой обратный вызов за раз.
Вернемся к нашему исходному примеру и решим его по этой методике, см. эту песочницу кодов. Обратите внимание, что теперь вы можете увидеть это на вкладке сети:
Таким образом, основная идея заключается в том, что всякий раз, когда вы выполняете асинхронный вызов с обратным вызовом, задайте себе следующие вопросы, прежде чем завершить свою задачу:
- Я выполняю что-то абортируемое?
- Нужно ли прерывать предыдущий вызов?
- Могу ли я иметь несколько обратных вызовов очистки?
В зависимости от ответов перепроверьте свой дизайн и убедитесь, что у вас нет проблем с параллелизмом.
Заключение
К настоящему времени вы должны знать о проблеме параллелизма асинхронных обратных вызовов, а также уметь решать ее с помощью различных методов. Теперь обсуждение подводит нас к исполнителю побочного эффекта и к тому, как его прервать/прервать. Давайте оставим это обсуждение для другого поста, в котором речь пойдет исключительно об этом.
*: Был установлен случайный цвет в качестве цвета фона тела, чтобы вы замечали каждое изменение пользовательского интерфейса более привлекательным образом.
**: Вы можете просто сделать эффект с очисткой критериев поиска в нашем примере, но это не решит всех ваших повседневных проблем, или вы получите много дублирования кода и useEffect
для любого побочного эффекта.
***: Очистки, которые вы регистрируете, могут просто переключать значение boolean
, что предотвращает выполнение обратного вызова.
Увидимся в следующий раз! Не стесняйтесь обращаться ко мне или любому члену команды, если у вас есть какие-либо вопросы.
Примечание
Этот пост изначально был написан в x-hub’s blog.