Pull to refresh

Comments 14

А топологическая сортировка для определения порядка и логирование измененных полей в цикле пересчета для уменьшения количества вычислений не проще будет?

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

неактуальным в смысле неверным? Обновление lastName заставит обновиться и fullName, разве нет?
Ячейка label больше не будет зависеть от fullName а fullName так как от него никто не зависит и он не является «активным» обсервером то тоже отпишется от своих зависимостей и больше не будет вычисляться когда будет обновляться lastName
Ячейка label больше не будет зависеть от fullName а fullName так как от ...

Да, верно. Это довольно легко поправить. В cellx можно подсмотреть как.


существует тот самых "правильный" алгоритм, который ни при каких условиях и хитросплетенных зависимостях не вызовет двойного вычисления ячейки

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


let a = new Cell(1);
let b = new Cell(1, () => {
    console.log(123);
    return a.get();
});

b.get();

a.set(5);
a.set(1);

b.get();

console.log здесь сработает дважды, в первой реализации это полностью исправимо, во второй я в конечном счёте не нашёл такого способа (тут нужно понимать, что для конкретно приведённого выше примера исправление делается довольно легко, но дальше и пример можно легко допилить вновь сделав исправление неэффективным и тд.).

Это не те лишние вычисления которые связаны с алгоритмом. В статье я рассматривал ситуации избегания лишних вычислений только при изменении одной ячейки. Если же нужно чтобы несколько изменений одной или разных ячеек накапливались и выполнялся в итоге толко один набор перевычислений то это легко добавить. Тут есть два способа. Первый это написать некий враппер подобно декоратору action mobx внутри которого будем выполнять все операции обновления. При вызове этого враппера устанавливаем глобальных флаг и потом ячейка посмотрит на этот флаг будет вычисляться не сразу а добавится в некий временный массив. А в конце вызова этот враппер снимет глобальных флаг и вызовет обновление накопившихся ячеек. Но у этого варианта есть недостаток — нужно постоянно врапить асинхронные операции и так же он будет работать с async-await. А вот вторым вариантом является установка минимального таймера и отложенный пересчет ячеек после того как произойдет синхронная установка новых значений
Кстати я тут описал общий способ реализации этих двух вариантов но для алгоритма которого я описал в статье (рекурсивный спуск и подъем) реализация будет состоять в добавлении всего нескольких строчек. В примере в статье я убрал для наглядности но в варианте который находится в репозитории уже добавлена реализация второго варианта с таймером. И никаких лишних вычислений у нас снова нет)
В статье я рассматривал ситуации избегания лишних вычислений только при изменении одной ячейки

в примере программист меняет только ячейку [a], да и зачем вообще такое ограничение? Вы вообще не рассчитываете, что программист захочет поменять сразу две ячейки? Ну и вы видимо не поняли природу этого лишнего вычисления, action из mobx здесь не причём, здесь [b] помечается как протухшая, а дальше [a] принимает исходное значение, но [b] по прежнему остался протухшим хотя очевидно, что пересчитывать его нет смысла.

Я решил разбирать сложности по очереди. Если лишние вычисления появляются уже при изменении только одной ячейки то что говорить про множественные изменения? Поэтому в статье и описывается три алгоритма уменьшения вычислений при изменении одной ячейки где только последний («спуск-подъем») обеспечивает наименьшее количество вычислений.

Насчет множественных изменений — нам нужно чтобы при выполнении какого-то обрабочика события мы могли выполнить множественное обновление стора, например поменять много свойств на объекте (или в разных объектах без разницы) но чтобы компонент перерендерился только один раз в конце всех наших действий а не синхронно после каждого изменения поля. action декоратор выполняет это через обертку и флаги, а в репозитории xmob есть пример где все вычисления будут выполняться не синхронно а один раз после таймаута когда все изменения после закончаться.

Насчет примера с когда значение одной ячейки может после нескольких изменений вернуться в первоначальное значение. Тут я согласен, вычисления быть не должно но это наверное настолько редкая ситуация что я пока не могу придумать реалистичного кейса чтобы после нескольких синхронных изменений значение ячейки вернулось в первоначальное состояние. Но даже если такой редкий кейс найдется, то можно легко модифицировать алгоритм так чтобы не было установки флага «dirty» на зависимых ячейках сразу а происходила проверка изменилась ли ячейка уже в процессе актуализации после таймера
это наверное настолько редкая ситуация что я пока не могу придумать реалистичного кейса

