[Разработка веб-сайтов, JavaScript, Проектирование и рефакторинг, ООП, ReactJS] Техники повторного использования кода

Автор Сообщение
news_bot ®

Стаж: 6 лет 7 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
08-Мар-2021 18:30

В этой статье я опишу различные техники повторного использования кода и разбиения сложных объектов на части, с которыми я столкнулся. Постараюсь объяснить, почему классическое наследование, а также некоторые другие популярные подходы не работают в сложных случаях, и какие есть альтернативы.Возможно многих удивит, что в основе большинства подходов повторного использования кода и создания составных объектов лежат стандартные структуры данных – массив, список, словарь, дерево, граф.Т.к. в последние годы я пишу на JavaScript и React, то они будут использоваться в некоторых примерах. Да и в целом, периодически я буду упоминать React и другие веб-технологии. Тем не менее, думаю, что значительная часть статьи должна быть понятна и полезна разработчикам из других стеков. Для некоторых подходов я добавил схемы, чтобы показать, как организованы составляющие сложных объектов. Будет часто упоминаться агрегация(агрегирование/делегирование/включение) и композиция. Описывается то, как я это понял, поэтому где-то информация может быть ошибочной.Чтобы разделить логику одного сложного объекта на составные части, существуют несколько механизмов:
  • Разделение функционала на классы/объекты и смешивание их полей, методов в одном объекте.
  • Вынесение части функционала в обертки и помещение в них основного объекта, либо вкладывание объектов один в другой с организацией списка вложенных объектов.
  • Вынесение части функционала в отдельные объекты/функции и помещение их в основной объект.
  • Разделение функционала объекта на независимые части и использование какого-то внешнего механизма для организации нужного поведения с использованием этих частей.
