#javascript #reactjs
Вопрос:
Я пишу useDebounce
утилитарный крючок.
function debounce(fn, delay) {
let timer = 0;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
}
function useDebounce(fn, delay) {
const ref = React.useRef(fn);
React.useLayoutEffect(() => {
ref.current = fn;
}, [fn]);
return useMemo(() => debounce(ref.current, delay), [delay]);
}
Я использую ref
для хранения обратного вызова и его обновления useLayoutEffect
, чтобы пользователям API не нужно было запоминать свой собственный обратный вызов. А также я хотел заранее ответить, что я знаю, как useMemo
это работает, и я знаю, что вы можете запомнить обратный вызов, т. Е. fn
переданный useDebounce
извне, но я не хочу, чтобы это ложилось бременем на пользователей API, поэтому я сделал это сам.
Вот живая демонстрация: https://codesandbox.io/s/closure-bug-xcvyd?file=/src/App.js
Теперь функция, которую я хочу осудить, это
const increment = () => {
console.log(count);
setCount(count 1);
};
поэтому я просто передал его, useDebounce
но, похоже, функция закончилась устаревшим закрытием count
, потому что она обновляется только count
из 0
->> 1
, а затем после этого, независимо от того, сколько раз вы нажимаете на кнопку, она больше не обновляется.
Да, я знаю, что могу написать setCount(c => c 1);
, чтобы обойти эту проблему.
Но что меня озадачило, так это то, что если я перепишу useMemo(() => debounce(ref.current, delay), [delay]);
return useMemo(() => debounce((...args) => ref.current(...args), delay), [ delay ]);
, то эта проблема будет устранена автоматически.
Кажется, я не могу понять, как (...args) => ref.current(...args)
решить эту проблему.
Ответ №1:
Давайте посмотрим, что происходит шаг за шагом.
- Вы размещаете
fn
внутри ссылки. - Вы обновляете ссылку с новым значением
- Вы переходите
fn
к дебату внутриuseMemo
, и именно в этом заключается ошибка.
При следующем рендеринге вы снова обновляете ref
, но функция запоминания не использует ее вообще. Он запоминает ссылку на самый первый пройденный fn
, и это изменится только тогда, когда пользователь вашего крючка изменит задержку.
В фиксированном примере с функцией стрелки происходит вот что:
- Вы размещаете
fn
внутри ref - Вы обновляете ссылку с новым значением
- Вы запоминаете функцию, которая закрывается
ref
, и будете заглядывать в нее при каждом вызове, чтобы она выбирала самое свежееfn
значение из ref.
function useDebounce(fn, delay) {
// storing function into ref
const ref = React.useRef(fn);
// updating function after memoization, and on each render when function changed
React.useLayoutEffect(() => {
ref.current = fn;
}, [fn]);
return useMemo(function() {
// here you are referencing current `fn`
// The very first `fn` that was passed into hook
// ref don't play role here - you are passing `ref.current`
let fnToDebounce = ref.current
return debounce(fnToDebounce, delay)
}, [delay]);
}
function useDebounce(fn, delay) {
// storing function into ref
const ref = React.useRef(fn);
// updating function after memoization, and on each render when function changed
React.useLayoutEffect(() => {
ref.current = fn;
}, [fn]);
return useMemo(function() {
// Here you are closuring reference to `ref` and `fnToDebounce` have no idea what is inside it.
// When it would be called, it will get ref and only at that point will retrieve `ref.current`, that will be the latest `fn`
let fnToDebounce = (...args) => ref.current(...args);
return debounce(fnToDebounce, delay);
}, [delay]);
}
Комментарии:
1. хм, мне все еще трудно это понять. «При следующем рендеринге вы снова обновляете ссылку, но функция запоминания не использует ее вообще». это почему? не
fn
следует ли использовать обновленное черезref.current
, такref.current
как оно всегда указывает на свежееfn
? Я что-то здесь упускаю?2. Объекты передаются по ссылке (ссылка на память), а функция является объектом. Когда вы переходите
ref.current
вdebounce
себя, вы передаете ссылку на тех, кто был сохранен первымfn
. Во втором случае вы передаете функцию, которая будет обращаться к ссылке по ссылке и получатьcurrent
от нее свежие данные. Извините, мой английский здесь не очень хорош. Я постараюсь прокомментировать ваш код3. да, я знаю, что функции в javascript являются вызываемыми объектами, а объекты передаются по ссылке, которая на самом деле является просто кучей указателей на память. Вот именно здесь я не совсем понимаю. поступая
useMemo(() => debounce(ref.current, delay), [delay]);
таким образом , мы передаемref.current
информацию, которая обновляется приuseLayoutEffect
каждомfn
изменении, верно? так не следует лиref.current
указать на самый последнийfn
?4. Нет. Вы не обновляете ссылку на ref.current. Вы обновляете
ref
. Это какlet obj = {a: 1}; obj.a = 2
. Если вы закрылисьobj.a
, прежде чем сменить его на2
, вы навсегда получите1
. Но если вы закроетесьobj
, вы получите самое свежее состояние всякий раз, когда будете пытаться получитьobj.a
5. хорошо, я думаю, я вроде как понял…. спасибо! но я думаю, что в фрагменте кода, который вы только что опубликовали, вам нужно явно вернуться
debounce(fnToDebounce, delay)
, чтобы заставить его работать, я думаю?
Ответ №2:
Это перейдет ref.current
к дебату.
useMemo(() => debounce(ref.current, delay), [delay]);
Это эквивалентно этому:
useMemo(() => debounce(fn, delay), [delay]);
Функция запоминания будет создана только при первом вызове крючка. В закрытии будет оригинал increment
, который заключает оригинал count
, но ref
не заключен.
В этой версии, однако, вы передаете лямбда-функцию с ref
enlosed.
return useMemo(() => debounce((...args) => ref.current(...args))
Каждый раз, когда вызывается useDebounce, вы меняете increment
функцию на новую с текущим count
вложением. useLayoutEffect
обновит ссылку, которая также вложена в функцию запоминания/отмены.
Таким образом, во втором случае у вас есть вложенная цепочка замыканий, которая гарантирует, что функция отмены всегда будет иметь доступ к последней count
версии .
useMemo -> debounce -> (lambda) -> ref -> current -> increment -> count
Вы могли бы упростить код, просто используя хук useCallback вместо того, чтобы создавать свой собственный. Но вы должны передать функцию обновления в setCount, чтобы избежать устаревшего count
значения.
const increment = React.useCallback(
debounce(() => setCount((n) => n 1), delay),
[setCount, delay]
)
Комментарии:
1. говоря «Запоминаемая функция будет создана только при первом вызове крючка». вы имели в виду, что эта функция
() => debounce(ref.current, delay)
создается только один раз? тогда не должна ли правильная версия, т. е.() => debounce((...args) => ref.current(...args), delay)
создаваться только один раз? Я все еще пытаюсь понять, почему во второй форме будут свежиеfn
илиref.current
2. Вы каждый раз передаете новую функцию, но
useMemo
будете запоминать только начальную и возвращать ее, если не измените зависимостьdelay
.3. да, именно так. но это все равно не отвечает на мой вопрос: вот почему
() => debounce((...args) => ref.current(...args), delay)
проблема решена. Итак, вы сказалиuseMemo(() => debounce(ref.current, delay), [delay]);
, что это эквивалентноuseMemo(() => debounce(fn, delay), [delay]);
тому, что каждый разdelay
, когда ничего не меняется,useMemo
крючок будет использовать предыдущую функцию, которая есть() => debounce(fn, delay)
. Ибо() => debounce((...args) => ref.current(...args), delay)
, интересно, это потому, что с помощью lamda мы откладываем оценкуref.current
, чтобы она могла сохранить свежее закрытие?4. Нет. Закрытие не свежее. Но он содержит
ref
синглтон в закрытии. Ониref
всегда будут ссылаться на один и тот же объект в течение всего жизненного цикла компонента. Поэтому, когда вы станетеref.current
новойincrement
функцией с новымcount
вложением , лямбда-функция тоже будет иметь новоеref.current
значение.5. Я должен упомянуть, что мой ответ и ответ мистера Ежа отличаются только формулировками. Мы пытаемся объяснить точно то же самое. Замыкания могут быть очень запутанными, и когда вы добавляете запоминание из крючков react, это добавляет еще один уровень сложности.
Ответ №3:
Это стандартный используемый прыжок
function useDebounce(value, delay) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}