Замыкания в React Hooks

React.js

предисловие

Сегодня в полдень, получив упакованный ланч, я ела филе глубоководной трески в томатном соусе, это было не передать словами. Вдруг товар подбежал и сказал: «А можно сегодня запустить спрос?» Я вдруг был в шоке, думая, что столкнулся с проблемой и не могу найти ее причину, поэтому робко ответил: «Да... Да ...», когда продукт услышал слово «можно», он напевал мелодию и ушел, оставив меня одного, лицом к несвежему филе глубоководной трески... Снова и снова думая о том, как решить проблема.. .

1. Начните с замыканий в JS

JSЗакрытие по существу происходит из двух моментов: лексической области видимости и передачи текущего значения функции.

Формирование замыкания очень простое, после завершения функции функция возврата или функция сохраняется, т.е. замыкание формируется.

о词法作用域Для связанных точек знаний, вы можете обратиться к《你不知道的JavaScript》Выяснить.

React Hooksзакрытия в и мы вJSЗамыкания, наблюдаемые в .

определить фабричную функциюcreateIncrement(i), возвращаетincrementфункция. каждый звонокincrementфункция, значение внутреннего счетчика увеличиваетсяi.

function createIncrement(i) {
    let value = 0
    function increment() {
        value += i
        console.log(value)
    }
    return increment
}
const inc = createIncrement(10)
inc() // 10
inc() // 20

createIncrement(10)Возвращает функцию приращения, которая присваиваетincПеременная. при звонкеinc()час,valueДобавьте 10 к переменной.

первый звонокinc()Возвращает 10, второй вызов возвращает 20 и так далее.

передачаinc()без параметров,JS все еще может получить токvalueа такжеi

ПринципcreateIncrement()середина. Замыкания происходят, когда функция возвращается к функции. Замыкания захватывают переменные в лексической области видимостиvalue а такжеi.

词法作用域是定义闭包的外部作用域. В этом примереincrement()Лексический объем словаcreateIncrement()область , которая содержит переменнуюvalueа такжеi.

звони куда угодноinc(), даже вcreateIncrement()вне его области, он может получить доступvalueа такжеi.

Закрытый мешок - это тот, который вы можете词法作用域Функции, которые запоминают и изменяют переменные, независимо от области выполнения.

2. Закрытие в реактивных крючках

Благодаря повторному использованию и побочным эффектам уменьшено управление состоянием,HooksЗаменяет компоненты на основе классов. Кроме того, мы можем извлекать повторяющуюся логику в пользовательскиеHookдля повторного использования между приложениями.Hooks сильно зависит отJSЗакрытие, но закрытие иногда может быть сложным.

Когда мы используем метод с несколькими побочными эффектами и управлением состояниемReactОдна из проблем, с которой вы можете столкнуться при использовании компонентов, — это устаревшие замыкания, которые бывает трудно исправить.

3. Устаревшие затворы

заводская функцияcreateIncrement(i)вернутьincrementфункция.incrementФункция параvalueУвеличиватьi, и возвращает текущую записьvalueФункция

function createIncrement(i) {
    let value = 0
    function increment() {
        value += i
        console.log(value)
        const message = `Current value is ${value}`
        return function logValue() { // setState相当于logValue函数
            console.log(message)
        }
    }
    return increment
}
const inc = createIncrement(10)
const log = inc() // 10,将当前的value值固定
inc() // 20
inc() // 30

log() // "Current value is 10" 未能正确打印30

Также упомянуть здесьuseRef, почему, когда вы используетеletобъявленuseRefне сталкивается с этой проблемой при использованииletКогда заявленная переменная, она встретится с этой проблемой, потому чтоuseRefНа самом деле это не переменная базового типа, а объект, каждый раз при изменении значенияНа самом деле вы изменяете значение объекта, а на объект ссылаются как на указатель., поэтому независимо от того, где берется значение, можно получить самое последнее значение!

function createIncrement(i) {
    let value = 0
    function increment() {
        value += i
        console.log(value)
        const message = `Current value is ${value}`
        return function logValue() { // setState相当于logValue函数
            console.log(message)
        }
    }
    return increment
}
const inc = createIncrement(1) // i被固定为1,输入几就被固定为几
inc() // 1
const log = inc() // 2
inc() // 3

