Задний план
Неизменный — модное слово, которое я часто слышу, но до недавнего времени никогда не понимал его важности. Лично мне трудно увидеть важность чего-либо, пока я не пойму его практическое применение. Неизменяемость была темой, обсуждаемой в конце последней встречи ReactNYC, на которой я присутствовал, что позволяет рассматривать этот термин в перспективе.
Презентация Нира Кауфмана об неизменности в первую очередь была посвящена оптимизации. Несмотря на то, что я не мог полностью понять каждый пример, я впоследствии провел собственное исследование. Медленно, но верно я продолжал копаться в этой концепции, пытаясь разобраться в нюансах каждого предоставленного Ниром примера, такого как совместное использование структуры, копирование при записи и Immer.js. Хотя я никоим образом не являюсь экспертом в этой теме, я изучил множество новых методов оптимизации, которые могут помочь любому будущему программисту.
Примечание. В этом разделе об неизменности основное внимание будет уделено объектам Javascript.
Неизменный
Прежде чем мы углубимся в методы, давайте сначала обсудим, что это значит, когда объект неизменен. Согласно определению Википедии:
В объектно-ориентированном и функциональном программировании неизменный объект (неизменяемый [1]) объект — это объект, состояние которого нельзя изменить после его создания.
Мы можем пойти еще шире и посмотреть на определение неизменяемости Merriam-Webster:
:не может или не подвержен изменению
Для аналогии можно думать о переменной const
, присвоенной примитивному типу данных, как о неизменяемой. По определению переменной const
ей нельзя повторно присвоить новое значение после инициализации. Поскольку значение нельзя переназначить, переменная const
может рассматриваться как неизменяемая.
Примечание. Все примеры и методы, показанные ниже, больше соответствуют определению неизменяемости Merriam-Webster, чем определению Википедии, поскольку я в первую очередь сосредоточился на аспекте «нельзя изменить».
Из моего исследования и определения из Википедии кажется, что неизменность обычно относится к объектам.
По умолчанию объекты Javascript изменяемы. Это означает, что возможно назначение новой пары "ключ-значение" или изменение существующей пары "ключ-значение". В качестве быстрого примера:
Строка 1 создала экземпляр объекта, строка 3 изменила существующую пару ключ-значение, а строка 5 назначила новую пару ключ-значение.
Однако что, если я хочу сделать объект неизменным после изменения и добавления этих пар ключ-значение? Вы можете с Object.freeze()
! Согласно его документации:
Метод
Object.freeze()
замораживает объект. Замороженный объект больше нельзя изменить; замораживание объекта предотвращает добавление к нему новых свойств, удаление существующих свойств, предотвращает изменение перечислимости, конфигурируемости или возможности записи существующих свойств, а также предотвращает изменение значений существующих свойств. Кроме того, замораживание объекта также предотвращает изменение его прототипа.freeze()
возвращает тот же объект, который был передан.
Другими словами, никто не может каким-либо образом изменить объект, форму или форму… по большей части. Давайте проверим эту функцию из нашего предыдущего примера.
В строке 8 новая переменная с именем immutableObject
была создана с помощью замороженной переменной mutableObject
. Строки 9 и 10 изменились и добавили пары ключ-значение в immutableObject
, но строка 12 показывает, что внутри объекта ничего не изменилось.
Ура! Похоже, мы сделали объект неизменяемым на данном этапе. Еще один забавный факт с Object.freeze()
заключается в том, что если вы сохраняете замороженный объект в новой переменной, она фактически указывает на то же место в памяти, что и объект, который вы хотели заморозить. Строка 13 показывает это.
Возвращаясь к по большей части,Object.freeze()
не работает для вложенных объектов. Это означает, что Object.freeze()
выполняет неглубокую заморозку объекта, через который вы проходите. Суть ниже демонстрирует это:
Как вы можете видеть из вышеизложенного, в строке 5 я изменил значение во вложенном объекте, и оно отобразилось в строке 6. Если вы хотите заморозить вложенный объект, вам нужно будет заморозить каждый слой объекта, значение которого равно тип объекта.
Иммер.js
Поскольку Javascript изначально не обладает неизменностью, для этого были разработаны библиотеки. Две популярные библиотеки, Immer.js и Immutable.js, кажутся наиболее популярными. В этом случае я бы погрузился в Immer.js.
Прелесть, но также и запутанная часть Immer.js заключается в том, что он использует структурный обмен. Структурный обмен аналогичен постоянной структуре данных, в которой Википедия определяет его как:
В вычислениях постоянная структура данных — это структура данных, которая всегда сохраняет предыдущую версию самой себя при ее изменении. Такие структуры данных являются фактически неизменяемыми, поскольку их операции не обновляют (видимо) структуру на месте, а вместо этого всегда дают новую обновленную структуру.
В свете изучения еще одной новой темы, мне пришлось рыться в бездне Google, чтобы углубить свои знания. Я наткнулся на два блога, в которых подробно рассказывается о совместном использовании структур и Immer.js/.
По сути, совместное использование структуры сохраняет исходную структуру прежней, и единственная ее часть, которая изменяется, — это создание новой части в исходной структуре, которая указывает на новое значение. В основном сохраняя исходную структуру, он фактически делает объект/массив неизменяемым.
Давайте сделаем небольшой шаг назад и посмотрим на редукторы в вашем магазине Redux. Обычно, если в вашем редукторе были какие-либо изменения состояния, вы должны использовать оператор распространения, чтобы сделать копию исходного состояния, применить все необходимые изменения, а затем обновить состояние новыми изменениями. Используя оператор распространения, вы копируете и создаете новые данные, строка за строкой, в вашем объекте/массиве, хотя данные точно такие же, как и раньше. Если это масштабируется для 100 000 элементов, вы будете дублировать 100 000 элементов (за исключением тех, где его необходимо обновить), и, следовательно, потреблять тонну памяти и замедлять работу вашего приложения.
С Immer.js и его функцией produce()
структурное разделение позволяет избежать этого. Ради аргумента, в объекте, чтобы получить доступ к значению на основе его ключа, компьютер следует «маршруту». Каждый возможный «маршрут» составляет исходную структуру объекта.
Примечание. Термин "маршрут" не является фактическим термином, используемым в этом разделе, но я создал его для простоты объяснения.
Теперь предположим, что вам нужно обновить элемент в вашем состоянии. С помощью Immer.js он может определить конкретный «маршрут», по которому элемент необходимо обновить, исохранить «маршруты» всех необновленных элементов посредством структурного совместного использования. Это означает, что новый «маршрут» создается для отражения нового значения, тогда как все старые «маршруты» повторно используются/одинаковы, поскольку их текущее значение не изменяется. Если бы вы использовали оператор распространения в этой аналогии, вы бы воссоздали все те же «маршруты», хотя все они указывают на одно и то же предыдущее значение.
Если вы хотите прочитать о структуре данных и понять правильное техническое объяснение, пожалуйста, прочитайте о попытках.
Ключевые вынос
Хотя я не занимался копированием при записи, пожалуйста, не стесняйтесь исследовать эту тему самостоятельно. Главный вывод из всех этих исследований заключается в том, что код всегда можно каким-то образом оптимизировать. Хотя этот блог имел в виду оптимизацию по скорости, вы также можете оптимизировать по памяти, например, рефакторинг вашего кода. Существуют также небольшие способы оптимизации, такие как запоминание (например, React.memo()
). Каждая маленькая часть оптимизации будет иметь большое значение.