Может ли реактивное управление состоянием быть одновременно неизменным и наиболее эффективным?

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

Это всегда одна из тех тем, вызывающих разногласия. Изменчивость против неизменности, ООП против функционального программирования, императивное против декларативного. Я ранее упоминал, что это не так уж и однозначно в своей статье о Dualities. Но я считаю, что это та область, которую я должен защищать больше всего, когда говорю об API с Solid. Я собираюсь исследовать, почему Solid кажется единственной неизменяемой мелкозернистой реактивной библиотекой, почему я был так непреклонен в своем выборе и почему этот подход превосходит любую существующую неизменяемую систему.

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

Быть реактивным

Реактивные библиотеки бывают разных форм и размеров, и это, как правило, является бесконечным источником путаницы при попытке определить термины. С одной стороны, у вас есть Rx, например RxJS, который основан на потоках соединений. Эти библиотеки работают с потоками конвейера данных и легко неизменяемы по своей природе, поскольку они могут создавать новые объекты на каждом этапе преобразования. С другой стороны, у вас есть то, что я называю детализированными реактивными библиотеками, которые основаны на создании реактивного графа или дерева. У вас есть похожий наблюдаемый примитив в качестве отправной точки, но отличается тем, что каждый узел несет значение. Создавая вычислительные выражения, которые оборачивают чтение значения этих узлов для создания новых узлов, вы составляете поведения. Последний подход используется в Solid, MobX, Vue, Svelte, Knockout и т. Д.

В обоих этих подходах основной атом неизменен. Эти библиотеки основаны на управлении изменениями. Если бы что-то могло изменить данные вне их системы, они бы не узнали, что что-то изменилось. Цикл выполнения библиотеки Reactive основан на каскадном распространении данных (It Reacts…), поэтому у нее не будет возможности сравнивать значения, если она не настроена для этой цели. Ожидается, что он будет контролировать все взаимодействие с данными.

Значит, реактивные библиотеки неизменяемы? Не мелкозернистые. Я хочу подчеркнуть, что, хотя они основаны на этих внешне неизменных атомах, если вы посмотрите на систему в целом, есть несколько точек данных, где они объединены в деревья, а сами деревья не являются неизменяемыми. В то время как внутренняя часть атома есть, вам необходимо сохранять ссылки для отслеживания изменений и уведомления подписчиков. Данные совмещены с реактивным графиком. Вы не можете просто выбросить это при каждом изменении. Так что нет, эти системы построены на основе мутаций.

Изменить распространение

Это похоже на бросание камней в озеро, а иногда и на пушечное ядро ​​в бассейн, внесение изменений может вызвать каскад эффектов в вашей системе. Реактивные библиотеки позволяют легко составлять поведения и строить сети зависимостей. Я имею в виду, что не имеет значения, какое взаимодействие мы можем подключить и автоматизировать. Нет ни сверху вниз, ни слева направо. И эти цепочки могут начинаться с любого узла графа в любое время. Если это кажется хаотичным, вы правы.

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

К их преимуществу, это поколение библиотек решило большинство проблем путем пакетирования изменений и изоляции графов. Они достигают этого разными способами. Solid использует явные синхронные пакеты, которые собирают все изменения перед их фиксацией и помечают соответствующие узлы как устаревшие или потенциально устаревшие. Последующие изменения не выполняются, пока текущий контекст не завершит свой цикл. Оттуда мы используем механизм push / pull, чтобы определить, нужно ли повторно вычислить любое устаревшее значение. Этот подход очень похож на то, что используется в MobX с их транзакциями. Ни один последующий процесс не видит частично измененное состояние.

Но то, что я хотел привлечь больше, - это изоляция графов. Большинство библиотек приняли сильную компонентную модель. В библиотеках, таких как MobX или Vue, их реактивные атомы живут и умирают благодаря Компоненту. Реактивная система Svelte в основном компилируется прямо в код вашего компонента. В отличие от этого, Solid - это возврат к тем ранним библиотекам, поэтому управление должно осуществляться по-другому.

Без использования Virtual DOM или подходов к рендерингу сверху вниз, таких как React или Vue, и не желая отказываться от переносимости, как Svelte, изоляция компонентов не принесет нам пользы. И не будем питать иллюзий. У всех этих библиотек есть средства для создания глобальных хранилищ, что возвращает нас к общему контексту. Так что им нужно снова решить проблему немного по-другому.

