React observer что это

Обновлено: 18.05.2024

egghead.io lesson 1: observable & observer

The observer HoC / decorator subscribes React components automatically to any observables that are used during render. As a result, components will automatically re-render when relevant observables change. But it also makes sure that components don't re-render when there are no relevant changes. As a result MobX applications are in practice much better optimized than Redux or vanilla React applications are out of the box.

  • observer is provided through the separate mobx-react package.
  • If your code base doesn't have any class based components, you can also the mobx-react-lite package, which is smaller.

observer automatically tracks observables used during render

Using observer is pretty straight forward:

Because observer automatically tracks any observables that are used (and none more), the Timer component above will automatically re-render whenever timerData.secondsPassed is updated, since it is declared as an observable.

Note that observer only subscribes to observables used during the own render of the component. So if observables are passed to child components, those have to be marked as observer as well. This also holds for any callback based components.

observer listens to any observables used

In above components the timer data is received as prop, but in principle that is irrelevant for the working for observer . Rewriting the Timer component above to the following results in semantically the same application:

Note that the timerData is now read directly from the closure. This is a practice we don't recommend, but is shown to demonstrate that observer tracks any observables used, regardless where they are coming from.

Using context to pass observables around

This means that it also possible to store observables in context and use them, and observer will still do its job:

Storing observables in local component state

Similarly, we can store observables in local component state using useState . Although in practice local component state is often simple enough to not really need observables. Note that we don't need the state setter since we will mutate the observable, rather than replacing it entirely:

  • Tip: the useLocalStore hook further simplifies this pattern.
  • Tip: if for some reason props or non-observable local state needs to be synced with the observable state, the useObservableSource hook can be used.

In general we recommend to not resort to using MobX observables for local component state too quickly; as this can theoretically lock you out of some features of React's Suspense mechanism. Generally speaking it only adds real value when there are complex computations involved (which MobX will optimize) or when there are complex view models.

Local observable state in class based components

Just like normal classes, you can introduce observable properties on a component by using the @observable decorator. Hence the example above could also have been written as:

For more advantages of using observable local component state, for class based components see 3 reasons why I stopped using setState .

When to apply observer ?

The simple rule of thumb is: all components that render observable data. If you don't want to mark a component as observer, for example to reduce the dependencies of a generic component package, make sure you only pass it plain data, for example by converting it to plain data in a parent component, that is an observer , using toJS .

Characteristics of observer components

  • Observer enables your components to interact with state that is not managed by React, and still update as efficiently as possible. This is great for decoupling.
  • Observer only subscribe to the data structures that were actively used during the last render. This means that you cannot under-subscribe or over-subscribe. You can even use data in your rendering that will only be available at later moment in time. This is ideal for asynchronously loading data.
  • You are not required to declare what data a component will use. Instead, dependencies are determined at runtime and tracked in a very fine-grained manner.
  • @observer implements memo / shouldComponentUpdate automatically so that children are not re-rendered unnecessary.
  • observer based components sideways load data; parent components won't re-render unnecessarily even when child components will.
  • The props object and the state object of an observer component are automatically made observable to make it easier to create @computed properties that derive from props inside such a component.

Use the <Observer> component in cases where you can't use observer

Sometimes it is hard to apply observer to a part of the rendering, for example because you are rendering inside a callback, and you don't want to extract a new component to be able to mark it as observer . In those cases <Observer /> comes in handy. It takes a callback render function, that is automatically rendered again every time an observable used is changed:

How can I further optimize my React components?

Using observer with classes without decorators

Using @observer as a decorator is optional, const Timer = observer(class Timer . < >) achieves exactly the same.

How to enable decorators?

When combining observer with other higher-order-components, apply observer first

When observer needs to be combined with other decorators or higher-order-components, make sure that observer is the innermost (first applied) decorator; otherwise it might do nothing at all.

Gotcha: dereference values inside your components

MobX can do a lot, but it cannot make primitive values observable (although it can wrap them in an object see boxed observables). So not the values that are observable, but the properties of an object. This means that @observer actually reacts to the fact that you dereference a value. So in our above example, the Timer component would not react if it was initialized as follows:

In this snippet just the current value of secondsPassed is passed to the Timer , which is the immutable value 0 (all primitives are immutable in JS). That number won't change anymore in the future, so Timer will never update. It is the property secondsPassed that will change in the future, so we need to access it inside the component. One could also say: values need to be passed by reference and not by value.