со строками конечно редкая, а вот с числами и особенно с boolean вполне случается.


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

вооот!) Это то, что я хотел получить! Правда я думал вы попробуете исправить реализацию в коде, а я придумаю следующий пример ломающий исправление. Ну да ладно, и так норм. Так вот, так не получится :). Во-первых, программист может захотеть прочитать зависимую ячейку до того как сработает таймер, который её "состарит" и она выдаст неактуальное значение, во-вторых, усложняем пример до четырёх ячеек:


let a = new Cell(1);
let b = new Cell(2);
let c = new Cell(() => a.get() + b.get());
let d = new Cell(() => c.get());

а теперь меняем [a] и [b] так что бы [c] не изменился:


a.set(2);
b.set(1);

дальше запускается "состаривающий" проход, он правильно состаривает [c], тк. [a] и [b] изменились и пересчитать (позже, после этого прохода) всё же нужно (алгоритм же не знает, что получится исходное значение), но как ему понять нужно ли состарить [d] не вычисляя пока [c]? А если здесь всё же вычислять [c], то это уже по сути возвращение к первой схеме реализации РП, в которой этой проблемы изначально нет.


Я потратил довольно много времени пытаясь довести вторую реализацию хотя бы до уровня первой, в плане количества подобных фич, в результате получается либо совсем уж фичасто, либо примерно так же, но совсем уж медленно. В результате какой смысл использовать вторую схему если она минимум (в простейшем варианте с бесконечным числом фич) в 5 раз медленнее первой? В 5 Карл!!! Это не на 20% и не на 30%, это на 500% медленнее!

Во-первых, программист может захотеть прочитать зависимую ячейку до того как сработает таймер, который её «состарит» и она выдаст неактуальное значение,

Для этого последнего алгоритма «cпуска-подъем» который описывается в статье нет никакой разницы сработает ли вычисление после таймера или когда программист захочет прочитать зависимую ячейку до того как сработает таймер. При вызове метода .get() если значение не равно «actual» будет точно такой же процесс актуализации зависимостей как и при вызове таймера. И соотвественно вернется всегда только актуальные значение.
дальше запускается «состаривающий» проход, он правильно состаривает [c], тк. [a] и [b] изменились и пересчитать (позже, после этого прохода) всё же нужно (алгоритм же не знает, что получится исходное значение), но как ему понять нужно ли состарить [d] не вычисляя пока [c]?

Если мы имеете ввиду невозможность после двух присваиваний избежать вычисления вообще (потому что ячейка [c] при вычислении все равно не изменит свое значение) то да тут я согласен и даже уверен избежать вычисления [c] в принципе невозможно.
Если же вы спрашиваете про ячейку [d] то тут все просто — она после того как вычислится [c] увидет что ее значение не изменилось и сама не будет вычисляться. (точнее это не она увидит а ячейка [c] не установит ей флаг «dirty» потому что сама не изменила значение, которое потом проверится и «check» заменится на «actual», но это сути не меняет)
А если здесь всё же вычислять [c], то это уже по сути возвращение к первой схеме реализации РП, в которой этой проблемы изначально нет.

Это в какой такой первой схеме реализации РП этой проблемы изначально нет? Нет лишних вычислений только в третьем варианте который описывается в статье (я его называю как «спуск и подъем»)
Да, что-то пошло не так и по описанной схеме лишнего вычисления действительно нет :), давненько я со всем этим разбирался и сейчас видимо так сразу не вспомню, что я там такого хитрого придумывал для лишних вычислений.
В любом случае статья отличная, читается легко, насколько это возможно для подобного материала. Спасибо.

$mol_atom именно так и работает, за одним исключением.


Если же значение равно "check", то ячейка попросит свои зависимые ячейки актуализироваться (вызовет метод actualize()) и после этого снова проверит свое значение и если оно равно "check" то мы меняем значение на "actual" и не вызываем перерасчет, если же оно "dirty" то мы соответственно должны вызвать перевычисление.

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

Действительно, что-то я пропустил этот момент, спасибо за замечание
Sign up to leave a comment.

Articles

Change theme settings