В React нет магии

Основы JavaScript, которые должен знать каждый разработчик React, чтобы раскрыть секреты!

Название может показаться очевидным, но с тех пор, как я начал преподавать кодирование, я вижу разработчиков, которые, когда они не понимают, что происходит, думают, что происходит какое-то волшебство.

Цель этих строк — понять, что происходит внутри компонентов каждый раз, когда мы обновляем состояние, сделав шаг за обзором некоторых основных концепций «Vanilla JavaScript» и увидев, как это применимо к потоку React. .

Повестка дня

  • Резюме того, что такое Scope и Closures в JavaScript.
  • Как эти две концепции влияют на наши потоки в React (с useState).
  • Разница между обновлением состояния с помощью значения и с помощью функции обновления.
  • Резюме ссылок на объекты в JavaScript.
  • Как работать со ссылками в React.

Вы помните, что такое Scope?

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

Приведенный выше пример работает следующим образом: механизм выполнения JS будет проходить через наш код, начиная с «глобальной области видимости», где у нас есть только одно объявление — count. Затем, когда мы вызываем count в первый раз, движок JS инициализирует новую область видимости, которую мы можем назвать «областью count». Для JavaScript это совершенно новое измерение. Здесь у нас есть различные переменные и объявления, доступные только изнутри области видимости, а не снаружи. Как только мы закончим и функция вернется, эта среда будет уничтожена. Здесь мы объявляем n равным 0 и суммируем с ним 1 и, наконец, регистрируем его значение.

Что произойдет, если мы снова вызовем count? Очевидно, будет снова напечатано 1, потому что новые вызовы всегда будут устанавливать новую область действия и снова объявлять let n = 0.

Чтобы настаивать на этой концепции области, если мы попытаемся записать n в последней строке, мы получим undefined, потому что n существует только в «области действия» и мы не можем получить к ней доступ.

Итак, как нам получить переменную, которая обновляется каждый раз, когда мы вызываем функцию?

Следуя концепции «каждая область может получить доступ к своим внешним областям», мы можем сместить nодной области вверх, чтобы при вызове count мы всегда ссылались на одно и то же объявление и постоянно обновляли его значение, поэтому в последней строке мы напечатаем 2.

Но очень неудобно загрязнять глобальную область видимости нескольких переменных, не так ли?

А как же замыкания?

Замыкания прекрасно решат проблему. Здесь вы можете найти определение того, что такое замыкание:

«Замыкание — это когда функция может запомнить и получить доступ к своей лексической области видимости, даже если эта функция выполняется за пределами своей лексической области».

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

Где обновляется состояние?

Двигаясь вперед и переходя к React, но не забывайте о том, что мы говорили ранее: объявление существует только в своей области действия.
Почему мы удивлены, увидев, что в этом коде по-прежнему регистрируется 0?

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

Теперь я хочу задать вопрос: В чем основное отличие от крючков?

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

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

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

Состояния обновляются со значением или средством обновления

Теперь вы можете сказать мне, что будет показано на экране с этим кодом?

Каковы ваши предположения? (Впереди спойлеры…)

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

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

Итак, позвольте мне помочь вам бросить это. Во-первых, он вызывает useState и инициализирует состояние 0, которое возвращается в виде кортежа с функцией для его обновления. Теперь в «области использования состояния» и в «области действия счетчика» и _state, и count равны 0.
Затем мы вызываем useEffect один раз из-за пустого массива зависимостей. Это установит интервал, через который каждую секунду будет вызываться setCount, передавая count. Вот важная часть. Задав интервал, мы объявляем новую анонимную функцию в первой «области счетчика» в качестве обратного вызова. Каждый раз, когда мы будем вызывать этот обратный вызов, он будет искать значение count внутри той же области видимости, где count всегда будет 0. При вызове setCount мы фактически делаем следующее:

setCount(0 + 1) // => 1

Вот почему мы всегда будем видеть 1 на экране, даже если интервал запущен.

Как мы можем решить эту проблему? Мы должны иметь возможность получить доступ к самому обновленному значению count, которое содержится в области действия useState, чтобы не полагаться на значение в «области действия счетчика». К счастью, хук useState дает нам возможность передать функцию обновления, которая получает в качестве аргумента последнее вычисленное значение.

setCount((count) => count + 1)