If the problem is not entirely clear, make sure to study what does MobX react to? first!

Advanced interaction patterns with reactions, observables, props, etc

Вывод

Mobx это логичное развитие паттерна observer для решения проблемы "точечных" обновлений компонентов и мемоизации функций. Если немного отрефакторить и вынести код в примере выше из компонента в Observable и вместо вызова .get() и .set() поставить геттеры и сеттеры, то мы почти что получим observable и computed декораторы mobx-а. Почти — потому что у mobx вместо простого вызова в цикле находится более сложный алгоритм вызова подписчиков для того чтобы исключить лишние вызовы computed для ромбовидных зависимостей, но об этом в следующей статье.

Применение паттерна observer в Redux и Mobx


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

В redux-е этот паттерн применяется без всяких изменений — пакет "react-redux" предоставляет функцию connect которая оборачивает компонент и при вызове componentDidMount вызовет subscribe() метод у Observable , при вызове componentWillUnmount() вызовет unsubscribе() а dispatch() просто вызовет метод trigger() который в цикле вызовет всех слушателей где каждый в свою очередь вызовет mapStateToProps() и потом в зависимости от того изменилось ли значение — вызовет setState() на самом компоненте. Все очень просто, но платой за такую простоту реализации является необходимость работать с состоянием иммутабельно и нормализировать данные а при изменении отдельного объекта или даже одного свойства оповещать абсолютно всех подписчиков-компонентов даже если они никак не зависят от той измененной части состояния и при этом в компоненте-подписчике необходимо явно указывать от каких частей стора он зависит внутри mapStateToProps()

Mobx очень похож на redux тем что использует этот паттерн observer только развивает его еще дальше — что если мы не будем писать mapStateToProps() а сделаем так чтобы компоненты зависели от данных которые они "рендерят" самостоятельно , по отдельности. Вместо того чтобы собирать подписчиков на одном объекте состояния всего приложения, подписчики будут подписываться на каждое отдельное поле в состоянии. Это как если бы для юзера, у которого есть поля firstName и lastName мы создали бы целый redux-стор отдельно для firstName и отдельно для lastName .

Таким образом, если мы найдем легкий способ создавать такие "сторы" и подписываться на них, то mapStateToProps() будет не нужен, потому что эта зависимость от разных частей состояния уже выражается в существовании разных сторов.

Итак на каждое поле у нас будет по отдельному "мини-стору" — объекту observer где кроме subscribe() , unsubscribe() и trigger() добавится еще поле value а также методы get() и set() и при вызове set() подписчики вызовутся только если само значение изменилось.

Вместе с этим требование иммутабельности стора нужно трактовать немного по-другому — если мы в каждом отдельном сторе будем хранить только примитивные значение, то с точки зрения redux нет ничего зазорного в том чтобы вызвать user.firstName.set("NewName") — поскольку строка это иммутабельное значение — то здесь происходит просто установка нового иммутабельного значения стора, точно так же как и в redux. В случаях когда нам нужно сохранить в "мини-сторе" объект или сложные структуры то можно просто вынести их в отдельные "мини-сторы". Например вместо этого

лучше написать так чтобы компоненты могли по отдельности зависеть то от "email" то от "address" и чтобы не было лишних "перерендеров"

Второй момент — можно заметить что с таким подходом мы будем вынуждены на каждый доступ к свойству вызывать метод get() , что добавляет неудобств.

Но эта проблема решается через геттеры и сеттеры javascript-а

А если вы не относитесь негативно к декораторам то этот пример можно еще больше упростить

В общем можно пока подвести итоги и сказать что 1) никакой магии в этом моменте нет — декораторы это всего лишь геттеры и сеттеры 2) геттеры и сеттеры всего лишь считывают и устанавливают root-state в "мини-сторе" а-ля redux

Идем дальше — для того чтобы подключить все это к реакту нужно будет в компоненте подписаться на поля которые в нем выводятся и потом отписаться в componentWillUnmount

Здесь функция connect оборачивает компонент или stateless-component (функцию) реакта и возвращает компонент который благодаря этому механизму автоподписки подписывается на нужные "мини-сторы".