В статье же я разделил техники/паттерны в зависимости от получаемой структуры данных, используемой для хранения составляющих сложного объекта.1) Объединение (смешивание) функционала нескольких объектов в одном
Смешивание и примеси (миксины)
Классическое наследование
Множественное наследование и интерфейсы2) Композиция/агрегация с использованием списка
Прототипное наследование
Паттерн декоратор и аналоги3) Композиция/агрегация с использованием одноуровневых структур данных (ссылка, массив ссылок, словарь)
Паттерн стратегия
Entity Component (EC) 4) Композиция/агрегация с вынесением логики вне объекта и его составляющих
Entity Component System (ECS)5) Композиция/агрегация с использованием графов
Паттерн State machine6) Композиция/агрегация с использованием деревьев
Паттерн composite и другие древовидные структуры
Behaviour tree7) Смешанные подходы
React hooksОбъединение (смешивание) функционала нескольких объектов в одном.Смешивание и примеси (миксины)Самый простой, но ненадежный способ повторного использования кода – объединить один объект с другим(и). Подходит лишь для простых случаев, т.к. высока вероятность ошибки из-за замещения одних полей другими с такими же именами. К тому же, так объект разрастается и может превратиться в антипаттерн God Object.Существует паттерн примесь (mixin/миксина), в основе которого лежит смешивание.
Примесь – это объект, поля и методы которого смешиваются с полями и методами других объектов, расширяя функциональность объекта, но который не используется сам по себе.
Можно добавить несколько миксин к одному объекту/классу. Тогда это схоже с множественным наследованием.Классическое наследованиеЗдесь описывается классическое наследование, а не то, как наследование классов устроено в JS.Подразумеваю, что читатель уже знаком с понятиями "наследование" и "множественное наследование". В отличие от простого смешивания, в классическом наследовании есть ряд строгих правил и механизмов, которые позволяет избежать множество проблем. В основе классического наследования лежит все то же смешивание - члены нескольких объектов объединяются в один объект.
При наследовании происходит копирование членов родительского класса в класс-наследник. При создании экземпляра класса тоже происходит копирования членов класса. Я не исследовал детали этих механизмов, к тому же они явно отличаются в различных языках. Подробнее с этой темой можно ознакомиться в 4-ой главе книги "Вы не знаете JS: this и Прототипы Объектов".Когда можно использовать наследование, а когда не стоит?
Наследования не стоит использовать в качестве основной техники для повторного использования кода для сложных объектов. Его можно использовать совместно с композицией для наследования отдельных частей сложного объекта, но не для самого сложного объекта. Например, для React компонентов наследование плохо, а для частей (вроде объектных аналогов custom hooks) из которых мог быть состоять компонент-класс, наследования вполне можно использовать. Но даже так, в первую очередь стоит рассматривать разбиение на большее число составляющих или применения других техник, вместо наследования.При возможности появления сложной иерархии наследование (более 2-х уровней, где первый уровень иерархии – родитель, а второй уровень - наследники) тоже не следует использовать наследование. Множественное наследование и интерфейсыПри использовании множественного наследования получаются довольно запутанные иерархии классов. Поэтому во многих языках от отказались от множественного наследования реализации. Но множественное наследование по-прежнему применяют при наследовании абстракций в виде интерфейсов.Интерфейсы есть, например, в Typescript. Реализация нескольких интерфейсов в одном классе отчасти похоже на наследование, но с их использованием «наследуется» только описание свойств и сигнатура методов интерфейса. Наследование реализации не происходит. Интерфейсы следует понимать не как наследование, а как контракт, указывающий, что данный класс реализует такой-то интерфейс. Плохо, когда один класс реализует слишком много интерфейсов. Это означает, что-либо интерфейсы слишком сильно разбиты на части, либо у объекта слишком большая ответственность.
Композиция/агрегация с использованием спискаПрототипное наследованиеПри прототипном наследовании уже не происходит смешивания родительского объекта и его наследника. Вместо этого наследник ссылается на родительский объект (прототип).При отсутствии свойства (поле, метод и т.д.) в объекте, происходит поиск этого свойства в цепочке прототипов. То есть часть функционала делегируется вложенному объекту, который тоже может делегировать функционал вложенному объекту внутри себя. И так далее по цепочке. Прототип на любом уровне цепочки может быть только один.Стоит отметить, что в JavaScript операции записи/удаления работают непосредственно с объектом. Они не используют прототип (если это обычное свойство, а не сеттер). Если в объекте нет свойства для записи, то создается новое. Подробнее об этом.Цепочка прототипов организована как стек (Last-In-First-Out или LIFO). Какой объект добавлен в цепочку последним (если считать от итогового объекта-контейнера), к тому обращение будет первым.Также существует вариант, когда при создании нового объекта с прототипом, создается копия прототипа. В таком случае используется больше памяти (хотя это проблема разрешаема), но зато это позволяет избежать ошибок в других объектах из-за изменения прототипа конкретного объекта.
Паттерн Декоратор и аналогиДекоратор(wrapper/обертка) позволяет динамически добавлять объекту новую функциональность, помещая его в объект-обертку. Обычно объект оборачивается одним декоратором, но иногда используется несколько декораторов и получается своего рода цепочка декораторов.Цепочка декораторов устроена как стек (LIFO). Какой объект добавлен в цепочку последним (если считать от итогового объекта-контейнера), к тому обращение будет первым.Цепочка декораторов похожа на цепочку прототипов, но с другими правилами работы с цепочкой. Оборачиваемый объект и декоратор должны иметь общий интерфейс.
На схеме ниже пример использования нескольких декораторов на одном объекте:
Как в случае с прототипами, зачастую можно подменять декораторы во время выполнения. Декоратор оборачивает только один объект. Если оборачивается несколько объектов, то это уже что-то другое.HOF (higher order function) и HOC (Higher-Order Component) - паттерны с похожей идей. Они оборачивают функцию/компонент другой функцией/компонентом для расширения функционала.HOF - функция, принимающая в качестве аргументов другие функции или возвращающая другую функцию в качестве результата. Примером HOF в JS является функция bind, которая, не меняя переданную функцию, возвращает новую функцию с привязанным к ней с помощью замыкания значением. Другим примером HOF является карринг.HOC - чистая функция, которая возвращает другой компонент (а он уже содержит в себе произвольную логику), который внутри себя "рендерит" переданный компонент. При этом сам переданный компонент не меняется, но в него могут быть переданы props.Также стоит упомянуть композицию функций. Это тоже своего рода обертка. С помощью этой техники создаются цепочки вложенных функций:
const funcA = сompose(funcB, funcC, funcD);
или же менее читабельный вариант:
const funcA = ()=> {
  funcB( funcC( funcD() ) ) ;
};
То же самое можно получить такой записью:
function funcA() {
  function funcB() {
      function funcC() {
         function funcD()
      }
  }
}
Недостатком последнего варианта является жесткая структура функций. Нельзя поменять их очередность или заменить одну из функций без создания новой аналогичной цепочки или ее части. funcC нельзя использовать без funcD, а funcB без funcC и без funcD. В первых же двух примерах – можно. Там функции независимы друг от друга.ИтогоПрототипное наследование и использование декораторов гибче, чем подходы со смешиванием.Часто говорят: «предпочитайте композицию наследованию». Стоит учесть, что существует множество вариантов композиции с различной эффективностью в той или иной ситуации. Только от простой замены наследования на композицию, вряд ли получиться решить проблемы без появления новых проблем. Нужно еще выбрать подходящую замену.Когда по аналогии с иерархией наследования используется несколько уровней вложения одних объектов в другие, получается иерархия вложенных объектов. Почти то же самое, что и наследование, только с возможностью подменять объекты в иерархии. Конечно, в случае декораторов этого обычно избегают и вместо иерархии получается цепочка. В цепочке декораторов выходит так, что каждый следующий используемый декоратор помимо своих членов классов должен реализовать члены всех остальных объектов в цепочке. В итоге, по аналогии с наследованием, снова получается раздутый объект с множеством полей и методов. На схеме выше был пример такого объекта - DecoratorC.Зачастую при использовании нескольких декораторов на одном объекте не добавляют новые поля и методы, а лишь подменяют реализацию уже существующих членов объекта. Остается другой недостаток – из-за большой вложенности довольно сложно разобраться, что же делает итоговый составной объект, т.к. для этого надо пройтись по цепочке вложенных объектов.Как видите, по-прежнему остаются довольно серьезные проблемы. Но, есть другие решения, о которых рассказано в следующих главах. Композиция/агрегация с использованием одноуровневых структур данных (ссылка, массив ссылок, словарь)Под одноуровневой структурой данных я подразумеваю структуру, элементы которой не ссылаются на другие элементы. Паттерн стратегия Паттерны декоратор и стратегия служат для одной цели – с помощью делегирования расширить функциональность объекта. Но делают они это по разному. Хорошо описана эта разница по ссылке: «Стратегия меняет поведение объекта «изнутри», а Декоратор изменяет его «снаружи».»Паттерн Cтратегия описывает разные способы произвести одно и то же действие, позволяя динамически заменять эти способы в основном объекте (контексте). На схеме ниже пара примеров связи стратегий с основным объектом.
К похожим способам (использование ссылки) расширения функционала объекта и повторного использования кода можно отнести события в HTML элементах и директивы в Angular и Vue.
<button onclick="customAction()" /> // html
<input v-focus v-my-directive="someValue" /> // vue
Entity Component (EC)Я не знаю, как называется данный паттерн. В книге Game Programming Patterns он называется просто "Компонент", а по ссылке его называют системой компонентов/сущностей. В статье же я буду называть его Entity Component (EС), чтобы не путать с подходом, который будет описан в следующей главе.
Сначала пройдемся по определением:
  • Entity (сущность) – объект-контейнер, состоящий из компонентов c данными и логикой. В React и Vue аналогом Entity является компонент. В Entity не пишут пользовательскую логику. Для пользовательской логики используются компоненты. Компоненты могут храниться в динамическом массиве или словаре.
  • Component  – объект со своими данными и логикой, который можно добавлять в любую Entity. В React компонентах похожим аналогом являются custom hooks. И описываемые здесь компоненты и пользовательские хуки в React служат для одной цели – расширять функционал объекта, частью которого они являются.