Это дает нам гарантию, что мы суммируем 1 с последним значением, и наш счетчик теперь будет работать правильно.

Примитивы и ссылки

Еще одним большим источником путаницы среди разработчиков JS является концепция примитивы и объекты и все вытекающие отсюда последствия.
В React мы должны хорошо понимать эту тему, чтобы быть уверенными, что не наткнемся на досадные ошибки, такие как бесконечные циклы. Думаю, все догадываются, к чему я клоню.

Просто резюмировать. Несмотря на то, что JS не является строго типизированным, в нем есть различные элементы, которые позволяют нам выполнять определенные операции, и они определяются своими типами.

У нас есть примитивы, которые представляют собой все, кроме объектов, массивов и функций. Последние три подпадают под название "объекты". У объектов есть особенность: когда мы их инициализируем, мы выделяем место в памяти для сохранения значения и сохраняем ссылку на это пространство в текущей области. Мы можем передавать ссылку, но мы всегда ссылаемся на один и тот же объект в памяти.

С учетом сказанного, когда мы проводим сравнение между объектами, мы сравниваем их ссылки, а это означает, что они представляют собой точно такое же пространство в памяти, а не то, что они имеют одинаковые свойства и значения.

Вот пример некоторых сравнений:

Ссылки в React

Говоря о React, на самом деле, когда у нас есть, например, пользовательский хук, который возвращает функцию или объект, нам нужно быть осторожными и убедиться, что мы используем их правильно, чтобы избежать бесполезных рендеров или даже бесконечных циклов.

Взгляните на следующий пример. В компоненте SessionTime мы используем хук useCounter, чтобы получить время, прошедшее с начала сеанса, и функцию для его сброса. Каждую секунду мы вызываем useCounter и повторно объявляем resrt и time каждый раз с новыми ссылками. Это привело бы к бесконечному циклу внутри SessionTime, поскольку useEffect имеет resert в массиве зависимостей.

Важно:Массив зависимостей — это список элементов, которые React Hooks используют, чтобы понять, когда их нужно запустить снова. Внутренне они делают сравнение с проглатыванием deps[i] === deps[i], и если оно ложно, оно запускает крючок.
Если мы вернемся к приведенному выше коду сравнения, мы увидим, что с примитивами все было бы в порядке, но с объектами нам нужно быть осторожными, потому что недостаточно, чтобы они выглядели одинаково, они должны быть то же самое.

Решение здесь состоит в том, чтобы использовать мемоизацию, поэтому, если нам не нужно их обновлять, мы всегда будем сохранять одну и ту же ссылку. В этом случае мы можем обернуть reset в useCallback и решить проблему.

Заметили, что time все еще создает новую ссылку? Что ж, когда мы используем мемоизацию, нам нужно рассматривать каждый случай отдельно. Здесь time используется для count, которое обновляется каждую секунду, таким образом, обертывание его в useMemo не изменит реализацию, а просто займет больше места в памяти, не получая ничего взамен. Скажем, в данном случае это ожидаемо.

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

Выводы

Основная цель этой статьи — понять, что все, что происходит внутри наших компонентов и хуков, — это просто JavaScript, и как только мы это поймем, волшебства больше не будет.

Резюмируем:

  • Область действия — это набор сред, в которых мы сохраняем переменные и объявления, к которым может быть получен доступ только из той же области или только из внутренних областей.
  • Закрытие — это функции, которые делают доступными эти области видимости и позволяют нам сохранять значения в памяти.
  • В React каждый рендеринг — это вызов новой функции, и каждый вызов функции устанавливает новую область видимости с новыми переменными и объявлениями.
  • Когда мы обновили состояние с помощью setState, мы фактически обновляем значение внутри хука useState, а не внутри компонента. Компонент получит новое значение обновления при следующем вызове.
  • Когда мы вызываем setState, если мы хотим использовать самое последнее значение, мы можем использовать средство обновления setState(last => last + new)
  • Когда мы сравниваем объекты, функции и массивы, мы сравниваем их ссылку (адрес в памяти, где сохранен элемент), а не то, имеют ли они одинаковые свойства и значения.
  • Когда мы передаем зависимости хукам, нам нужно убедиться, что мы не передаем новую ссылку каждый раз, запоминая их объявления.

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord . Заинтересованы в хакинге роста? Ознакомьтесь с разделом Схема.