Порядок выполнения реактивных реакций имеет свои особенности, которые часто оставляют на волю случая. Однако давайте рассмотрим все возможные варианты…

📰 Subscribe: По времени подписки
🧨 Event: По времени возникновения события
📶 Deep: По глубине зависимости
👨‍💻 Code: По позиции в программе

📰 Subscribe: По времени подписки

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

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

Это типичная проблема большинства библиотек.

🧨 Event: По времени возникновения события

Предположим, нам удалось зафиксировать порядок подписок тем или иным способом. Однако есть еще один источник нестабильности — порядок действий.

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

📶 Deep: По глубине зависимости

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

В этом примере Post изменяется на тот, к которому у нас нет доступа. Сначала обновится содержимое страницы, что не только приведет к ненужным пересчетам, но и может вызвать ошибки или просто мусор в виде побочных эффектов. И только затем, при вычислении Page, выяснится, что PostPage должен быть полностью уничтожен, а вместо него должно отображаться сообщение об ошибке доступа Forbidden.

Обратите внимание, что существование Title и Body зависит от значения Page. Но сами значения Title и Body уже не зависят от значения Page. И наоборот, значение Page не зависит от значений Title и Body. То есть связь между ними не реактивна. Но она есть. И это уже связь между владельцем и имуществом. То есть значение Page владеет реактивными состояниями Title и Body и, следовательно, контролирует их жизненный цикл.

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

👨‍💻 Code: По позиции в программе

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

Здесь сначала обновится Allow, затем Page, что приведет к потере PostPage и, как следствие, к уничтожению PostPage со всеми внутренними состояниями без их вычисления.

🐵 Стабильность порядка вычислений

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

Это кажется очевидным, но вы будете в ужасе, когда узнаете, что стабильность поведения почти никогда не гарантируется. В результате возможна ситуация, когда программист взял тот же код, открыл те же окна, ввел те же значения… но у пользователя возникает ошибка, а программист не может ее воспроизвести. И тогда начинается веселая отладка.

Поэтому в $mol_wire уделено особое внимание не только корректности, но и предсказуемости порядка вычислений. Это позволяет не бояться побочных эффектов даже в сильно динамичных приложениях, не теряя согласованности состояний. При этом код остаётся простым и элегантным:

class App extends Object {
  
  @mem Post( next?: Post ) { return next } // mutable state

  @mem PostPage() { // object factory
    return PostPage.make({
      Post => this.Post()
    })
  }

  @mem Forbidden() { // object factory
    return Forbidden.make({})
  }

  @mem Allow() { // derived state
    return Guard( this.Post() )
  }

  @mem render() { // recursive side effect
    if( this.Allow() ) this.PostPage().render()
    else this.Forbidden().render()
  }
  
}

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

А пока, подписывайтесь на что-нибудь, вступайте во что-то там, и держите руку на пульсе вот этого вот.