Обычно Entity может содержать вложенные entities, тем самым образуя дерево entities. Не думаю, что это является неотъемлемой его частью, а скорее является смешиванием нескольких подходов.Данный паттерн похож на паттерн стратегия. Если в объекте использовать динамический массив со стратегиями, организовать их добавление, удаление и получение определенной стратегии, то это будет похоже на Entity Component. Есть еще одно серьезное отличие - контейнер не реализует интерфейс компонентов или методы для обращения к методам компонентов. Контейнер только предоставляет доступ к компонентам и хранит их. Получается составной объект, который довольно своеобразно делегирует весь свой функционал вложенным объектом, на которые он ссылается. Тем самым EC избавляет от необходимости использования сложных иерархий объектов.Плюсы  EC
  • Низкий порог вхождения, т.к. в основе используется простая одноуровневая структура данных.
  • легко добавлять новую функциональность и использовать код повторно.
  • можно изменять составной объект (Entity) в процессе выполнения, добавляя или удаляя его составляющие (компоненты)
Минусы
  • для простых проектов является ненужным усложнением из-за разбиение объекта на контейнер и компоненты
В одной из своих следующих статей я опишу применение этого подхода для React компонентов. Тем самым я покажу, как избавиться от первых двух недостатков компонентов на классах, описанных в документации React-а:
https://ru.reactjs.org/docs/hooks-intro.html#its-hard-to-reuse-stateful-logic-between-components
https://ru.reactjs.org/docs/hooks-intro.html#complex-components-become-hard-to-understandЭтот подход используется с самого начала выхода движка Unity3D для расширения функционала элементов (объектов) дерева сцены, включая UI элементы, где вы можете получше ознакомится с данным подходом. Но в таком случае придётся потратить не мало времени на изучение движка.ИтогоПаттерн стратегия сам по себе не очень мощный, но если развить его идею, можно получить довольно эффективные техники. К такой можно отнести Entity Component.В случае использования EC может появиться новая проблема – при большом количестве компонентов, связанных между собой в одном объекте, становиться сложно разобраться в его работе. Выходом может стать некий компонент, который контролирует взаимодействия между компонентами в одной Entity или в группе вложенных Entities. Такой подход известен как паттерн Посредник (Mediator). Но даже “посредника“ будет недостаточно для более сложных случаев. К тому же он не является универсальным. Для каждой Entity с множеством связанных компонентов придёться реализовывать новый тип “посредника”. Есть и другой выход. EC можно комбинировать с другими подходами на основе графов и деревьев, которые будут описаны позже.Композиция/агрегация с вынесением логики вне объекта и его составляющихEntity Component System (ECS)Я не работал с этим подходом, но опишу то, как я его понял.В ECS объект разбивается на 3 типа составляющих: сущность, компонент (один или несколько), система (общая для произвольного числа объектов). Этот подход похож на EC, но объект разбивается уже на 3 типа составляющих, а компонент содержит только данные.Определения:
  • Entity – его основное назначение, это идентифицировать объект в системе. Зачастую Entity является просто числовым идентификатором, с которым сопоставляется список связанных с ним компонентов. В других вариациях Entity также может брать на себя роль контейнера для компонентов. Как и в EC подходе, в Entity нельзя писать пользовательский код, только добавлять компоненты.
  • Component - объект с определенными данными для Entity. Не содержит логики.
  • System - в каждой системе описывается логика. Каждая система перебирает список компонентов определенных типов или компоненты определенных entities и выполняет логику с использованием данных в компонентах. Может извлекать компоненты из entities. Результатом выполнения системы будет обновление данных в компонентах. В некоторых случаях системы могут быть обычными функциями, получающими на вход нужные данные.
