Комментарии 29
Основная задача была в том, чтобы создать инструмент с околонулевым порогом входа, чтобы за штурвал мог уверенно сесть не только опытный пилот, но и вчерашний студент.
Допустим, но что именно в reactive-engine делает порог входа околонулевым? Например по сравнению с reatom — в нем я больше всего разбираюсь. Сейчас я вижу с точки зрения порога входа не то чтобы нуль, но точно выше reatom, а сам reatom, в свою очередь, не совсем даже сопоставим с порогом входа zustand, который является эталоном нулевого входа
Добрый вечер, коллега. Все наши рассуждения субъективны, а следовательно, относительны. Ни в чью сторону не кидаю камни (если это не какой-нибудь динозавр, переживший свое время - хотя, из опыта, в отдельных случаях можно сделать скидку), и не следует забывать про то, что способов решить любую математическую задачу всегда больше одного. В этой статье рассказано про инструмент с упором на производительность и простоту использования. Понимаю вашу логику: если эталон нулевого входа Zustand (где мы просто пишем плоский объект и функции-мутации), то Reatom кажется лаконичным. Отвечая на ваш вопрос, под "околонулевым порогом" в данном случае имелся ввиду полный отказ от функционально-декларативного стиля в пользу классического ООП. Ну и (опять же, субъективно), всегда пробрасывать контекст - по мне, так немного сквозной бойлерлейт, а вызов специальных функций для изменения стейта намекает на иммутабельный подход: в моем случае подход другой (поэтому дальше сравнивать смысла нет, если только не в разрезе производительности - до этого ещё дойдем в будущем).
Резюмирую ответ, в чем лично я вижу удобство ReactiveEngine в контексте нашей дискуссии:
Императивный подход без привыкания к синтаксису библиотеки (что, по мне, так позволяет быстро переключаться с React на Vue и т.д.) - здесь можно углубиться в рассуждениях, к примеру, я считаю, создание микростора должно быть в руках разраба, а не под чужим капотом, но эти вещи тоже можно считать вкусовщиной
Отсутствие сквозного контекста
При удалении инстанса класса сборщик мусора отработает в соответствии с тестами (здесь с Reatom ещё не сравнивал)
Логирование прозрачно (здесь с Reatom ещё не сравнивал)
Сквозного контекста больше нет, это версия двухгодичной давности. Рекомендую покурить свежую доку реатома, где изложен современный синтаксис. Я так понимаю, информация про явный контекст пришла из разбора ИИ. Вот актуальный синтаксис:
const a = atom(0)
const b = atom(1)
const ab = computed(() => a() + b())
effect(() => console.log(ab()))По поводу императивного подхода без привыкания к синтаксису библиотеки — это достаточно непонятный аргумент. Не понятно что такого reactive-engine дает, что привыкать к этому не надо. В этом плане разве что mobx/kr-observable даёт некоторую "нативность", но точно не факт использования классов. Да и классическим ОПП в js классы нельзя назвать, классика — это то что было до классов, то есть функциональный стиль, чему и следует реатом.
3-4 пункты — в реатоме с этим все ок, логгер даже показывает графы причинности изменений (то есть рисует что от чего зависило при изменении атома или от чего вызван экшен)
Таким образом, я не увидел каких-то стойких и хорошо сформулированных аргументов. Может есть еще какие-то поинты?
Давайте по порядку. В ваших утверждениях не все логично. Допустим, с обновленным синтаксисом Реатома - ок, спасибо за уточнение.
Не понятно, что такого reactive-engine дает, что привыкать к этому не надо... точно не факт использования классов
Попробую перефразировать то что скрыто между строк в моем предыдущем ответе ) Классы дают нативную инкапсуляцию свойств и методов. Разработчику не нужно привыкать к функциям библиотеки (atom, computed, action) - и да, в ReactiveEngine есть аналоги, но вы их используете один раз при объявлении свойств класса, дальнейшее взаимодействие происходит на чистом js/ts (императивный, мутабельный подход без "специй", если вы понимаете о чем я).
Классическим ООП в js классы нельзя назвать, классика — это то что было до классов, то есть функциональный стиль
Похоже, вы путаете две вещи: Классику JavaScript - исторический функционально-прототипный стиль языка и классическое ООП - парадигму проектирования на основе классов (как в других языках). Когда говорят "классическое ООП", имеют в виду архитектурный паттерн, а не особенности реализации прототипного наследования под капотом JS образца ES6 (советую называть это синтаксическим сахаром - так вам будет проще отличать одно от другого). JavaScript-классы — это полноценная реализация классического ООП с точки зрения проектирования (инкапсуляция, наследование, полиморфизм).
Аргумент про MobX - ок, без проблем. Здесь тоже есть различия в подходах - можно сравнить их, и размеры бандлов, если очень хочется.
Про относительность суждений и взглядов я уже говорил, если вас все устраивает в том инструменте, к которому вы привыкли - используйте его, если он покрывает ваши задачи.
У меня нет задачи обратить вас в иную веру, или переучить чему-либо кого-то из оппонентов, если что.
Классы дают нативную инкапсуляцию свойств и методов. Разработчику не нужно привыкать к функциям библиотеки (atom, computed, action)
Замыкания тоже дают нативную капсуляцию свойств и методов. Не понимаю преимуществ классового подхода в этом плане. А то что разработчикам нужно привыкать к функциями библиотеки — ну как бы да, это в любой библиотеке надо привыкать, но засчитывать это за порог входа пожалуйста не надо, это плохая метрика измерения порога входа.
и да, в ReactiveEngine есть аналоги, но вы их используете один раз при объявлении свойств класса
в любой библиотеке так происходит, в очередной раз — это нельзя засчитать за критерий. Даже если в одном случае используются нативные геттеры и сеттеры, а в другом явные .set и call operator, какой-то существенной разницы а этом нет для порога входа, разница только в определенных ейджкейсах (например, нативные сеттеры не поддерживают optional chaining при доступе к сеттеру)
классическое ООП - парадигму проектирования на основе классов (как в других языках)
ООП — это не парадигма программирования на основе классов, это ни в каком смысле не валидно, только если не виде вульгарного упрощения. Классы это один из вариантов как ООП может проявляется, но не единственный.
Я по-прежнему не услышал каких-то четких валидных критериев, по которым можно определить, что reactive-engine прост для новичков, поэтому я дам своих тейков почему до простоты далеко:
1) resource — в целом лишний термин и абстракция. Его можно было реализовать как асинхронный компьютед
2) resource и computed на самом деле не ленивые, то есть банально нельзя сделать кверю, которая дернится при первом монтировании компонента без дополнительного погружения в DI систему которая даёт ленивость и которая новичку входу сразу повышает
3) signal vs reactive — лишняя дихотомия, которая создает замешательство "а какой реактивный примитив мне применить к этой задаче?" Между друг другом очевидно они не стыкуются, на reactive нельзя отдельно подписаться, еще есть куча всяких ограничений на нем же. Это очевидное повышение порога входа на ровном месте, когда можно было просто ограничиться одним signal и не создавать второй способ делать одно и тоже. Если это было такое решение проблемы глубокой реактивности — то например реатом это элегантно решает без лишних абстракций за счет поддержки вложенных атомов друг в друга (атомизация)
в целом уже много противоречий и запутанностей, которые ничем не компенсируются и об простом входе речи уже не идет
Я вынужден вас периодически возвращать к тейку про относительность. Привыкать к API нужно везде, тут спора нет. Но одно дело - выучить 3 "декоратора" для разметки класса и дальше писать обычный императивный код, и совсем другое - полностью перестроить мышление на функционально-реактивный конвейер данных. Именно эту разницу в ментальном сдвиге мы и называем "околонулевым порогом".
1) resource — в целом лишний термин и абстракция. Его можно было реализовать как асинхронный компьютед
Вычисляемое свойство обязано быть синхронным и чистым, чтобы гарантировать O(1). (опровергните, плз)
2) resource и computed на самом деле не ленивые, то есть банально нельзя сделать кверю, которая дернится при первом монтировании компонента без дополнительного погружения в DI систему которая даёт ленивость
Здесь вообще мимо. В коде ядра все computed и resource ленивые изначально (опровергните, плз).
3) signal vs reactive — лишняя дихотомия, которая создает замешательство "а какой реактивный примитив мне применить к этой задаче?
Метод reactive - это просто синтаксический сахар, который под капотом пробегается по объекту и превращает его свойства в те же самые сигналы (опровергните, плз).
Между друг другом очевидно они не стыкуются, на reactive нельзя отдельно подписаться, еще есть куча всяких ограничений на нем же
Вы много придумали невероятных фактов, из-за которых пакет никогда бы не публиковался, посмотрите код ещё раз, плз. В этот раз чёт все мимо )
Вы чуть выше сказали, что не видите стойких аргументов (напомню про относительность суждений), а я, в свою очередь, не вижу с вашей стороны понимания, как он работает (тут ничего страшного нет, я иного не ожидал в первый день).
Вычисляемое свойство обязано быть синхронным и чистым, чтобы гарантировать O(1). (опровергните, плз)
ну во-первых, чистым быть не обязана на мой взгляд, это просто популярная условность. Во-вторых, если компьютед возвращает промис, а не какое-то другое значение, то это не делает вычисление синхронным - создание промиса синхронно. В-третьих, как тут O(1) вообще связано с обсуждаемым вопросом? Если в компьютеде выполняется вычисление с вложенным циклом, как reactive-engine гарантирует O(1)?
Здесь вообще мимо. В коде ядра все computed и resource ленивые изначально (опровергните, плз).
тесты говорят обратное:
const x = e.signal(0); let calls = 0
e.computed(() => { calls++; return x.value })
x.value = 1; await tick(); x.value = 2; await tick()
console.log('computed calls w/o readers:', calls) // 3const id = e.signal(1); const fetched = []
e.resource(async v => { fetched.push(v); return v }, id)
id.value = 2; await tick()
console.log('resource fetches w/o readers:', fetched) // [1, 2]то есть, компьютед/ресурс даже не прочитали еще, а он уже может перевычислится, а ресурс еще может и сайдэффект запустить раньше времени, как его прочитают. То есть тут даже ленивый DI на самом деле не поможет.
Метод reactive - это просто синтаксический сахар, который под капотом пробегается по объекту и превращает его свойства в те же самые сигналы (опровергните, плз).
по сути автор должен знать свой же код, верно? "те же сигналы" там не создаются, это отдельная сущность, совместимая с сигналами из движка. Следовательно, синтаксическим сахаром быть это и не может, ну просто другая семантика под капотом, даже если и похоже на те же сигналы.
Пример в отличии семантики:
const state = engine.reactive({ count: 0 })
const stop = engine.effect(() => {
console.log('effect', state.count) // effect 0
})
stop()
state.count = 1 // effect 1
state.count = 2 // effect 2подписки текут после остановки эффекта, а вот с сигналами бы это уже не прошло (тоже проверял - предлагаю написать тесты в ядре на это поведение)
Вы много придумали невероятных фактов, из-за которых пакет никогда бы не публиковался, посмотрите код ещё раз, плз.
reactive({}).subscribe
// Property 'subscribe' does not exist on type '{}'отдельно подписаться - нельзя =), напрямую заюзать через engine.use нельзя (нужен прокси в виде компьютеда как мост, который наверное тоже никак на порог входа не влияет и бойлерплейт не добавляет), всякие .push тоже не работают (во vue/mobx в тоже время работает), .length не отслеживается если массив растет. То есть ограничения все же есть. Но при этом да, признаю, сигналы и реактивны норм стыкуются, перепроверил.
Кстати, предлагаю сюда податься https://github.com/johnsoncodehk/reactive-framework-test-suite чтобы пофиксить все проблемы в реактивном ядре, заодно будет интересно и на результаты глянуть
как тут O(1) вообще связано с обсуждаемым вопросом? Если в компьютеде выполняется вычисление с вложенным циклом, как reactive-engine гарантирует O(1)?
В очередной раз возвращаю вас к тейку про относительность. Разумеется, никакой магии не существует, но здесь нужно отличать сложность функции и сложность реактивного кэша (второй раз ловлю вас за подменой понятий). Когда в реактивных системах говорят про O(1), имеют в виду сложность доступа к кэшированному значению (чтение) - если компонент или другой метод читает этот компьютед 100 раз подряд, то тяжелый вложенный цикл выполнится ровно 1 раз. Все остальные 99 раз геттер Ядра за O(1) вернет готовую переменную из памяти. Движок гарантирует константное время доступа, избавляя приложение от холостых повторных вычислений.
> то есть, компьютед/ресурс даже не прочитали еще, а он уже может перевычислится, а ресурс еще может и сайдэффект запустить раньше времени, как его прочитают. То есть тут даже ленивый DI на самом деле не поможет.
Окей. С этим соглашусь. Термин "ленивый" здесь применим только к кэшу компьютеда при повторных чтениях из внешнего кода. Думаю, "жадный" ресурс - это неплохо для ускорения загрузки данных и предотвращения задержек отображения данных в UI. Здесь криминала пока не вижу.
подписки текут после остановки эффекта, а вот с сигналами бы это уже не прошло (тоже проверял - предлагаю написать тесты в ядре на это поведение)
твой пример выглядел максимально убедительно, и я был уверен, что Proxy-ловушки сдадутся и подписки "потекут". Но я взял твой кейс, добавил тест:
И знаешь что? Тест успешно прошел. Подписки НЕ текут, и эффект НЕ воскресает.
Да, Proxy оперирует своими изолированными мапами. Но за счет сквозного контроля allEffects на уровне планировщика ядра, удалось сделать эту систему абсолютно безопасной от утечек при остановке эффектов! Пока видится, что код работает как швейцарские часы.
it('должен гарантированно аннулировать подписку на reactive объект после остановки эффекта и не воскресать при мутациях', async () => {
const engine = new ReactiveEngine()
const state = engine.reactive({ count: 0 })
const spy = vi.fn()
const stop = engine.effect(() => {
spy(state.count)
})
expect(spy).toHaveBeenCalledTimes(1)
spy.mockClear()
// Останавливаем эффект
stop()
// Мутируем свойство Proxy
state.count = 1
await new Promise<void>((r) => queueMicrotask(r))
// ТЕСТ ДОЛЖЕН ПОКАЗАТЬ, ЧТО ЭФФЕКТ БОЛЬШЕ НЕ ВЫЗЫВАЕТСЯ
expect(spy).not.toHaveBeenCalled()
expect(spy).toHaveBeenCalledTimes(0)
})Ах, да, возможно, из-за того, что я накатил фикс Ядра 1.6.0 сегодня ночью...
отдельно подписаться - нельзя =), напрямую заюзать через
engine.useнельзя (нужен прокси в виде компьютеда как мост, который наверное тоже никак на порог входа не влияет и бойлерплейт не добавляет), всякие.pushтоже не работают (во vue/mobx в тоже время работает),.lengthне отслеживается если массив растет. То есть ограничения все же есть. Но при этом да, признаю, сигналы и реактивны норм стыкуются, перепроверил.
Здесь - Ок, проверил, согласен. Да, пока ограничения на массивы из коробки есть: если ты делаешь .push(), движок не сгенерирует автоматический автобатчинг по длине массива.
Про массивы - Ок, тут есть над чем поработать (все проверил, без комментариев), текущий статус - чтоб не раздувать либу есть существенная оговорка в работе с массивами state.tags = [...state.tags, 'js']) - будет актуально для версий v1.6.x. Пока на паузе этот момент ⏸️.
В соответствующих чатиках анонсируется лай...
Да, по итогам фикса метода use добавил пару тестов.
Разумеется, никакой магии не существует, но здесь нужно отличать сложность функции и сложность реактивного кэша
ну окей, я подозревал что может иметься ввиду и это, но какая разница тогда для сложности доступа, возвращается там промис или не промис? Асинхронность не накладывает ограничения на сложность доступа по кешу. Надеюсь теперь стало понятнее
Термин "ленивый" здесь применим только к кэшу компьютеда при повторных чтениях из внешнего кода
а это не мемоизация вычислений называется? =) Чувствую подмену понятий. Может тогда стоит в статье и доках исправить, что они не ленивые а мемоизируют вычисления?
Думаю, "жадный" ресурс - это неплохо для ускорения загрузки данных и предотвращения задержек отображения данных в UI. Здесь криминала пока не вижу.
криминал тут в том, что этим нельзя управлять. Будут производить лишние вычисления, которые никому не нужны, будут происходить сетевые запросы столько сколько меняются зависимости, хотя ресурс никому сейчас не нужен. Но если тебе это не нужно — ты это не можешь отключить. Плохо
Кстати по поводу дебаунса — зачем была введена лишняя сущность в виде withDebounce? Вообще мне нравится как куча расширений создаёт пирамиду, где на вершине сверкает исполняемый код ресурса =) Для порога входа это имеет значение — если ресурсы конкурентный по-умолчанию, то тогда дебаунс можно было бы легко реализовать через отменяемый await sleep() перед самим запросом вместо лишний расширений и вложенностей под это. Аналогично может быть решен и троттлинг, просто правила отмены ресурса должны конфигурироваться в нем самом опциями
Так, на связи "реактив ньюз". По последним сводкам больной в коме, но вроде, больше жив, чем мертв (или наоборот - не важно), для меня важно, что мне удалось поучаствовать в этой вечеринке. Буду держать в курсе )
Спасибо за ссылку, для меня это интрига года, вот предварительные результаты: https://pravosleva.pro/p/_no-index.reactive-engine-news-1.7.1-reactive-framework-test-suite
Пакет в процессе перестраивания и рефакторинга, спасибо что обозначили ошибки. Все будет обновлено в 1.9.х
Ну очень много маркетинга и ложных обещаний при довольно банальной реализации… Посмотрел код репо, тестов очень мало, но даже при чтении наискосок много всего видно.
“Fine-grained reactivity”, “импульс изменений ровно в те узлы DOM” - нет, ререндеры Реакта через setState либо useSyncExternalStore, это ререндер всего компонента, а не конкретных узлов как в preact-signals например (но они это делают очень костыльно, кладут интерфейс реактового компонента в прототип сигнала, то есть по факту создают в дереве новые компоненты)
На каждую переменную делать
engine.use- выглядит совсем не удобно, представьте компонент-портянку с сотней таких ручных подписок.computed обновляется асинхронно через микротаску, и если не делать await Promise.resolve() после изменения то можно получить старые данные. Кейс редкий конечно, но очень внезапный, особенно для тех, кто работал со взрослыми системами реактивности
unsubscribeEffect = this.effect(…)- судя по всему computed из-за этого не ленивый, то есть пересчитывается даже если никто не читает. Могу ошибаться, читаю наискосок, но подозрительно.В динамических подписках типа
effect(() => Math.random() > 0.5 ? store.a : store.b)не факт что очистится лишняяЕще сомнительно выглядит батчинг в плане оптимизации, трекинг изменений в массивах / Map / Set тоже под вопросом и т.п.
На просторах интернета сотни, если не тысячи подобных библиотек, и полезны скорее для саморазвития. Не понятно, зачем еще одна.
Ну очень много маркетинга и ложных обещаний
Ничего из этого, но спасибо. Отвечу по пунктам (возможно, не за раз)
Поинт 1. Это Аргумент. Но задача движка разгрузить UI-слой за счёт: 1) изоляции графа зависимостей от React/etc/whatever чтобы выжать из него максимум. И если мы про React - то цель достигнута. На это есть намек в самом начале статьи, 2) Если у вас изменилось некое вычисляемое свойство в бизнес-логике, граф пересчитает его за O(1). React-компонент, который отображает это свойство, стриггерится через useSyncExternalStore и ререндерится. Но все остальные компоненты приложения, которые не подписаны на этот конкретный узел, не ререндерятся.
Поинт 2.
Про "портянку хуков" - разумеется, так делать не нужно. Попробуйте метод reactive (и это только один из вариантов)
⛔ Не для Vue!
В статье вот описан const counter = engine.use(logic.counter) , и тут еще один нюанс - например если в jsx будет <div>{props.data === 1 ? counter : null}</div> то компонент все равно будет ререндериться при изменении counter в сторе, т.к. уже подписан ручным хуком, верно? То есть в этом случае не "выжимается максимум", а наоборот - снижается перф, создаются лишние ререндеры (возможно - всего дерева компонентов, если это где-нибудь в App).
Для чего вообще use, если с ним нужно настолько осторожно обращаться, чтобы себе в ногу не выстрелить. Оставили бы как в mobx или kr-observable только внешний враппер вообще без хуков и ручных подписок.
А вы прям с лопатой )) Да, если хук вызван, компонент перерисуется в любом случае. Но помилуйте, милостивый государь, пример из статьи с engine.use(logic.counter) - это просто демонстрация базового API для примитивов. Метод use умеет принимать объект или инстанс класса целиком, возвращая Proxy.
const state = engine.use(myStore)
return (
<div>
{props.data === 1
? state.counter
: null}
</div>
)Ох тыж, тут кодить можно.
Собсна, вот где фича:
Если
props.data !== 1React выполняет веткуnull. Геттерstate.counterне вызываетсяДвижок видит, что свойство
counterне было прочитано в текущем кадре рендера, и НЕ подписывает компонент на измененияcounterЕсли
counterизменится в сторе, компонент НЕ будет ререндериться. Лишнего ререндера (тем более всего дерева App) не произойдет! Максимум перформанса выжимается автоматически
Про MobX - отдельная тема. Чуть позже прокомментирую.
Комментируя подход MobX, я должен упомянуть фрагмент из официальной доки React, в котором говорится, что useSyncExternalStore - ничто иное, как официальный стандарт подписки на внешнее состояние, и что он, мол, типа, гарантирует 100% предсказуемость поведения аппки в ряде бытовых условий. Я, кстати, пробовал создать аналогичный экспериментальный "магический враппер" который, кстати, даже работал, но в итоге отказался от этого. Вы спросите, "Почему?" Строго говоря, потому что в задачу движка изначально это поведение не входило. Ну и увеличение бандла в x10, думаю, того не стоит (это я пока просто предполагаю, есть основания).
The current snapshot of the store which you can use in your rendering logic.
Но, кстати заметить, даже с этим производительность интерфейса существенно повысилась.
Поинт 3. Здесь вы ошибаетесь. Библиотека совмещает синхронный ленивый сброс кэша и асинхронный батчинг эффектов UI. Как буду у компа, под этим комментом оставлю пару ссылок на конкретные строки в исходниках.
Асинхронно через микротаски склеиваются только триггеры на ререндер React-компонентов. Это нужно, чтобы UI не дрожал, если мы мутируем 10 свойств подряд.
Для computed мгновенный синхронный сброс кэша - в наличии 👉 Значение пересчитывается синхронно в момент обращения на следующей строчке кода, так что никакого await Promise.resolve() не требуется.
Поинт 4. Ваш тейк видится мне таким: раз внутри computed создается this.effect, а эффекты по определению являются "жадными" (выполняются сразу при изменении зависимостей), то кажется, что ленивость ломается.
Но нет. В этом месте используется хитрый гибридный финт, который сохраняет ленивость для конечного пользователя, но автоматизирует уведомления для графа.
Поясню, почему computed остается ленивым:
const unsubscribeEffect = this.effect(() => {
cachedValue = fn()
isDirty = false
// ...
(sig as any).value = cachedValue // Значение пушится во внутренний сигнал ядра
}, `[CORE_INTERNAL_EFFECT=1]:${name}`)
геттер value, через который разработчик и React-компоненты взаимодействуют с этим computed:
get value() {
if (isDirty) {
cachedValue = fn()
isDirty = false;
(sig as any).value = cachedValue
}
return sig.value // Возвращается значение ВНУТРЕННЕГО СИГНАЛА ЯДРА
}
В чем вы правы: граф зависимостей обновляет внутреннее состояние sig.value при изменении родительских сигналов. Но для внешнего кода этот computed ведет себя как абсолютно ленивая сущность с O(1) доступом.
Когда компонент или метод обращается к myComputed.value, функция fn() НЕ вычисляется заново. Движок просто возвращает уже готовое, посчитанное значение из внутреннего сигнала sig.value за О(1).
Защита от холостых вычислений заключается в следующем: Проверка if (isDirty) в геттере как раз гарантирует, что если к вычисляемому свойству обращаются вручную вне реактивного контекста фреймворка (и внутренний эффект еще не успел обновить сигнал), движок мгновенно выполнит ленивый расчет по требованию и отдаст свежие данные.
Зачем тогда нужен this.effect внутри (спросите вы)? Он необходим для автоматического связывания цепочек (эффект домино).
Если у нас есть цепочка:
Signal A -> Computed B -> Computed CТо благодаря внутреннему эффекту внутри Computed B изменение Signal A мгновенно инвалидирует (помечает грязным) внутренний сигнал B (и это можно выявить, включив логи) что автоматически разбудит и подготовит к пересчету Computed C. Включить можно не только "обычные" логи, но и "внутренние" (дополнительные) логи Ядра.
Так что здесь Push-механика на уровне Ядра завязана с O(1) кэшированием для внешнего кода.
тестов очень мало
При такой "банальной реализации", тесты покрывают 100% функционала ) или есть какая-то конкретика? Готов обсудить.
Поинт 5. Благодарю, есть такой баг. Добавил тест, добавил в очередь. Скоро накатим обновление.
Основная задача была в том, чтобы создать инструмент с околонулевым порогом входа
И сделали inject, use, signal (на каждое свойство)…
Посмотрите, для примера, kr-observable, где действительно околонулевой порог входа. Достаточно знать JavaScript
Есть одна деталь. kr-observable позволяет сделать объект наблюдаемым. Но, насколько я понял, он не умеет из коробки в транзакции сложной асинхронщины (или?). Лично мне зачастую нужны: встроенная отмена прошлых сетевых запросов при смене исходного сигнала, обработка таймаутов, повторные попытки при сбое сети, и ленивый пересчет зависимых данных с кэшированием (поправьте меня, если это все включено). Да, в итоге порог входа чуть выше, чем у простой Proxy читалки, но он околонулевой для задач такого уровня сложности
И ещё одна деталь:
ReactiveEngine не заставляет вас везде городить классы и декораторы. Если задача тривиальная - берете метод reactive и пишете в стиле kr-observable на чистом JS. Но если приложение растет и требует сложной архитектуры, DI-системы и предсказуемых цепочек вычислений - просто переходите на классы. В этом и фишка: порог входа гибкий и подстраивается под сложность задачи - и это не проектировалось специально, а является следствием "реактивности" на чистом JS. Кмк, движок даёт гибкость, которой у kr-observable нет.

Как React учился батчингу и как научить его «летать»?