В итоге у нас получился такой вот механизм автоподписок только на нужные данные и оповещений только когда эти данные изменились. Компонент будет обновляться только тогда когда изменились только те "мини-сторы" на которые он подписан. Учитывая, что в реальном приложении, где может быть тысячи этих "мини-сторов", с данным механизмом множественных сторов при изменении одного поля будут обновляться только те компоненты которые находятся в массиве подписчиков на это поле, а вот подходом redux когда мы подписываем все эти тысячи компонентов на один единственный стор, при каждом изменении нужно оповещать в цикле все эти тысячи компонентов (и при этом заставляя программиста вручную описывать от каких частей состояния зависят компоненты внутри mapStateToProps )

Более того этот механизм автоподписок способен улучшить не только redux а и такой паттерн как мемоизацию функций, и заменить библиотеку reselect — вместо того чтобы явно указывать в createSelector() от каких данных зависит наша функция, зависимости будут определяться автоматически точно так же выше сделано с функцией render()

Mobx — управление состоянием вашего приложения

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

Основная идея

Состояние (state ориг.) это сердце каждого приложения и нет более быстрого способа создания забагованого, неуправляемого приложения, как отсутствие консистентности состояния. Или состояние, которое несогласованно с локальными переменными вокруг. Поэтому множество решений по управлению состоянием пытаются ограничить способы, которыми можно его изменять, например сделать состояние неизменяемым. Но это порождает новые проблемы, данные нуждаются в нормализации, нет гарантии ссылочной целостности и становится почти невозможно использовать такие мощные концепты как прототипы(prototypes ориг.).

MobX позволяет сделать управление состоянием вновь простым, вернувшись к корню проблемы: он делает невозможным инконсистентность состояния. Стратегия достижения этого довольно проста: убедится что, все что может быть вынуто из состояния, будет вынуто. Автоматически.

Концептуально MobX обрабатывает ваше приложение как электронная таблица (отсылка к офисной программе для работы с таблицами прим. пер.).

Во-первых, есть состояние State приложения. Графы объектов, массивов, примитивов, ссылок которые формируют модель вашего приложения.

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

Реакции Reactions очень похожи на производные Derivations. Основное отличие: они не возвращают значение, но запускаются автоматически, чтобы выполнить какую то работу. Обычно это связано с I/O. Они проверяют, что DOM обновился или сетевые запросы выполнились вовремя

Наконец, есть действия Actions. Действия это все те штуки которые меняют состояние. MobX проследит, чтобы все изменения в состоянии приложения, вызванные действиями, автоматически обработались всеми производными и реакциями. Синхронно и без помех.

Простой todo store.

Довольно теории, рассмотрим его в действии, будет намного понятнее, чем внимательно читать написанное выше. Ради оригинальности давайте начнем с очень простого Todo хранилища. Ниже приведен очень простой TodoStore, который управляет коллекцией todo. MobX пока не участвует.

Мы только что создали todoStore инстанс с коллекцией todos. Теперь надо заполнить todoStore какими-нибудь объектами. Чтобы убедиться, что от наших изменений есть эффект, мы вызываем todoStore.report после каждого изменения:

Становимся реактивными

До сих пор в нашем коде не было ничего необычного. Но что если мы не хотим вызывать report явно, но объявим что нужно вызывать этот метод на каждое изменение состояния? Это освободит нас от обязанности вызывать этот метод в нашем коде. Мы должны быть уверены в том, что последний результат вызова report будет выведен на экран. Но мы не хотим беспокоится, каким образом это будет сделано.

К счастью, именно MobX может сделать это за нас. Автоматически вызывать код, который зависит от состояния. Так что наша функция report будет вызываться автоматически. Чтобы этого достичь TodoStore нужно стать отслеживаемым (observable ориг.), чтобы MobX смог следить за всеми изменениями. Давайте немного изменим наш класс.

Так же, свойство completedTodosCount будет вычислено автоматически из свойства todos . Мы можем достичь этого используя @observable и @computed декораторы.

Вот и все! Мы пометили некоторые свойства как @observable чтобы MobX знал что они могут изменяться со временем. Расчеты помечены @computed декораторами, чтобы знать что они могут быть вычислены на основе состояния.

Свойство pendingRequests и assignee еще не используются, но мы увидим их в действии чуть ниже. Для краткости, все примеры используют ES6, JSX и декораторы. Но не беспокойтесь, все декораторы в MobX имеют ES5 аналоги.