Также может быть некий объект-менеджер (или несколько менеджеров), который хранит все системы и объекты, а также периодически запускает все системы. Здесь уже реализация произвольная.Пример простой ECS: Допустим есть несколько объектов, у которых есть идентификаторы. Несколько из этих объектов ссылаются на компоненты Position, в которых хранятся текущие координаты x, y, и на компонент Speed, который содержит текущую скорость. Есть система Movement, которая перебирает объекты, извлекает из них компоненты Position и Speed, вычисляет новую позицию и сохраняет новые значения x, y в компонент Position. Как я уже говорил, реализации ECS могут отличаться. Например:a) entity является контейнером для своих компонентов b) компоненты содержится в массивах/словарях. Entity является просто идентификатором, по которому определяется компонент, связанный с сущностью.
http://jmonkeyengine.ru/wiki/jme3/contributions/entitysystem/introduction-2
http://entity-systems.wikidot.com/fast-entity-component-system#java
https://www.chris-granger.com/2012/12/11/anatomy-of-a-knockout/На схеме изображен первый вариант, когда entity ссылается на свои компоненты.
Плюсы ECS
  • Слабое сцепление составляющих объекта, поэтому легко добавлять новую функциональность комбинирую по-разному составляющие.
  • Проще тестировать, т.к. нужно тестировать только системы. Компоненты и сущности тестировать не нужно.
  • Легко выполнять многопоточно.
  • Более эффективное использование памяти, кэша и, следовательно, большая производительность.
  • Легко реализовать сохранение всего приложения, т.к. данные отделены от функционала.