Разве нет хорошего способа иметь одно простое решение состояния для реактивной библиотеки? Первые дни React заставили нас использовать разные решения для локального и глобального состояния, но в этом нет необходимости.

Неизменяемость FTW!

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

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

const data = {/* some data*/}
fn(data);
// do I know with certainty what data looks like at this point?
const value = data.someProperty;

Даже ООП здесь не поможет. Вы можете оставить свои классы и декораторов дома. Конечно, вы, возможно, заключили договор, но вы по-прежнему даете ребенку возможность изменять ваши данные, на этот раз предсказуемым образом, но все же по своему усмотрению. Это не обязательно плохо, но не всегда желательно. Как и в случае с сетевой безопасностью, наиболее безопасная система - это та, в которой каждому узлу предоставляется ровно столько доступа, сколько минимально необходимо.

Однако решение довольно простое. Разделите чтение и запись. Имейте неизменяемые данные с явными установщиками для обработки обновлений. Это не обязательно должно быть только для глобальных магазинов. Как показано с помощью React, это также отличный шаблон для управления локальным состоянием. Это не так хорошо работает с ООП, но я бы поспорил в пользу пользовательского интерфейса, который, по сути, представляет собой преобразование данных в форме view = fn(data) функциональный подход - как раз подходящий инструмент для работы. Поступая так, вы просто устраняете всю возможность для этой категории проблем.

Твердое состояние

Итак, принудительный однонаправленный поток в реактивной библиотеке. Это самостоятельная проблема, поскольку это библиотеки, построенные на мутации. Мы не можем просто использовать типичные неизменные шаблоны. Обычно, когда некоторые вложенные данные изменяются, вы выполняете неглубокое клонирование по пути изменения, сохраняя ссылки на неизмененные ветви. Например, изображение меняет номер дома пользователя с 101 на 102:

const state = {
  ...prevState,
  user: {
    ...prevState.user,
    address: {
      ...prevState.user.address
      streetNumber: 102
    }
  }
}

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

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

state.user.address.streetNumber = 102;

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

Итак, ключ к объекту состояния Solid - это прокси-объект ES2015. Не сильно отличается от MobX Observable. Он отслеживает доступы и оборачивает детей в их собственные прокси. Таким образом, мы можем отслеживать все реактивные атомы в дереве. Однако для Solid я намеренно сделал прокси-сервер только для чтения. Когда вы создаете State в Solid, он разделяется на этот прокси и установщик:

const [state, setState] = createState({/* some data */});

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

Так как же setState работает? Я взял свои любимые неизменяемые библиотеки и реализовал как неизменяемую, так и изменяемую парадигму. Неизменяемый вариант краток и напоминает React и ImmutableJS setIn:

//top level
setState({ count: state.count + 1 });
//function setter on path returning new state
setState("count", c => c + 1):
//nested path
setState("user", "address", "streetNumber", 102);

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

setState(produce(s => { s.user.address.streetNumber = 102; }))

Однако настоящий ключ заключается в том, что оба этих метода используют встроенную пакетную обработку Solid поверх изменяемой системы. Это означает, что производительность значительно лучше, чем может быть любая неизменяемая система. Под капотом нет ни клонирования, ни фактической неизменности. Вы получаете все преимущества неизменяемости с API, который вы хотели использовать, чтобы быть неизменяемым, без каких-либо недостатков производительности.

Заключение

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

Итак, я хочу, чтобы вы на мгновение задумались об альтернативе. На самом деле, вы, наверное, слышали об этом. Он называется Vue. Обе библиотеки позиционируют себя как средние среди библиотек пользовательского интерфейса. Обе библиотеки имеют реактивную систему и сильно заимствуют у React, но с точностью до наоборот. Значения Vue упрощают, прежде всего, изменяемое присваивание и двустороннюю привязку, но использует технологию Virtual DOM, такую ​​как React, для обеспечения строгого рендеринга. Solid ценит философию React, заключающуюся в строгом соблюдении требований, но отвергает его технологии в пользу большей производительности и упрощения абстракции. В некотором смысле, несмотря на то, что React и Angular, возможно, противоположны, Solid является своего рода анти-Vue. Как будто мы с Эваном увидели одно и то же и пришли к совершенно противоположным выводам. Каким интересным местом быть, если вы считаете, что Vue может быть самой популярной библиотекой пользовательского интерфейса? Я имею в виду, как вы с этим спорите?

Что ж, я стараюсь изо всех сил.