В конструкторе класса мы создали маленькую функцию, которая выводит отчет, и обернули ее в autorun . Он создаст реакцию, которая запустится единожды, и после этого будет автоматически перезапускаться всякий раз, когда отслеживаемые данные внутри функции изменятся. Поскольку report использует отслеживаемое свойство todos , то он будет выводить результат report по мере необходимости.

Круто, не правда ли? report вызывается автоматически, синхронно, без утечки промежуточных значений. Если внимательно изучить вывод в лог, вы увидите, что четвертая строка в коде не приведет к новой записи в лог. Потому что report фактически не изменился в результате переименования таска, но данные внутри изменились. С другой стороны, изменение атрибута name у первого todo обновило результат вывода report , так как name активно используется в выводе результата report . Это демонстрирует, что отслеживается не только массив todos , но и индивидуальные значения в нем.

Делаем React реактивным

Хорошо, до сих пор мы делали реактивными "глупые" отчеты. Теперь настало время сделать реактивный интерфейс вокруг того же хранилища. Компоненты у React (не смотря на свое название), не реактивные из коробки. @observer декоратор из пакета mobx-react исправляет это, оборачивая render метод в autorun , автоматически делая ваши компоненты синхронизированными с состоянием. Это концептуально ничем не отличается от того что мы делали с report до этого.

Следующий листинг определяет несколько React компонентов. От MobX здесь только @observer декоратор. Этого достаточно, чтобы убедится, что каждый компонент перерисовывается, когда изменяются релевантные для него данные. Вам больше не нужно вызывать setState , и вам не нужно выяснять, как подписываться на части вашего приложения используя селекторы или компоненты высокого порядка (привет Redux), которые нуждаются в конфигурировании. В основном, все компоненты становятся "умными". Если они не определены в "тупой" декларативной манере.

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

Работа со ссылками

До сих пор мы создавали отслеживаемые объекты (с прототипом и без), массивы и примитивы. Но вам может показаться интересным, как обрабатываются ссылки в MobX? В предыдущих листингах, вы могли заметить assignee свойство у todos . Давайте дадим ему некоторое другое значение, создав еще одно хранилище (ладно, это просто массив) содержащее людей, и назначим их к задачам.

Теперь у нас есть два независимых хранилища. Одно с людьми, другое с задачами. Чтобы назначить свойству assignee персону из хранилища с персонами, нам нужно просто присвоить значение через ссылку. Эти значения подхватятся TodoView автоматически. С MobX нет нужды в нормализации данных и написании селекторов, чтобы наши компоненты обновлялись. На самом деле, не имеет значения где хранятся данные. Пока объекты "наблюдаемы", MobX будет отслеживать их. Настоящие JavaScript ссылки тоже работают. MobX отслеживает их автоматически если они релевантны для производных значений.

Асинхронные действия

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

Мы начинаем с обновления свойства pendingRequests , чтобы интерфейс отобразил текущий статус загрузки. После завершения загрузки, мы обновим список todo и уменьшим счетчик pendingRequests . Просто сравните этот кусок кода с тем, что мы видели выше, чтобы увидеть как используется свойство pendingRequests .

DevTools

Пакет mobx-react-devtools предоставляет инструментарий разработчика, который может быть использован в любом MobX + React приложении.

Вывод

На этом все! Никакого бойлерплейта. Простые и декларативные компоненты которые формируют UI легко и просто. Полностью обновляются из состояния. Теперь вы готовы начать использовать пакеты mobx и mobx-react в вашем приложении.
Краткое резюме вещей которые вы сегодня узнали:

  • Используйте @observable декоратор или observable(объект или массив) функцию чтобы сделать ваши объекты отслежываемыми MobX
  • Декоратор @computed может быть использован для создания функций которые вычисляют свое значение из состояния
  • Используйте autorun , чтобы автоматически запускать ваши функции на основе отслеживаемого состояния. Это применимо для логирования или сетевых запросов.
  • Используйте декоратор @observer из пакета mobx-react , чтобы наделить ваши React компоненты реактивной силой. Они автоматически будут наиболее эффективно обновляться. Даже в больших и сложных приложениях с большим количеством данных.
MobX не контейнер состояния