Минусы ECS
  • Высокая сложность, не стандартный подход.
  • для простых проектов является ненужным усложнением.
Так как я занимаюсь фронтенд разработкой, а она по большей части относится к разработки UI, то упомяну, что ECS используется в игре World of Tanks Blitz для разработки UI:
https://www.youtube.com/watch?v=nu8JJEJtsVEИтогоECS является хорошей альтернативой созданию сложных иерархий наследования. В ECS можно создать иерархии наследования для компонентов или систем, но вряд ли от этого почувствуется выгода. Скорее, быстро почувствуются проблемы от таких попыток. Как и в аналогичном случае с EС, системы в данном подходе можно комбинировать с подходами на основе графов и деревьев. В таком случае логика по-прежнему будет реализована в системах, а хранение данных в компонентах. Но, я не знаю, эффективно ли совмещение этих подходов на практике. В первую очередь нужно стремиться реализовывать системы попроще, а уже потом рассматривать комбинацию с другими подходами.Композиция/агрегация с использованием графовК данному способу повторного использования кода я отнес паттерн «машина состояний» (State machine/Finite state machine/конечный автомат). Аналогом машины состояний простой является switch:
switсh (condition) {
  case stateA: actionA();
  case stateB: actionB();
  case stateC: actionC();
}
Недостатком является то, что он может сильно разрастить, а также у разработчика может не быть возможности добавить новые состояния в систему.Для сложных случаев каждое состояние с логикой можно вынести в отдельный объект и переключаться между ними.В более сложных случаях можно разделить на большее число составляющих: состояние, действие, переход, условие перехода, событие.Также существуют иерархические машины состояний, где состояние может содержать вложенный граф состояний.Я уже описывал паттерн “Машина состояний” и его составляющие, и вкратце писал о иерархической машине состояний в статье "Приемы при проектировании архитектуры игр" в главе "машина состояний".Преимущества использования машины состояний:Хорошо описано по ссылке.Добавлю, что становится легче предусмотреть, обработать и протестировать все возможные случаи работы контекста (подсистемы), т.к. видны все его состояния и переходы. Особенно, если состояния являются просто объектами с данными и отделены от остальной логики и отображения.Где при разработке UI можно использовать машину состояний?Например, для логики сложной формы, у которой поведение и набор полей немного отличается в зависимости от роли пользователя и других параметров. Каждый объект-состояние может описывать состояние всех компонентов формы (активный, видимый, фиксированный текст элемента в таком-то состоянии и т.д.), отображение которых может отличаться в зависимости от роли пользователя и других параметров. Компоненты формы получают часть объекта-состояния, которая им нужна для своего отображения.Другие примеры использования в UI:
https://24ways.org/2018/state-machines-in-user-interfaces/
https://xstate.js.org/docs/ (библиотека для JS, которую можно использовать c React, Vue, Svelte)
https://github.com/MicheleBertoli/react-automata (библиотека для React)
https://habr.com/ru/company/ruvds/blog/346908/Подходит ли State machine в качестве основного механизма повторного использования кода и разбиения сложных объектов на составные части?Иногда он так и используется. Но, он мне кажется сложноватым и не всегда подходящим для использования в качестве основного. Зато он точно хорош в качестве дополнительного, когда нужно организовать взаимодействия между несколькими объектами или частями составного объекта.Стоит учитывать, что граф может получиться слишком сложным. Если у вас обычный код получается запутанным, то и граф скорее всего получится запутанным. В сложном графе нужно стремиться уменьшать количество связей, группировать состояния.Композиция/агрегация с использованием деревьевПаттерн composite и другие древовидные структурыДеревья часто встречается в разработке. Например, объекты в JavaScript могут содержать вложенные объекты, а те также могут содержать другие вложенные объекты, тем самым образую дерево. XML, JSON, HTML, DOM-дерево, паттерн Комповщик (Composite)  – все это примеры древовидной композиции.Дерево является графом, в котором между любыми 2-мя его узлами может быть только один путь. Благодаря этому, в нем гораздо проще разобраться, чем в графе получаемом с помощью машине состояний.Behaviour treeИнтересным вариантом композиции является Behaviour tree(дерево поведения). Это организация логики программы (обычно AI) или ее частей в виде дерева. В дереве поведения в качестве узлов выступают управляющие блоки - условие, цикл, последовательность действий, параллельное выполнение действий и блоки действий. В теории, могут быть реализованы и другие управляющие блоки, вроде switch case, асинхронного блока и других аналогов управляющих конструкций языка, но я не встречал подобного. Код действий для деревьев поведения пишет разработчик. Обычно решения для деревьев поведений содержат визуальный редактор для их создания и отладки.Я уже описывал деревья поведений в прошлом в этой статье.Более наглядный пример схемы готового дерева из плагина banana-tree
Дерево поведения можно рассматривать как аналог обычной функции с вложенными функциями. Я думаю, понятно, что схему выше можно перевести в функцию с вложенными функциями, условиями и циклами.Если в функции написать слишком много кода или же в ней будет слишком много вложенных условий, то она будет нечитабельна и с ней будет тяжело работать. Аналогично и с деревьями поведения. Их следует делать как можно проще и с меньшей вложенностью блоков с циклами и условиями.Деревья поведения позволяют создавать сложную логику с помощью комбинации более простых действий. Сложная логика тоже может быть оформлена в виде узла дерева (в виде действия или поддерева). Также деревья поведения предоставляют единый механизм для разработки в таком стиле. Этот подход мотивирует выносить функционал в отдельные настраиваемые объекты, зачастую предоставляет наглядное средство для отладки, упорядочивает и уменьшает связи между объектами, позволяет избежать жесткой зависимости составляющих.Для простых случаев, как обычно, этот подход будет ненужным усложнением.Смешанные подходыДля более эффективной организации кода можно комбинировать некоторые подходы. Например, в качестве узлов машины состояний можно использовать деревья поведения.Довольно многое можно отнести к смешанных подходам. Entity Component в Unity3D реализован так, что позволяет хранить не только компоненты, но и вложенные сущности. А для пользовательских компонентов можно использовать наследование в простых случаях, либо объединить компоненты с более продвинутыми техниками (паттерн mediator, машина состояний, дерево поведения и другие).Примером смешивания подходов является анимационная система Mecanim в Unity3D, которая использует иерархическую машину состояний с деревьями смешивания (blend tree) для анимаций. Это относится не совсем к коду, но является хорошим примером комбинации подходов.К смешанным подходам я отнес React компоненты с хуками, т.к. там довольно специфичный случай. С точки зрения разработчика, для повторного использования кода в компоненте используется дерево функций (хуков). С точки зрения организации данных в компоненте – компонент хранит список с данными для хуков. К тому же связь между хуками и компонентом устанавливается во время выполнения. Т.к. я разрабатываю на React, то решил включить частичное описание внутреннего устройство компонента с хуками в статью. React hooksЭта часть статьи для разработчиков, знакомых c React. Остальным многое в ней будет не понятно. Особенность функциональных компонентов в React-е в том, что разработчик не указывает сам связь между компонентом и его хуками. Отсюда возникает вопрос, как React определяет к какому компоненту применить такой-то хук?Как я понял, хуки при вызове добавляют к текущему обрабатываемому компоненту (точнее к fiber-ноде) свое состояние – объект, в котором могут быть указаны переданные сallback-и (в случае useEffect, useCallback), массив зависимостей, значения (в случае useState) и прочие данные (в случае useMemo, useRef, …).А вызываются хуки при обходе дерева компонентов, т.е. когда вызывается функция-компонент. React-у известно, какой компонент он обходит в данный момент, поэтому при вызове функции-хука в компоненте, состояние хука добавляется (или обновляется при повторных вызовах) в очередь состояний хуков fiber-ноды. Fiber-нода – это внутреннее представление компонента.
Стоит отметить, что дерево fiber элементов не совсем соответствует структуре дерева компонентов. У Fiber-ноды только одна дочерняя нода, на которую указывает ссылка child. Вместо ссылки на вторую ноду, первая нода ссылается на вторую (соседнюю) с помощью ссылки sibling. К тому же, все дочерние ноды ссылаются на родительскую ноду с помощью ссылки return.Также для оптимизации вызова эффектов (обновление DOM, другие сайд-эффекты) в fiber-нодах используются 2 ссылки (firstEffect, nextEffect), указывающие на первую fiber-ноду с эффектом и следующую ноду, у которой есть эффект. Таким образом, получается список нод с эффектами. Ноды без эффектов в нем отсутствуют. Подробнее об этом можно почитать по ссылкам в конце главы.Вернемся к хукам. Структура сложного функционального компонента с несколькими вложенными custom hooks для разработчика выглядит как дерево функций. Но React хранит в памяти хуки компонента не как дерево, а как очередь. На схеме ниже изображен компонент с вложенными хукам, а под ним fiber-нода с очередью состояний этих же хуков.
На схеме в fiber-ноде отображены также поля, которые участвует в создании различных структур для оптимизации рендеринга. Они не будут рассмотрены в рамках статьи.Чтобы просмотреть содержимое fiber-ноды, достаточно воспользоваться console.log и вставить туда JSX код, который возвращает компонент:
function MyComponent() {
  const jsxContent = (<div/>);
  console.log(jsxContent);
  return jsxContent;
}
Корневую fiber-ноду можно просмотреть следующим образом:
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
console.log(rootElement._reactRootContainer._internalRoot);
Также есть интересная наработка: react-fiber-traverseПод спойлером приведен код компонента с хуками и отображение его fiber-ноды
import { useState, useContext, useEffect,useMemo, useCallback,
         useRef, createContext } from 'react';
