Архитектура и реактивное программирование - достаточно абстрактная статья, а я хочу вам сейчас рассказать один очень простой и очень эффективный паттерн применения реактивности. Вывел его только что рефача код девтулов.
Домен: есть список логов и фильтры к ним. Каждый лог - отдельная сущность (модель \ компонент), которая внутри себя вычисляет матч на фильтры для определения своей видимости.
Нужно добавить фичу - применение фильтра до добавления лога в список, что бы не раздувать этот самый список. Т.е. сейчас фильтры просто скрывают логи из отображения, но не удаляют из списка. А нужна возможность удалять логи по фильтру или не пускать логи в список вовсе, что бы быстро не упираться в лимит размера списка (у меня на работе есть страницы которые выстреливают пол тыщи логов только на маунте).
Соответственно, нужно взять код применения фильтра из модельки лога и засунуть его в модельку списка логов и чекать, если матча нет - лог не создавать.
Я по старой привычке сделал самый простой и тупой способ - вынес функцию
Я посмотрел на свой код и обнаружил несколько подобных общих функций для разных случаях и понял что они вносят сложность, которую можно было бы избежать.
Проблемы с отдельными функциями:
1. Выделенные функции не имеют должного доступа ко всем параметрам или состояниям их применения в замыкании, все зависимости нужно явно описывать и пробрасывать - не люблю этот бойлерплейт.
2. Т.к. функция лежит отдельно от кода сущности сходу может быть не ясно за что именно она отвечает. Буквально, согласитесь чтоне хочется в целом норм, но можно лучше (читай дальше).
3. Они не явно связаны с местом их применения и можно легко о них забыть (не знать другому разработчику), что в будущем приведет к дублировании их логики, или наоборот, забыть удалить после каого-то рефакторинга. Это главная проблема.
Как же все это решать?Можно вернуться к ООП, сделать функцию методом релевантного объекта. Т.е. у объекта Log будет метод isMatch. Еще нужно не забыть этот метод замемоизировать, в моем случае это было действительно важно.
И вот что я подумал. А как так вышло что я сначала написал инлайн процедуру, потом вспомогательную утилиту, а потом захотел ее переместить в метод? Откуда столько приседаний? Да, можно сказать что это нормальная эволюция кода, но я знаю что все могло бы быть еще проще. Если бы я придерживался классической объектно реактивной архитектуры с реатомом (релевантно для любых сигнальных либ, в том числе mobx).
А паттерн очень прост - чистые вычисления выносим в атом (компьютед стейт), который создаем в замыкании со всем зависимым кодом. Этот атом является свойством Log. Этоо классический пример атомизации. Теперь у нас есть публично доступный стейт лога для проверки перед добавлением в список логов и он уже замемоизирован. Вишенка на торте - он отображается в инспекторе состояний, причем я даже могу посмотреть от чего он менялся.
Может быть это все звучит просто, но я вот на ровном месте затупил и сходу не правильно дизайнил код. Это именно чувствуется, нужно многое отрефакторить, очень быстро мой наивный классический процедурно-функциональный подход сделал мой код малоподвижным и сложным для анализа.
Самое главное - писать сразу в атомизированном стиле ничего не стоит, это очень простой паттерн, который сходу очень положительно влияет на архитектуру кода..
#ReactiveArchitecture
Домен: есть список логов и фильтры к ним. Каждый лог - отдельная сущность (модель \ компонент), которая внутри себя вычисляет матч на фильтры для определения своей видимости.
Нужно добавить фичу - применение фильтра до добавления лога в список, что бы не раздувать этот самый список. Т.е. сейчас фильтры просто скрывают логи из отображения, но не удаляют из списка. А нужна возможность удалять логи по фильтру или не пускать логи в список вовсе, что бы быстро не упираться в лимит размера списка (у меня на работе есть страницы которые выстреливают пол тыщи логов только на маунте).
Соответственно, нужно взять код применения фильтра из модельки лога и засунуть его в модельку списка логов и чекать, если матча нет - лог не создавать.
Я по старой привычке сделал самый простой и тупой способ - вынес функцию
isMatch
и переиспользовал ее в списке. Просто и тупо, работает как часы. Но что-то в голове у меня щелкнуло...Я посмотрел на свой код и обнаружил несколько подобных общих функций для разных случаях и понял что они вносят сложность, которую можно было бы избежать.
Проблемы с отдельными функциями:
1. Выделенные функции не имеют должного доступа ко всем параметрам или состояниям их применения в замыкании, все зависимости нужно явно описывать и пробрасывать - не люблю этот бойлерплейт.
2. Т.к. функция лежит отдельно от кода сущности сходу может быть не ясно за что именно она отвечает. Буквально, согласитесь что
isMatch
совсем не голословна, но и писать в жава стиле, "isLogMatchFilter" 3. Они не явно связаны с местом их применения и можно легко о них забыть (не знать другому разработчику), что в будущем приведет к дублировании их логики, или наоборот, забыть удалить после каого-то рефакторинга. Это главная проблема.
Как же все это решать?
И вот что я подумал. А как так вышло что я сначала написал инлайн процедуру, потом вспомогательную утилиту, а потом захотел ее переместить в метод? Откуда столько приседаний? Да, можно сказать что это нормальная эволюция кода, но я знаю что все могло бы быть еще проще. Если бы я придерживался классической объектно реактивной архитектуры с реатомом (релевантно для любых сигнальных либ, в том числе mobx).
А паттерн очень прост - чистые вычисления выносим в атом (компьютед стейт), который создаем в замыкании со всем зависимым кодом. Этот атом является свойством Log. Этоо классический пример атомизации. Теперь у нас есть публично доступный стейт лога для проверки перед добавлением в список логов и он уже замемоизирован. Вишенка на торте - он отображается в инспекторе состояний, причем я даже могу посмотреть от чего он менялся.
Может быть это все звучит просто, но я вот на ровном месте затупил и сходу не правильно дизайнил код. Это именно чувствуется, нужно многое отрефакторить, очень быстро мой наивный классический процедурно-функциональный подход сделал мой код малоподвижным и сложным для анализа.
Самое главное - писать сразу в атомизированном стиле ничего не стоит, это очень простой паттерн, который сходу очень положительно влияет на архитектуру кода..
#ReactiveArchitecture
Хабр
Архитектура и реактивное программирование
Что такое реактивное программирование? Не Rx. И даже не Excel. Это архитектурный паттерн, позволяющий абсолютно иначе писать код. В статье мы устаканим фундаментальные знания, утвердимся в том, что...
const { then } = Promise.prototype
Promise.prototype.then = function () {
console.log('then')
return then.apply(this, arguments)
}
const thenable = n => ({
then(cb) {
console.log('thenable')
cb(n)
}
})
const test = async (n) => n
main()
async function main() {
await test(1).then(console.log)
console.log(await thenable(2))
console.log(await test(3))
}
log:
1
thenable
2
3
В next.js все это время можно было на изи скипнуть мидлвары, срочно обновитесь, если у вас есть в них логика (авторизация, пейвол и т.п.).
https://security.snyk.io/vuln/SNYK-JS-NEXT-9508709
https://security.snyk.io/vuln/SNYK-JS-NEXT-9508709
Learn more about npm with Snyk Open Source Vulnerability Database
Improper Authorization in next | CVE-2025-29927 | Snyk
Critical severity (9.3) Improper Authorization in next | CVE-2025-29927
Please open Telegram to view this post
VIEW IN TELEGRAM
Следующая версия реатома будет содержать так много наикрутейших фич...
Например, у нас будут глобальные
Я уже добавил себе набор хелперов поверх vitest:
1) В каждый атом добавляю
2) Врапаю каждый тест автоматически с уникальным контекстом, что бы гарантировать отсутствия влияния тестов друг на друга. Иначе говоря, прощай before\after Each и привет надежное распараллеливание тестов.
DX просто бомба 😎
Например, у нас будут глобальные
MIDDLEWARES
, которые могут использоваться для добавления: логирования, автоматических биндингов (привет, vue ref.value), упрощения тестирования!Я уже добавил себе набор хелперов поверх vitest:
1) В каждый атом добавляю
testing.subscribe
который возвращает Mock
функцию с соответствующими методами.2) Врапаю каждый тест автоматически с уникальным контекстом, что бы гарантировать отсутствия влияния тестов друг на друга. Иначе говоря, прощай before\after Each и привет надежное распараллеливание тестов.
DX просто бомба 😎
50$ криптой тому господину, который запилит самую крутую демосцену на тему https://reatom.dev/favicon.svg
https://github.com/artalar/reatom/issues/1071
Условия:
- (UPD с комментариев) это не оферта, а предложение поконтрибьютить по приколу и может быть получить небольшой приятный приз
- она должна мне понравится на столько что бы ее можно было сунуть куда-то на лендинг (сайта реатома)
- музыка не обязательна
- максимум в пару десятков КБ
- до конца недели
- перевод в сетке эфира
Что такое демосцена? Это аудиовизуальная абстракция или история сделанная с помощью кода! Преимущественно микроскопического размера. Можно потыкать тут https://nanogems.demozoo.org/. Крутой пример: https://youtu.be/4r1732c04aY
https://github.com/artalar/reatom/issues/1071
Условия:
- (UPD с комментариев) это не оферта, а предложение поконтрибьютить по приколу и может быть получить небольшой приятный приз
- она должна мне понравится на столько что бы ее можно было сунуть куда-то на лендинг (сайта реатома)
- музыка не обязательна
- максимум в пару десятков КБ
- до конца недели
- перевод в сетке эфира
Что такое демосцена? Это аудиовизуальная абстракция или история сделанная с помощью кода! Преимущественно микроскопического размера. Можно потыкать тут https://nanogems.demozoo.org/. Крутой пример: https://youtu.be/4r1732c04aY
YouTube
림프닌자 - 변신 로봇 3 (limp ninja - transformer 3)
Limp Ninja's demo for Revision 2019 and invitation for Evoke 2019.
code / design: bruce
design: langford
music: variafx
code / design: bruce
design: langford
music: variafx
Forwarded from Трудно быть Коротаевым
Reatom universe
Сделал целую вселенную из крутящихся частей логотипа Reatom, которые собираются буквально из хаоса. Получилась аналогия, что стейт менджер приводит все в порядок)
Одна беда, не могу понять какой вариант лучше
https://codepen.io/lekzd/pen/wBvExKo
Сделал целую вселенную из крутящихся частей логотипа Reatom, которые собираются буквально из хаоса. Получилась аналогия, что стейт менджер приводит все в порядок)
Одна беда, не могу понять какой вариант лучше
https://codepen.io/lekzd/pen/wBvExKo
Please open Telegram to view this post
VIEW IN TELEGRAM
YouTube
Артём Арутюнян — История реактивности
Ближайшая конференция: HolyJS 2025 Autumn, 20—21 ноября, Санкт-Петербург + online. Подробности и билеты: https://jrg.su/EM4wwV
— —
Десять лет назад подъем React сделал observables антипаттерном, а сейчас signals внедряют в стандарт JavaScript. Как так получилось?…
— —
Десять лет назад подъем React сделал observables антипаттерном, а сейчас signals внедряют в стандарт JavaScript. Как так получилось?…
Как быстрее очистить массив, обнулив его или пересоздав? Например,
Спасибо @cevek, он предоставил тесты и их объяснение:
Еще стоит отметить при работе с массивами, что старая ссылка может уже существовать в старом поколении памяти и добавление туда новых элементов из, соответственно, нового поколения может увеличить прыжки по памяти и замедлить выполнение.
Иначе говоря, пересоздание массива быстрее сразу по паре причин, да и в своем коде с этим удобнее и безопаснее работать.
queue.length = 0
или queue = []
?Спасибо @cevek, он предоставил тесты и их объяснение:
array.length = 0
не просто удаляет элементы, а изменяет capacity массива, т.е. дефрагментирует выделенный под него кусок памяти. Важно, что происходит это синхронно, т.к. ссылка на массив меняться не должна и сразу для всего пользовательского кода должна выдавать новый вариант массива. А при array = []
старый массив может быть обработан сборщиком мусора асинхронно, а может даже и параллельно.Еще стоит отметить при работе с массивами, что старая ссылка может уже существовать в старом поколении памяти и добавление туда новых элементов из, соответственно, нового поколения может увеличить прыжки по памяти и замедлить выполнение.
Иначе говоря, пересоздание массива быстрее сразу по паре причин, да и в своем коде с этим удобнее и безопаснее работать.
setTimeout(() => main(true))
setTimeout(() => main(false))
function main(immutable) {
const size = 1e5;
const arr = Array.from({ length: size }, () => ({}));
const time = [];
const gctime = [];
global.gc();
for (let i = 0; i < 20; i++) {
const arrs = [];
for (let j = 0; j < 1000; j++) {
arrs.push(arr.slice());
}
const start = process.hrtime.bigint();
for (let j = 0; j < arrs.length; j++) {
if (immutable) arrs[j] = [];
else arrs[j].length = 0;
}
const end = process.hrtime.bigint();
time.push(Number(end - start) / arrs.length);
global.gc();
gctime.push(Number(process.hrtime.bigint() - end) / arrs.length);
}
console.log(immutable ? 'Immutable' : 'Length');
console.log('script:',Math.min(...time));
console.log('gc:', Math.min(...gctime));
}
> node --expose-gc test.js
Immutable
script: 20.2
gc: 28870.873
Length
script: 522.224
gc: 28798.743
Следующая версия реатома практически готова и это будет самая простая и самая эффективная (фичастая) библиотека для работы с сотсояниями и эффектами.
И это НЕ шутка на первое апреля 🙂
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
artalog
Опчк, опубликовали 🤩 https://youtu.be/evt4KVG8BbQ
В докладе я упоминал про "пять виртуальных очередей", помимо тасков и микротасков, и в комментариях люди начали этим интересоваться. Рассказываю.
Пять самых явных описаны в 8.1.7.4 Generic task sources. Хотя, скорее всего, их больше.
Самое главное в этом то что приоритет обработки разных task source в спецификации не описан, а остается на усмотрение UA (User Agent, т.е. среда выполнения, т.е. браузер).
Накидал простенькие тесты для наглядности: https://stackblitz.com/edit/task-sources-test?file=index.html
Пять самых явных описаны в 8.1.7.4 Generic task sources. Хотя, скорее всего, их больше.
Самое главное в этом то что приоритет обработки разных task source в спецификации не описан, а остается на усмотрение UA (User Agent, т.е. среда выполнения, т.е. браузер).
Накидал простенькие тесты для наглядности: https://stackblitz.com/edit/task-sources-test?file=index.html
Forwarded from Roman Dvornov
Вот так и рождаются мифы о том как работают JS движки 🙂
> array.length = 0 не просто удаляет элементы, а изменяет capacity массива, т.е. дефрагментирует выделенный под него кусок памяти. Важно, что происходит это синхронно, т.к. ссылка на массив меняться не должна и сразу для всего пользовательского кода должна выдавать новый вариант массива.
Начнем с того, что выделенные фрагменты памяти не дефрагментируются, максимум помечаются как более не используемые, но и этого не делается. Так "используемость" определяется по наличию ссылок на фрагмент, в зону отвественности объекта (массив тоже объект) входит только задача убрать ссылки. Признание фрагмента неиспользуемым (мертвым) - это задача GC, который проверяет количество ссылок на объект (фрагмент памяти).
Дефрагментация про другое – это процесс переукладки используемых фрагментов памяти более компактно, обычно в начало спейса, так чтобы свободная часть была представлена одним фрагментом. Процесс не применяет к объектам точечно, но применяется к области памяти (спейсу). В V8 этот процесс называется compat и является частью Major GC, одним из финальных его этапов. Так как процесс недешевый (нужно не только перенести много памяти с одного места в другое, но и обновить все ссылки, то есть прописать новые адреса), то он выполняется не на каждый Major GC, а только когда в этом действительно есть необходимость. При этом, "дефрагментация" осуществляется не для всех спейсов, в основном для старого поколения, и для некоторых других (например, large object space). Для молодого поколения необходимости в дефрагментации нет, так как когда заканчивается память в этом спейсе, то срабатывает Minor GC – из него эвакуируются выжившие объекты (переносятся во вторую молодую или в старую), а сам спейс зачищается и начинает наполняться заново, объектами которые создаются.
Массив это не монолит - он представляет из себя несколько объектов. "Входной точкой" является объект JS Array (типичный JS объект), под капотом опционально elements (массив значений фиксированной длины - fixed array) и другие объекты (например, для хранения собственных свойств, когда делается array.foo = 1). У массива действительно есть capacity, а именно свойство хранящее число используемых слотов в elements, так как elements может быть больше чем используется (обычно выделяется с запасом, чтобы не пересоздавать elements на каждый добавляемый элемент). Если размера не хватает, то создается новый fixed array большей длины, поле elements меняется на него - со старого ссылка снимается и он может быть собран GC. Таким образом, при изменении длины массива (в том числе через length), движку не нужно обновлять ссылки на массив, так как другие объекты ссылаются на JS Array, а его адрес сохраняется (адрес меняется только при миграции между спейсами и при "дефрагментации"). Свойство elements является внутренним, и к нему нет доступа из JS.
В V8 случай array.length = 0 обычно обрабатывается отдельно, в этом случае создается пустой elements (https://github.com/v8/v8/blob/2ff8d15b64f4840a1987b005fddaadb7b6fb87ae/src/objects/elements.cc#L1578). Почему "обычно", потому что в V8 (и других движках) есть множество внутренних типов для elements (по сути самого массива), в зависимости от того какие в нем хранятся значения и есть ли "дырки" (типы: https://github.com/v8/v8/blob/2ff8d15b64f4840a1987b005fddaadb7b6fb87ae/src/objects/elements-kind.h#L105). От типа elements зависит внутреняя логика и, в частности, применяемые оптимизации. Так массив чисел и массив объектов могут иметь совсем разные профили.
> А при array = [] старый массив может быть обработан сборщиком мусора асинхронно, а может даже и параллельно.
С точки зрения механики управления памятью (в плане сборки мусора, ее синхронности и т.д.), в обоих случаях разницы не будет. Разве что при создании нового массива будет создавать больше объектов (новые JS Array), которые позже могут эвакуироваться из молодой памяти – поэтому GC будет немного медленней, но в данном случае незначительно так как сами объекты крохотные.
> array.length = 0 не просто удаляет элементы, а изменяет capacity массива, т.е. дефрагментирует выделенный под него кусок памяти. Важно, что происходит это синхронно, т.к. ссылка на массив меняться не должна и сразу для всего пользовательского кода должна выдавать новый вариант массива.
Начнем с того, что выделенные фрагменты памяти не дефрагментируются, максимум помечаются как более не используемые, но и этого не делается. Так "используемость" определяется по наличию ссылок на фрагмент, в зону отвественности объекта (массив тоже объект) входит только задача убрать ссылки. Признание фрагмента неиспользуемым (мертвым) - это задача GC, который проверяет количество ссылок на объект (фрагмент памяти).
Дефрагментация про другое – это процесс переукладки используемых фрагментов памяти более компактно, обычно в начало спейса, так чтобы свободная часть была представлена одним фрагментом. Процесс не применяет к объектам точечно, но применяется к области памяти (спейсу). В V8 этот процесс называется compat и является частью Major GC, одним из финальных его этапов. Так как процесс недешевый (нужно не только перенести много памяти с одного места в другое, но и обновить все ссылки, то есть прописать новые адреса), то он выполняется не на каждый Major GC, а только когда в этом действительно есть необходимость. При этом, "дефрагментация" осуществляется не для всех спейсов, в основном для старого поколения, и для некоторых других (например, large object space). Для молодого поколения необходимости в дефрагментации нет, так как когда заканчивается память в этом спейсе, то срабатывает Minor GC – из него эвакуируются выжившие объекты (переносятся во вторую молодую или в старую), а сам спейс зачищается и начинает наполняться заново, объектами которые создаются.
Массив это не монолит - он представляет из себя несколько объектов. "Входной точкой" является объект JS Array (типичный JS объект), под капотом опционально elements (массив значений фиксированной длины - fixed array) и другие объекты (например, для хранения собственных свойств, когда делается array.foo = 1). У массива действительно есть capacity, а именно свойство хранящее число используемых слотов в elements, так как elements может быть больше чем используется (обычно выделяется с запасом, чтобы не пересоздавать elements на каждый добавляемый элемент). Если размера не хватает, то создается новый fixed array большей длины, поле elements меняется на него - со старого ссылка снимается и он может быть собран GC. Таким образом, при изменении длины массива (в том числе через length), движку не нужно обновлять ссылки на массив, так как другие объекты ссылаются на JS Array, а его адрес сохраняется (адрес меняется только при миграции между спейсами и при "дефрагментации"). Свойство elements является внутренним, и к нему нет доступа из JS.
В V8 случай array.length = 0 обычно обрабатывается отдельно, в этом случае создается пустой elements (https://github.com/v8/v8/blob/2ff8d15b64f4840a1987b005fddaadb7b6fb87ae/src/objects/elements.cc#L1578). Почему "обычно", потому что в V8 (и других движках) есть множество внутренних типов для elements (по сути самого массива), в зависимости от того какие в нем хранятся значения и есть ли "дырки" (типы: https://github.com/v8/v8/blob/2ff8d15b64f4840a1987b005fddaadb7b6fb87ae/src/objects/elements-kind.h#L105). От типа elements зависит внутреняя логика и, в частности, применяемые оптимизации. Так массив чисел и массив объектов могут иметь совсем разные профили.
> А при array = [] старый массив может быть обработан сборщиком мусора асинхронно, а может даже и параллельно.
С точки зрения механики управления памятью (в плане сборки мусора, ее синхронности и т.д.), в обоих случаях разницы не будет. Разве что при создании нового массива будет создавать больше объектов (новые JS Array), которые позже могут эвакуироваться из молодой памяти – поэтому GC будет немного медленней, но в данном случае незначительно так как сами объекты крохотные.
Forwarded from Roman Dvornov
Однако для JS движка создать новый массив нередко быстрее, чем поменять существующий, так как в существующем уже накоплена информация о типах элементов и это может приводить к дополнительным проверкам и действиям, спаливать выполнение в медленные пути.
Но проблема не только в этом, а в том что меняется свойство length. Даже если массив будет изначально пустой (если задать size = 0 в тесте), разница все равно будет существенной (на моей машине Length в 10 раз медленее для size = 0). Это говорит о том, что скорей всего установка значения свойству length не оптимизируется, так как считается редким кейсом (в действительности это редко применяется). Если поиграться с опцией --max-opt для Nodejs, то можно заметить, что при отключении оптимизирующих компиляторов (--max-opt=1) время для Immutable вырастает в разы, а вот для Length на несколько десятков процентов – что поддерживает гипотезу.
Что интересно, если поменять другое свойство у массива, то есть не length, а скажем array[j].foo = 1 – то время тратится существо меньше.
В современных движках, часто результаты зависят не от того насколько одно быстрее другого с точки зрения логики, а насколько одно или другое хорошо оптимизируется движком – оптимизируют в основном частые патерны.
Но проблема не только в этом, а в том что меняется свойство length. Даже если массив будет изначально пустой (если задать size = 0 в тесте), разница все равно будет существенной (на моей машине Length в 10 раз медленее для size = 0). Это говорит о том, что скорей всего установка значения свойству length не оптимизируется, так как считается редким кейсом (в действительности это редко применяется). Если поиграться с опцией --max-opt для Nodejs, то можно заметить, что при отключении оптимизирующих компиляторов (--max-opt=1) время для Immutable вырастает в разы, а вот для Length на несколько десятков процентов – что поддерживает гипотезу.
Что интересно, если поменять другое свойство у массива, то есть не length, а скажем array[j].foo = 1 – то время тратится существо меньше.
В современных движках, часто результаты зависят не от того насколько одно быстрее другого с точки зрения логики, а насколько одно или другое хорошо оптимизируется движком – оптимизируют в основном частые патерны.