Люди часто используют MobX как альтернативу Redux. Но пожалуйста, обратите внимание, что это просто библиотека, для решения определенной проблемы а не архитектура или контейнер состояния. В этом смысле приведенные выше примеры являются надуманными и рекомендуется использовать правильные архитектурные решения, как инкапсуляция логики в методах, их организация в хранилищах или контроллерах и т.д. Или как кто то написал на Hacker News:

«Использовать MobX означает использование контроллеров, диспетчеров, действий, супервизоров или любой другой формы управления потоком данных, это ведет нас к тому, что архитектурную потребность вашего приложения проектируете вы сами, а не используя то что используют по умолчанию для чего то большего чем Todo приложение»

Заинтригованы? Вот некоторые полезные ссылки (на английском прим. пер.):

Изучаем и реализуем алгоритм работы правильного observer паттерна для react компонентов


Итак продолжаем развивать observer-паттерн. В предыдущей статье от старого и очень простого паттерна "observer" маленькими шагами мы пришли к mobx и написали его мини-версию. В этой статье мы напишем полноценную версию mobx которая реализует алгоритм обновления зависимостей в правильном порядке для избежания ненужных вычислений. Надо сказать что попытки описать этот алгоритм на хабре предпринимались и раньше в статьях товарища vintage про атомы тут, тут, и тут но там не описан в полной мере последний "правильный" порядок обновления о чем и будет речь в этой статье.

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

Давайте немного отрефакторим — вынесем логику установки глобального массива внутрь самого обзервера. Это можно представить как например ячейки таблицы в гугл-докс — есть ячейка которая просто хранит значение а есть ячейка которая хранит не только значение (которое будет закешировано) а и формулу(функцию) для ее пересчета. И заодно кроме формулы функции-пересчета мы добавим еще параметр функции для выполнения сайд-эффектов, как например вызов setState(<>) на компоненте, когда у нас изменится значение. В итоге получим вот такой вот класс Cell

Теперь выясним режимы обновления нашей обcерверов. В примере выше у нас пока все обcерверы активные — после того как первый раз вызвался .get он подписался на свои dependencies и будет вызываться каждый раз когда какая-то зависимость изменит свое значение. Этот режим удобен для компонентов которые должны обновляться каждый раз когда изменились данные на которые они подписаны но есть так называемые "кешируемые" или "мемоизированные" функции для которых такое поведение нежелательно. Например есть обзервер const fullName = new Cell(()=>firstName.get() + lastName.get()) который должен вычислять полное имя когда изменится либо имя либо фамилия. Но что если после того как он вычислится к fullName в приложении при каких-то условиях обращаться не придется? Мы получим лишнее вычисление и чтобы этого избежать можно сделать так чтобы компонент вычислялся не сразу а только когда к не нему обратятся — при вызове .get() .

Лишние вычисления вообще являются ключевым моментом при сравнении библиотек основанных на модели "ячеек и формул в таблице". Лишние вычисления могут появляться при неправильном (как у нашего примера выше) алгоритме определения какие зависимости нужно вызвать после того как изменилось значение в случае ромбовидной схеме зависимостей (когда в графе зависимостей присутствуют циклы)

Давайте рассмотрим такую ситуацию — есть четыре ячейки — firstName , lastName , fullName (которая вычисляет полное имя) и label (которая выводит либо имя если оно длинное иначе полное имя)

Здесь самый простой вариант ромбовидных зависимостей — от firstName зависит fullName , от fullName зависит label но label также зависит и от firstName и получается как бы цикл.
Надо уточнить что в процессе нас интересует перевычисление только значения ячейки label (например нужно отрендерить в компоненте) поэтому если значение fullName для label вдруг вычислять не требуется то его вычислять и не стоит.

И вот первый баг — при измении firstName — в нашей реализацией Cell когда мы вы цикле вызываем подписчиков у нас компонент label будет вычисляться дважды — первый раз firstName вызовет label потому что он непосредственно него подписан, а второй раз label вычисляется когда fullName изменит свое значение. Первое вычисление label не нужно потому что содержит временные данные — новое имя и старое fullName . Соотвественно нам нужно избавиться от ненужных вычислений и сделать мы это можем только вызвав подписчиков в правильном порядке — сначала fullName а потом label .

Как мы это можем сделать? Если подумать то есть парочка вариантов.