log() // "Current value is 2" 未能正确打印3

Устаревшее закрытие захватывает переменные с устаревшими значениями.

В-четвертых, исправить проблему устаревших замыканий

(1) Используйте новое закрытие

Первый способ справиться с устаревшими замыканиями — найти замыкание, которое захватывает последнюю переменную.

найти последниеmessageЗакрытие переменной, то есть с последнего вызоваinc()Возвращенное закрытие.

const inc = createIncrement(1)
inc() // 1
inc() // 2
const latestLog = inc()
latestLog() // "Current value is 3"

ВышеупомянутоеReact HookСпособы обработки закрытия свежести.

HooksHookПоследнее закрытие обратного вызова предоставлено (например,useEffect(callback))Последняя переменная была получена из области действия функции компонента. То есть вuseEffectвторой параметр[]Добавьте значение, которое прослушивает изменения, и каждый раз, когда оно изменяется, выполняйтеfunction, чтобы получить последнее закрытие.

Переменная (2) закрыта изменена

Второй способ — позволитьlogValue()Использовать напрямуюvalue.

давайте переместим линиюconst message = ...;прибытьlogValue() В теле функции:

function createIncrementFixed(i) {
  let value = 0;
  function increment() {
    value += i;
    console.log(value);
    return function logValue() {
      const message = `Current value is ${value}`;
      console.log(message);
    };
  }
  
  return increment;
}

const inc = createIncrementFixed(1);
const log = inc(); // 打印 1
inc();             // 打印 2
inc();             // 打印 3
// 正常工作
log();             // 打印 "Current value is 3"

logValue()закрытиеcreateIncrementFixed()в рамкахvalueПеременная.log()Теперь печатается правильное сообщение.

5. Устаревшие замыкания в хуке

useEffect()

В использованииuseEffect HookРаспространенная ситуация, когда происходят замыкания.

в компонентеWatchCountсередина,useEffectотпечатков в секундуcountценность .

function WatchCount() {
    const [count, setCount] = useState(0)
    useEffect(function() {
        setInterval(function log() {
            console.log(`Count is: ${count}`)
        }, 2000)
    }, [])
    
    return (
      <div>
      {count}
      <button onClick={() => setCount(count + 1)}> 加1 </button>
      </div>
    )
}

Нажимаем кнопку плюс 1 несколько раз, видим из консоли, что каждые 2 секунды печатает какCount is: 0

На первом рендереlog()захват среднего закрытияcountзначение переменной0. Позже, даже еслиcountУвеличивать,log()по-прежнему является инициализированным значением, используемым в0.log()

Обходной путь: сообщите useEffect(), что закрытие в log() зависит от количества:

function WatchCount() {
  const [count, setCount] = useState(0);

  useEffect(function() {
    const id = setInterval(function log() {
      console.log(`Count is: ${count}`);
    }, 2000);
    return function() {
      clearInterval(id);
    }
  }, [count]); // 看这里,这行是重点,count变化后重新渲染useEffect

  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1) }>
        Increase
      </button>
    </div>
  );
}

После установки зависимостей один разcountИзменять,useEffect()Просто обновите закрытие.

должным образом управляемыйHookЗависимости — это ключ к решению проблемы устаревших замыканий. Рекомендуемая установкаeslint-plugin-react-hooksЭто может помочь обнаружить забытую нашу зависимость.

useState()

компонентыDelayedCountимеет 2 кнопки

  • Нажмите кнопку "Increase asyncУвеличивать счетчик с задержкой в ​​1 секунду в асинхронном режиме
  • В режиме синхронизации нажмите кнопку "Increase sync” немедленно увеличит счетчик
function DelayedCount() {
  const [count, setCount] = useState(0);

  function handleClickAsync() {
    setTimeout(function delay() {
      setCount(count + 1);
    }, 1000);
  }

  function handleClickSync() {
    
    setCount(count + 1);
  }

  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>Increase async</button>
      <button onClick={handleClickSync}>Increase sync</button>
    </div>
  )
}