import ReactDOM from 'react-dom';
const ContextExample = createContext('');
function ChildComponent() {
  useState('childComponentValue');
  return <div />;
}
function useMyHook() {
  return useState('valueB');
}
function ParentComponent() {
  const [valueA, setValueA] = useState('valueA');
  useEffect(function myEffect() {}, [valueA]);
  useMemo(() => 'memoized ' + valueA, [valueA]);
  useCallback(function myCallback() {}, [valueA]);
  useRef('refValue');
  useContext(ContextExample);
  useMyHook();
  const jsxContent = (
    <div>
      <ChildComponent />
      <button onClick={() => setValueA('valueA new')}>Update valueA</button>
    </div>
  );
  console.log('component under the hood: ', jsxContent);
  return jsxContent;
}
const rootElement = document.getElementById('root');
ReactDOM.render(
  <ContextExample.Provider value={'contextValue'}>
    <ParentComponent />
  </ContextExample.Provider>,
  rootElement,
);

С более подробным описанием работы внутренних механизмов React на русском языке можно ознакомиться по ссылкам:Как Fiber в React использует связанный список для обхода дерева компонентов
Fiber изнутри: подробный обзор нового алгоритма согласования в React
Как происходит обновление свойств и состояния в React — подробное объяснение
За кулисами системы React hooks
Видео: Под капотом React hooksУ подхода с хуками на данный момент есть недостаток - фиксированное дерево функций в компонентах. При стандартном использовании хуков, нельзя изменить логику уже написанного компонента или хуков, состоящих из других хуков. К тому же это мешает тестированию хуков по отдельности. В какой-то степени можно улучшить ситуацию композицией (compose) хуков. Например, существует такое решение.Линейность кода и составляющих сложного объектаИзвестно, что множество вложенные условий, callback-ов затрудняют читаемость кода: https://refactoring.guru/ru/replace-nested-conditional-with-guard-clauses
https://habr.com/ru/company/oleg-bunin/blog/433326/ (в статье упоминается линейный код)
https://www.azoft.ru/blog/clean-code/ (в статье упоминается линейность кода)Я думаю, что наследование, большие цепочки и иерархии вложенных объектов могут привести к аналогичной ситуации, но для составляющих сложного объекта. Даже если объект расположен линейно в коде, внутри он может быть устроен так, что необходимо пройтись по множеству родительских классов или вложенных объектов, чтобы разобраться в его функционале. Я уже писал ранее про деревья поведения, что в них следует избегать большой вложенности. Так и в других случаях.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_proektirovanie_i_refaktoring (Проектирование и рефакторинг), #_oop (ООП), #_reactjs, #_proektirovanie (проектирование), #_oop (ооп), #_kompozitsija (композиция), #_patterny (паттерны), #_react, #_javascript, #_razrabotka_vebsajtov (
Разработка веб-сайтов
)
, #_javascript, #_proektirovanie_i_refaktoring (
Проектирование и рефакторинг
)
, #_oop (
ООП
)
, #_reactjs
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 06-Окт 01:09
Часовой пояс: UTC + 5