Вкратце алгоритм состоит из способа распространения вызова функции по графу зависимостей и выявление значение "глубины" каждой зависимости через инкремент-декремент счетчика и потом вызов их в порядке увеличения глубины. Пусть при изменении имени, ячейка firstName не будет сразу вызывать подписчиков в цикле, а установит внутри каждого из слушателей значение 1 и вызовет их чтобы каждый установил значение своих подписчиков на 1 больше чем у него самого. И так рекурсивно. Ячейка fullName получит значения 1 а ячейка label получит значение 2 потому что счетчик увечили сначала ячейка firstName а потом и ячейка fullName . Теперь, после того как рекурсивный вызов закончился, ячейка fistName вызывает обратную процедуру — уменьшение счетчика рекурсивно у своих подписчиков. И теперь момент — после того как вызвался код уменьшения счетчика надо проверить если значение вернулось к нулю то только тогда следует выполнить перевычисление ячейки. Итак, произойдет уменьшение счетчика label с 2 до 1 (но не вычисляется потому что не 0) потом уменьшится счетчик fullName c 1 на 0 и вычислится fullName и только потом вычислится сам label потому что fullName после вычисление вызовет уменьшение счетчика label c 1 до 0.

Таким образом мы получили вычисление label только один раз после того как все зависимые ячейки сами обновятся и будут иметь актуальное значение.

Другим вариантом (который по факту является оптимизированной версией первого) будет идея вызвать подписчиков в порядке увеличения их глубины. Под глубиной ячейки примем максимальное значение глубины своих зависимых ячеек + 1 а ячейка без формулы которая не имеет зависимостей будет иметь глубину 0. Получаем что firstName и lastName будут иметь значение 0, fullName будет иметь значение 1 а label будет иметь значение 2 потому что максимальное значение у подписчиков ( fullName и firstName ) равно 1, делаем +1 получаем 2.

Теперь когда ячейка fistName обновит свое значение она должна вызвать своих подписчиков в порядке увеличение глубины — сначала fullName а потом label . Массив можно сортировать каждый раз при вызове, а можно оптимизиировать и сделать вставку в отсортированный массив в момент добавления новой зависимости.

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

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

В обоих вариантах есть один очень неприметный баг. Формула ячейки label не просто зависит от firstName и fullName — она зависит от них при определенных условиях. Если значение firstName.get().length <= 3 то мы выводим fullName но если значение больше 3 то мы зависим только от firstName . А теперь подумаем что происходит при когда значение firstName меняется с 4 на 3. Ячейка firstName обновит свое значение и должна вызвать подписчиков в порядке глубины — сначала будет вызов fullName который вычислит свое значение а потом вызов label который вычислит свое значение уже имея актуальное значение fullName . На первый взгляд кажется все правильно. Но если подумать то вычисление fullName на самом деле здесь не нужно — потому что значение fistName будет равно 3 а значит когда последним вызовется label ему не потребуется вызвать fullName.get() потому что ветка if просто не выполнится. Причем, в следующий раз, когда потребуется вызвать fullName его значение будет неактуально потому что между его вызовом может сколько угодно раз обновляться lastName. Вот вам и баг с лишним вычислением. В итоге наш алгоритм с вызовом подписчиков в порядке их глубины не работает в общем случае.

Итак существует тот самый "правильный" алгоритм, который ни при каких условиях и хитросплетенных зависимостях не вызовет двойного вычисления ячейки. Для начала приведу код, который по совместительству является почти полноценной версией mobx (за исключением массива и декораторов) всего в 85 строчках кода

А теперь описание:
Пусть ячейка будет иметь три состояния — "actual" (которое значит что значение формулы актуально), "dirty" (которое будет значит что как только вызовется get() ячейка должна пересчитаться) и "check". Теперь как только ячейка изменит свое значение она не будет сразу вызывать вычисление подписчиков в каком-либо порядке а пометит своих подписчиков как "dirty". А те в свою очередь тоже пометят своих подписчиков но только значением "check" а те в свою очередь тоже пометят своих подписчиков значением "check", и так далее рекурсивно до конца. То есть только подписчики той ячеки которая изменилась будут иметь значение "dirty" а все остальные до конца дерева — значение "check", а чтобы при рекурсивном вызове мы не зациклились надо вызывать рекурсию только для тех ячеек которые еще не были помечены (имеют значение "actual").