Нажмите на "Increase async", а затем нажмите сразу"Increase sync" кнопка,count обновить только до 1.

Это потому чтоdelay()является устаревшим закрытием.

Давайте посмотрим, что происходит в этом процессе:

Первоначальный рендеринг:countзначение0. нажмите 'Increase async' кнопка.delay() захват закрытияcountзначение0.setTimeout()Звонок через 1 секундуdelay(). Нажмите "Increase async" кнопка.handleClickSync()передачаsetCount(0 + 1) Будуcount устанавливается в 1, и компонент повторно визуализируется. Через 1 секундуsetTimeout()воплощать в жизньdelay()функция. ноdelay()Сохранение среднего закрытия countначальное отображаемое значение 0, поэтому вызовsetState(0 + 1),результатcountостаться на 1.

delay()это устаревшее замыкание, которое использует устаревшее замыкание, захваченное во время первоначального рендерингаcount Переменная.

Чтобы решить эту проблему, метод функции может быть использован для обновленияcountусловие:

function DelayedCount() {
  const [count, setCount] = useState(0);

  function handleClickAsync() {
    setTimeout(function delay() {
      setCount(count => count + 1); // 这行是重点
    }, 1000);
  }

  function handleClickSync() {
    setCount(count + 1);
  }

  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>Increase async</button>
      <button onClick={handleClickSync}>Increase sync</button>
    </div>
  );
}

СейчасsetCount(count => count + 1)обновленdelay()середина countусловие.ReactОбязательно укажите последнее значение состояния в качестве аргумента функции обновления состояния, и проблема устаревших закрытий будет решена.

useLayoutEffect()

useLayoutEffectможно рассматривать какuseEffectсинхронная версия.

useLayoutEffectЕго сигнатура функции такая же, какuseEffectто же самое, но это будет работать во всехDOMСинхронный вызов после измененияeffect. можно использовать для чтенияDOMРазметка и запуск повторного рендеринга синхронно. Прежде чем браузер выполнит рисование,useLayoutEffectВнутреннее расписание обновлений будет обновляться синхронно.

Поэтому используйте стандартныйuseEffectчтобы не блокировать визуальные обновления.потому чтоuseLayoutEffectявляется синхронным, если бы мыuseLayoutEffectВызов обновлений состояния или выполнение некоторых очень трудоемких вычислений может привести кReactВремя работы слишком велико, что блокирует рендеринг браузера, что приводит к некоторым проблемам с заиканием..

Если вы перемещаете код изclass Компоненты перенесены для использованияHook функциональные компоненты, вы должны обратить внимание наuseLayoutEffectа такжеcomponentDidMount,componentDidUpdateЭтап вызова тот же. Тем не менее, мы рекомендуем начать сuseEffectи попробуйте только использовать его, если это не удаетсяuseLayoutEffect.

Если вы используете рендеринг на стороне сервера, имейте в виду, что независимо отuseLayoutEffectещеuseEffectне может быть вJavascriptВыполняется до завершения загрузки кода. Вот почему он был представлен в компонентах, отображаемых на сервере.useLayoutEffect сработает, когда код ReactТревога. Чтобы решить эту проблему, логика кода должна быть перемещенаuseEffectin (если эта логика не нужна для первого рендеринга), или задержать отображение компонента до завершения рендеринга на стороне клиента (еслиuseLayoutEffectперед казньюHTMLотображаются в беспорядке).

Оказать услугу с концаHTML Демонстрацияeffectкомпонентов можно получить, используяshowChild && <Child /> сделать условный рендеринг и использоватьuseEffect(() => { setShowChild(true); }, []) Задержка представления компонентов. Таким образом, до завершения клиентского рендерингаUI Он не будет выглядеть таким запутанным, как раньше.

Суммировать

Замыкание — это функция, которая захватывает переменную из того места, где она определена (или из ее лексической области видимости).

когда замыкание захватывает过时的变量, возникает проблема устаревших замыканий.

Эффективный способ решения замыканий

  1. установить правильно React Hookзависимости
  2. Для устаревших состояний используйте функцию для обновления состояния