Смена обработчиков в реагирующих хуках - PullRequest
1 голос
/ 10 ноября 2019

Какое-то время хотелось бы начать использовать компоненты реагирующей функции с реагирующими хуками вместо расширяющего класса реагирования компонента , но есть одна вещь, которая меня обескураживает. Вот пример из самого первого вступления реагирующих хуков:

import React, { useState } from 'react'
import Row from './Row'

export default function Greeting(props) {
  const [name, setName] = useState('Mary');

  function handleNameChange(e) {
    setName(e.target.value);
  }

  return (
    <section>
      <Row label="Name">
        <input
          value={name}
          onChange={handleNameChange}
        />
      </Row>
    </section>
  )
}

В качестве обработчика изменений для ввода используется объявление handleNameChange. Давайте представим, что Greeting компонент действительно очень часто обновляется по какой-то причине. Изменяет ли ручка обработки каждый раз при каждом рендере? С точки зрения JavaScript, насколько это плохо?

Ответы [ 2 ]

3 голосов
/ 10 ноября 2019

Инициализируется ли ручка изменения каждый раз при каждом рендере?

Да. Это одна из причин для useCallback хука.

С точки зрения JavaScript, насколько это плохо?

По сути, это просто созданиеновый объектОбъект функции и базовый код функции - это не одно и то же. Базовый код функции анализируется только один раз, обычно в байт-код или в простую быструю версию компиляции. Если функция используется достаточно часто, она будет агрессивно скомпилирована.

Поэтому создание нового объекта функции каждый раз создает некоторый отток памяти, но в современном программировании JavaScript мы создаем и выпускаем объекты все время поэтому движки JavaScript в высшей степени оптимизированы для обработки, когда мы это делаем.

Но использование useCallback позволяет избежать ненужного повторного его создания (ну, вроде как, продолжайте читать) , только обновляятот, который мы используем, когда его зависимости меняются. Зависимости, которые вам нужно перечислить (в массиве, который является вторым аргументом useCallback), - это то, что handleNameChange закрывает и может измениться. В этом случае handleNameChange не закрывает все, что изменяется. Единственное, что закрывается, это setName, гарантия React не изменится (см. «Примечание» на useState). Он использует значение из входных данных, но получает входные данные через аргументы, но не закрывает их. Таким образом, для handleNameChange вы можете оставить зависимости пустыми, передав пустой массив в качестве второго аргумента useCallback. (На каком-то этапе может быть что-то, что автоматически обнаруживает эти зависимости; сейчас вы объявляете их.)

Острые глаза заметят, что даже с useCallback вы все еще создание новой функции каждый раз (той, которую вы передаете в качестве первого аргумента useCallback). Но useCallback вернет предыдущую версию, вместо этого, если зависимости предыдущей версии совпадают с зависимостями новой версии (что они всегда будут в случае handleNameChange, потому что их нет). Это означает, что функция, которую вы передаете в качестве первого аргумента, сразу же доступна для сборки мусора. Механизмы JavaScript , в особенности , эффективны при сборке мусора объектов (включая функции), которые создаются во время вызова функции (вызов Greeting), но нигде не упоминаются при возврате этого вызова, что является частью того, почемуuseCallback имеет смысл. (Вопреки распространенному мнению, объекты могут и создаются в стеке, когда это возможно, современными движками.) Кроме того, повторное использование этой же функции в подпорках на input может позволить React более эффективно рендерить дерево (минимизируя различия).

Версия этого кода useCallback:

import React, { useState, useCallback } from 'react' // ***
import Row from './Row'

export default function Greeting(props) {
  const [name, setName] = useState('Mary');

  const handleNameChange = useCallback(e => {        // ***
    setName(e.target.value)                          // ***
  }, [])                                             // *** empty dependencies array

  return (
    <section>
      <Row label="Name">
        <input
          value={name}
          onChange={handleNameChange}
        />
      </Row>
    </section>
  )
}

Вот аналогичный пример, но он также включает второй обратный вызов (incrementTicks), который делает используйте что-то, над чем оно закрывается (ticks). Обратите внимание, когда handleNameChange и incrementTicks фактически изменяются (что отмечено кодом):

const { useState, useCallback } = React;

let lastNameChange = null;
let lastIncrementTicks = null;

function Greeting(props) {
    const [name, setName]   = useState(props.name  || "");
    const [ticks, setTicks] = useState(props.ticks || 0);
  
    const handleNameChange = useCallback(e => {
        setName(e.target.value)
    }, []); // <=== No dependencies
    if (lastNameChange !== handleNameChange) {
        console.log(`handleNameChange ${lastNameChange === null ? "" : "re"}created`);
        lastNameChange = handleNameChange;
    }
    const incrementTicks = useCallback(e => {
        setTicks(ticks + 1);
    }, [ticks]); // <=== Note the dependency on `ticks`
    if (lastIncrementTicks !== incrementTicks) {
        console.log(`incrementTicks ${lastIncrementTicks === null ? "" : "re"}created`);
        lastIncrementTicks = incrementTicks;
    }
  
    return (
      <div>
          <div>
              <label>
                  Name: <input value={name} onChange={handleNameChange} />
              </label>
          </div>
          <div>
              <label>
                  Ticks: {ticks} <button onClick={incrementTicks}>+</button>
              </label>
          </div>
      </div>
    )
}

ReactDOM.render(
    <Greeting name="Mary Somerville" ticks={1} />,
    document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>

Когда вы запустите это, вы увидите, что handleNameChange и incrementTicks были созданы оба. Теперь измените имя. Обратите внимание, что ничего не воссоздано (ну, ладно, новые не используются и сразу GC'able). Теперь нажмите кнопку [+] рядом с галочками. Обратите внимание, что incrementTicks воссоздан (так как ticks, который он закрывает, устарел, поэтому useCallback вернул созданную нами новую функцию), но handleNameChange остается тем же.

2 голосов
/ 10 ноября 2019

Строго говоря, с точки зрения JavaScript (игнорируя React), определение функции внутри цикла (или внутри другой регулярно вызываемой функции) вряд ли станет узким местом производительности.

Взгляните на эти jsperf case . Когда я запускаю этот тест, случай объявления функции работает со скоростью 797 792 833 операций в секунду. Это также не обязательно лучший метод, но часто он становится жертвой преждевременной оптимизации со стороны программистов, которые полагают, что определение функции должно быть медленным.

Теперь, с точки зрения React. Это может стать проблемой для производительности, когда вы передаете эту функцию дочерним компонентам, которые заканчивают повторную визуализацию, потому что технически это новая функция каждый раз. В этом случае становится разумным достигнуть useCallback, чтобы сохранить идентичность функции при нескольких рендерах.

Стоит также отметить, что даже с useCallback hook, выражение функции по-прежнему повторно объявляется при каждом рендеринге, просто его значение игнорируется, если не изменяется массив зависимостей.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...