Дальше при достижении конца дерева — то есть той ячейки у которой больше нет подписчиков и она является "активной" надо добавить такую ячейку в неких глобальный массив PendingCells . "Активной" является ячейка которая представляет не какую-то мемоизированную функцию (значение которой может не понадобиться прямо сейчас) а реакция (например компонент реакта) которая должна запускаться каждый раз когда любая из зависимых ячеек меняет свое значение.

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

Теперь переходим ко второму этапу. Ячейка которая изменилась и вызвала для своих подписчиков рекурсивный процесс, вызывает некую глобальную функцию flush() которая возьмет ячейки которые накопились в глобальном массиве PendingCells и вызовет у них функцию actualize() . Эта функция будет рекурсивной и будет делать вот что — если значение ячейки является "dirty" она вызовет перевычисление своей формулы (а мы помним что значение "dirty" будут иметь только ячейки которые являются прямыми подписчиками ячейки которая изменилась, а все остальные до конца дерева будут иметь значение "check"). Если же значение равно "check", то ячейка попросит свои зависимые ячейки актуализироваться (вызовет метод actualize() ) и после этого снова проверит свое значение и если оно равно "check" то мы меняем значение на "actual" и не вызываем перерасчет, если же оно "dirty" то мы соответственно должны вызвать перевычисление. При этом, проверку на "dirty" нужно после вызова "actualize()" на каждой зависимой ячейке, потому что если ячейка приняла значение "dirty" нет смысла вызывать актуализацию других ячеек и можно сразу прервать цикл и выполнить перерасчет. А то что остальные ячейки не актуализировались, это уже неважно, так как если произойдет обращение к ячейке чтобы получить значение формулы в методе .get() , ячейка должна проверить свое значение и если оно "check" то она должна вызвать этот метод actualize() а если "dirty" то соотвественно выполнить перерасчет. Вот и все, конец алгоритма.

Итак алгоритм на первый взгляд может показаться сложным но он достаточно простой — когда ячейка меняет свое значение у нас всего 2 этапа — первый этап это рекурсивный спуск чтобы пометить как dirty (для первого уровня) и check для всех остальных а второй этап это рекурсивный подъем при котором происходит актуализация значений.

Теперь проясню некоторые неочевидные моменты.

Первое — каким образом происходит избежание того бага с лишним перерасчетом? Это происходит потому что у нас нет жесткого условия вызывать перевычисление зависимых ячеек у ячейки которая изменились. Зависимые ячейки будут помечены как dirty, и все — они вычислятся только когда где-то потребуется узнать их значение. То есть, в примере с багом — ячейка fullName будет просто помечена как "dirty" а потом вычислять ее значение не потребуется так как в label выполнится условие firstName.get().length === 3 и label больше не будет зависеть от fullName .

Второе — почему такое странное действие — внутри метода actualize() — проверить — если значение равно "check" то вызвать actualize() у зависимых ячеек и в процессе этого и также после цикла снова повторно проверить значение и если "dirty" то прервать цикл и вызвать перерасчет а если "check" то сбросить после цикла на "actual" и ничего не делать? Все дело в том что в процессе вызова actualize() у зависимых ячеек некоторые из них могут иметь значение "dirty" и как мы знаем они должны выполнить перерасчет. А при вычислении есть условие — если ячейка поменяла свое значение то она должна пометить своих слушателей как "dirty". И таким образом ячейка которая до этого была "check" может после актуализации своих зависимых ячеек сама изменить значение когда изменится кто-либо из них и соотвественно нужно проверить условие снова. Но только в этом случае если никакие зависимые ячейки не изменили свое значение то значит и самой ячейки смысла вычисляться нет и мы меняем значение с "check" на "actual"

Применение паттерна observer в Redux и Mobx


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

В redux-е этот паттерн применяется без всяких изменений — пакет "react-redux" предоставляет функцию connect которая оборачивает компонент и при вызове componentDidMount вызовет subscribe() метод у Observable , при вызове componentWillUnmount() вызовет unsubscribе() а dispatch() просто вызовет метод trigger() который в цикле вызовет всех слушателей где каждый в свою очередь вызовет mapStateToProps() и потом в зависимости от того изменилось ли значение — вызовет setState() на самом компоненте. Все очень просто, но платой за такую простоту реализации является необходимость работать с состоянием иммутабельно и нормализировать данные а при изменении отдельного объекта или даже одного свойства оповещать абсолютно всех подписчиков-компонентов даже если они никак не зависят от той измененной части состояния и при этом в компоненте-подписчике необходимо явно указывать от каких частей стора он зависит внутри mapStateToProps()

Mobx очень похож на redux тем что использует этот паттерн observer только развивает его еще дальше — что если мы не будем писать mapStateToProps() а сделаем так чтобы компоненты зависели от данных которые они "рендерят" самостоятельно , по отдельности. Вместо того чтобы собирать подписчиков на одном объекте состояния всего приложения, подписчики будут подписываться на каждое отдельное поле в состоянии. Это как если бы для юзера, у которого есть поля firstName и lastName мы создали бы целый redux-стор отдельно для firstName и отдельно для lastName .

Таким образом, если мы найдем легкий способ создавать такие "сторы" и подписываться на них, то mapStateToProps() будет не нужен, потому что эта зависимость от разных частей состояния уже выражается в существовании разных сторов.

Итак на каждое поле у нас будет по отдельному "мини-стору" — объекту observer где кроме subscribe() , unsubscribe() и trigger() добавится еще поле value а также методы get() и set() и при вызове set() подписчики вызовутся только если само значение изменилось.

Вместе с этим требование иммутабельности стора нужно трактовать немного по-другому — если мы в каждом отдельном сторе будем хранить только примитивные значение, то с точки зрения redux нет ничего зазорного в том чтобы вызвать user.firstName.set("NewName") — поскольку строка это иммутабельное значение — то здесь происходит просто установка нового иммутабельного значения стора, точно так же как и в redux. В случаях когда нам нужно сохранить в "мини-сторе" объект или сложные структуры то можно просто вынести их в отдельные "мини-сторы". Например вместо этого

лучше написать так чтобы компоненты могли по отдельности зависеть то от "email" то от "address" и чтобы не было лишних "перерендеров"

Второй момент — можно заметить что с таким подходом мы будем вынуждены на каждый доступ к свойству вызывать метод get() , что добавляет неудобств.

Но эта проблема решается через геттеры и сеттеры javascript-а

А если вы не относитесь негативно к декораторам то этот пример можно еще больше упростить

В общем можно пока подвести итоги и сказать что 1) никакой магии в этом моменте нет — декораторы это всего лишь геттеры и сеттеры 2) геттеры и сеттеры всего лишь считывают и устанавливают root-state в "мини-сторе" а-ля redux

Идем дальше — для того чтобы подключить все это к реакту нужно будет в компоненте подписаться на поля которые в нем выводятся и потом отписаться в componentWillUnmount

Здесь функция connect оборачивает компонент или stateless-component (функцию) реакта и возвращает компонент который благодаря этому механизму автоподписки подписывается на нужные "мини-сторы".

В итоге у нас получился такой вот механизм автоподписок только на нужные данные и оповещений только когда эти данные изменились. Компонент будет обновляться только тогда когда изменились только те "мини-сторы" на которые он подписан. Учитывая, что в реальном приложении, где может быть тысячи этих "мини-сторов", с данным механизмом множественных сторов при изменении одного поля будут обновляться только те компоненты которые находятся в массиве подписчиков на это поле, а вот подходом redux когда мы подписываем все эти тысячи компонентов на один единственный стор, при каждом изменении нужно оповещать в цикле все эти тысячи компонентов (и при этом заставляя программиста вручную описывать от каких частей состояния зависят компоненты внутри mapStateToProps )

Более того этот механизм автоподписок способен улучшить не только redux а и такой паттерн как мемоизацию функций, и заменить библиотеку reselect — вместо того чтобы явно указывать в createSelector() от каких данных зависит наша функция, зависимости будут определяться автоматически точно так же выше сделано с функцией render()

Вывод

Mobx это логичное развитие паттерна observer для решения проблемы "точечных" обновлений компонентов и мемоизации функций. Если немного отрефакторить и вынести код в примере выше из компонента в Observable и вместо вызова .get() и .set() поставить геттеры и сеттеры, то мы почти что получим observable и computed декораторы mobx-а. Почти — потому что у mobx вместо простого вызова в цикле находится более сложный алгоритм вызова подписчиков для того чтобы исключить лишние вызовы computed для ромбовидных зависимостей, но об этом в следующей статье.

Читайте также: