Здравствуйте, меня зовут Дмитрий Карловский и я… состоятельный человек. У меня есть состояние на сервере, есть состояния в локальных хранилищах, есть состояние окна браузера, есть состояние доменной модели, есть состояние интерфейса. И всё это многообразие состояний нужно поддерживать синхронизированным. Если одно состояние как-то изменяется, то остальные связанные с ним состояния должны как можно скорее обновиться. Особую пикантность ситуации придаёт то, что синхронизация с сервером может занимать секунды, а блокировать пользовательский интерфейс можно лишь на доли секунд.
Далее вы узнаете: как реактивность побеждает асинхронность, как императивная реактивность уживается с функциональной, как простые абстракции позволяют писать надёжный и быстрый код, а также как я однажды перешёл на идемпотентную сторону силы и всё заверте
Разбираемся в сортах реактивности
Прежде всего стоит определиться с понятием "реактивность" (reactivity). Это — явление, когда изменение одного состояния приводит ко каскадному изменению других состояний. Реактивное программирование (reactive programming) использует этот принцип для описания правил изменения одних состояний при изменений других. В дальнейшем единицу реактивного состояния для простоты мы будем называть "атомом".
Если вы никогда не программировали в реактивной парадигме, то могу порекомендовать прочитать мою статью двухлетней давности, где объясняется зачем оно вообще надо. Тем не менее она слегка устарела, так как $mol_atom реализует уже более продвинутую логику.
[ State1 ]----/ Rule1-2 /---->[ State2 ]----/ Rule2-3 /---->[ State3 ]
Реактивные правила могут описываться в трёх парадигмах:
Перезапуски (restarts). Правила хранятся отдельно от состояния и представляют собой процедуру меняющую одни состояния на основе других состояний. Когда какое-либо состояние, задействованное этой процедурой, изменяется, процедура автоматически перезапускается. Проблема этого подхода в стационарности правил синхронизации, больших накладных расходах и риске уйти в бесконечный цикл.
[ State ]<-------->[ Rule1-2( State ) ]
[ State ]<-------->[ Rule2-3( State ) ]
Примеры реализаций: AngularJS@1, MeteorJS.
Проталкивание (push). Внутри атома хранятся правила формирования состояний других атомов. Обычно правила задаются при инициализации приложения и в дальнейшем не меняются, а если и меняются, то вручную. API реализующих этот подход библиотек очень сильно раздут — десятки и сотни методов решают его многочисленные проблемы: немедленное исполнение правил, зависимость от одного единственного атома, реагирование даже если новое значение равно старому и прочие.
[ State1 => Rule2( State1 ) ]-------->[ State2 => Rule3( State2 ) ]-------->[ State3 ]
Примеры реализаций: BaconJS, KefirJS, RxJS.
Затягивание (pull). Внутри атома хранится правило формирования его состояния в виде функции от состояний других атомов. Состояние формируется лениво, в момент обращения к нему. Если от состояния атома перестаёт кто-либо зависеть, то он может "засыпать" теряя зависимость от других атомов. В "ленивости", динамической автоматической активации и деактивации правил основное преимущество данного подхода, поэтому именно о нём и пойдёт дальнейшее повествование.
[ State1 ]-------->[ State2 = Rule2( State1 ) ]-------->[ State3 = Rule3( State2 ) ]
Примеры реализаций: KnockOutJS, MobXJS, CellX и собственно $mol_atom.
Стоит отметить, что приведённое разделение принципов условно и отражает основной принцип работы соответствующих библиотек.
Нет времени объяснять
Создаём пару изменяемых атомов с вычисляемым значением по умолчанию:
const userName = new $mol_atom( 'userName' , next => next || 'Anonymous' )
const showName = new $mol_atom( 'showName ' , next => next || false )
Создаём вычисляемый атом, не допускающий прямое изменение своего значения:
const greeting = new $mol_atom( 'greeting' , next => {
if( !showName.value() ) return 'Hello!'
return `Hello, ${ userName.value() }!`
} )
Создаём презентационный атом, выводящий приветствие в консоль всякий раз, когда что-либо в данных меняется:
const presenting = new $mol_atom( 'presenting' , next => {
console.log( greeting.value() )
} )
Принудительно активируем презентационный атом:
presenting.value()
//Hello!
Меняем сразу 2 атома, но вывод будет только один:
showName.value( true )
userName.value( 'John' )
//Hello, John!
Принудительно презентуем после каждого изменения данных:
userName.value( 'Jin' )
presenting.value()
// Hello, Jin!
showName.value( false )
presenting.value()
// Hello!
Пытаемся изменить приветствие напрямую — ничего не выходит:
greeting.value( 'Hi!' )
Принудительно устанавливаем приветствие в обход правил:
greeting.value( 'Hi!' , $mol_atom_force )
//Hi!
Меняем исходные данные — ноль реакции:
showName.value( false )
Принудительно обновляем значение приветствия по правилам:
greeting.value( void null , $mol_atom_force )
//Hello!
Включаем логирование всех атомов и меняем один из них:
$mol_log.filter( '' )
showName.value( true )
//21:44:11 showName.value() ["push", true, false]
//21:44:11 greeting.value() ["obsolete"]
//21:44:11 $mol_atom.sync []
//21:44:11 userName.value() ["push", "Anonymous", undefined]
//21:44:11 greeting.value() ["push", "Hello, Anonymous!", "Hello!"]
//21:44:11 presenting.value() ["obsolete"]
//Hello, Anonymous!
Выключаем логгирование:
$mol_log.filter( null )
Попробовать онлайн. Ещё пример.
А внутре у ней что?
Казалось бы, что может быть проще: при изменении одного состояния, вызывать пересчёт зависимых состояний. Однако..
Прежде всего нужно определиться что от чего зависит.
Допустим, вы делаете блог и на странице какого-нибудь поста нужно выводить в заголовке окна название поста, а на странице постов отфильтрованных по тегу — название тега. Получается, состояние заголовка окна динамически меняет свои зависимости. То оно зависит от названия поста, то от имени тега, но в обоих случаях оно зависит от некоторого условия, определяющего от чего ему нужно зависеть.
Уже в этом простом примере видно, как важна поддержка динамических зависимостей. Поэтому нам не подходит такая абстракция как "stream", вокруг которой построены популярные "push" библиотеки.
Типичная "pull" реализация работает следующим образом:
Когда атом вычисляет своё состояние, он первым делом помещает себя в некоторую глобальную переменную, что позволяет другим атомам понять, какой атом сейчас вычисляется.
Далее он создаёт пустой список своих зависимостей, в котором будут зарегистрированы все атомы, к которым так или иначе будет произведено обращение.
Только теперь происходит исполнение формулы — обычной функции, возвращающей некоторый результат.
В процессе исполнения формулы, может быть обращение к каким угодно другим функциям, объектам и браузерным интерфейсам.
Если в процессе исполнения формулы произойдёт обращение к значению другого атома, то тот, взяв из глобальной переменной зависимый атом, слинкует их таким образом, что оба будут знать, что значение одного зависит от значения другого.
После того, как формула вычислена, полученное значение запоминается в атоме, чтобы в дальнейшем можно было возвращать его сразу, без относительно долгого вычисления формулы.
И наконец, происходит сравнение нового списка зависимостей и старого, чтобы "разъединить" более не зависящие друг от друга атомы.
- Если значение атома меняется, то происходит уведомление завясящих от него атомов, что их значения устарели и им тоже требуется актуализация.
Таким образом мы имеем всегда актуальную сеть из атомов, которая динамически перестраивается, отражая реальные зависимости в данный момент времени.
Стоит отметить, что к формулам (и, как следствие, ко всем так или иначе вызываемым функциям) предъявляется требование быть идемпотентными. То есть, если ни одна зависимость не изменилось, то и результат работы функции должен остаться неизменным. Под результатом тут понимается не только возвращаемое формулой значение, но и производимые в процессе вычисления побочные действия.
Именно благодаря идемпонентности, появляется возможность кешировать результат на неопределённый срок и сбрасывать кеш лишь когда он либо больше не нужен, либо больше не актуален.
Но что если при вычислении значения возникнет исключение? Если его не перехватить, то текущий "вычисляемый атом", помещённый в глобальную переменную, так там и останется, что в дальнейшей работе приложения может привести к появлению весьма странных зависимостей, что довольно не просто отдебажить. Кроме того, каждый раз при обращении к атому, будет происходить вычисление формулы и всплытие исключения, что не только засорит консоль на больших объёмах данных, но и вызовет неслабые тормоза. И вишенкой на торте проблем будет то, что часть атомов так и останется в неактуальном состоянии.
Говоря простым языком, состояние приложение перестанет быть консистентным и начнёт работать нестабильно. Чтобы этого не допустить, каждый атом должен перехватить исключение и сохранить в себе вместо значения и позволить ему всплыть дальше. Таким образом, при следующем обращении к атому, это исключение будет брошено вновь. Если исключение было следствием некорректных данных, то как только данные станут корректными, атом пересчитает своё значение и вместо кидания исключения будет уже возвращать актуальное значение.
Но в какой момент зависимые атомы должны пересчитывать свои значения? Если делать это сразу при получении уведомления об устаревании, атом может впустую по многу раз исполнять формулу, когда последовательно меняются несколько его зависимостей. Поэтому, в хороших реализациях атом немедленно лишь помечается устарешим, а вот актуализируется он уже отложенно.
Вообще говоря, проблема лишних вычислений не так безобидна, как может показаться. Она может приводить ко следующим неприятным последствиям:
- Снижение производительности вплоть до невозможности пользоваться приложением.
- Возникновение исключений в неожиданных местах. Типичная ситуация — обращение к уже удалённому объекту.
- Лишние запросы к серверу, нотификации и тому подобные не "схлопывающиеся" повторения.
Существует несколько стратегий отложенной актуализации атомов:
В порядке устаревания. В момент устаревания, атом добавляется в конец очереди на пересчёт. Самая простая стратегия. Однако, она оставляет довольно большое число лишних пересчётов.
В порядке создания. Каждому атому задаётся числовой идентификатор. Позднее созданные атомы имеют и большее значение идентификатора. Пересчитываются атомы, начиная с атома с наименьшим идентификатором. Получается, что зависимые атомы пересчитываются раньше, чем те, от которых они зависят, так как они зачастую создаются в процессе вычисления зависящих. Это всё опять же приводит к лишним вычислениям.
В порядке увеличения глубины. Атомы сортируются по максимальной глубине зависимостей. Сначала пересчитываются атомы без зависимостей, потом зависящие от атомов без зависимостей, потом от атомов, которые зависят от атомов без зависимостей и так далее. При такой схеме лишних пересчётов практически не происходит. Однако, иногда возникают казусы, когда атом имеет небольшую глубину, но его существование определяется атомом с большей глубиной. Таким образом он пересчитывает своё значение первым, а позже удаляется, так как в нём больше нет необходимости.
- В правильном порядке. Зависимости актуализируются в том же порядке, в котором к ним было произведено обращение при вычислении зависимого атома.
Чтобы обеспечить правильный порядок актуализации, каждый атом может находиться в одном из 4 состояний:
Устаревший (obsolete). При следующем обращении, его значение будет вычислено по формуле. Когда атом переходит в это состояние, он уведомляет зависимые атомы, что они "возможно устарели".
Возможно устаревший (checking). При следующем обращении, он сначала убедится, что все его зависимости в актуальном состоянии. Как только одна из них изменит своё значение, то атом станет "устаревшим" со всеми вытекающими. Иначе — станет "актуальным" без перевычисления значения. Когда атом переходит в состояние "возможно устаревший", то тут же уведомляет зависимые атомы, что они тоже "возможно устарели". Таким образом этот состояние каскадно распространяется на всё зависимое поддерево. Если от этого атома никто не зависит, то он добавляет себя в очередь на отложенную актуализацию.
Актуальный (actual). При обращении, возвращает запомненное значение. Если при переходе в актуальное состояние, его значение изменилось, то он уведомляет зависимые атомы, что они "устарели".
- Вычисляется (pulling). Когда атом начинает перевычисляться, то переходит в это состояние. Обращение к атому в этом состоянии приводит к возникновению исключения, так как свидетельствует о циклической зависимости. После вычисления, даже если оно закончилось ошибкой, атом запоминает результат, переходя в "актуальное" состояние.
Такая логика работы может показаться слишком сложной и избыточной, однако но она позволяет гарантировать, что:
- Приложение не зависнет из-за циклических зависимостей.
- Пересчёт атома будет происходить не раньше, чем его зависимости примут актуальное значение.
- Пересчёт атома не будет произведён, если актуальные значения его зависимостей не поменялись.
- При обращении к атому, мы гарантированно получаем актуальное значение (остальные схемы этого не гарантируют, так как не исключают возможной необходимости его повторного вычисления).
Нужно больше мемов
Тут и далее примеры идут на языке TypeScript, который является ES6 с добавлением типизации после двоеточий.
Работать с атомами напрямую не очень удобно. Им нужно давать уникальные имена, чтобы в логах выводилось что-то осмысленное, а не просто абстрактные числа. Нужно их где-то хранить и не создавать лишний раз. Чтобы избавиться от этой рутины, введём понятие "свойства", как полиморфного метода, который в зависимости от числа параметров "возвращает" или "устанавливает и тут же возвращает" некоторое значение.
Обычное свойство имеет следующий интерфейс:
{
< Value >() : Value
< Vlaue >( nextValue? : Value ) : Value
}
Например:
class App {
title( next? : string ) {
if( next !== void null ) document.title = next
return document.title
}
}
Его можно сделать реактивным (кешируемым с автоматической инвалидацией кеша), просто добавив декоратор $mol_mem()
:
class App {
@ $mol_mem()
title( next? : string ) {
if( next !== void null ) document.title = next
return document.title
}
}
Оформим код приветствующего приложения в виде класса (для удобства воспользуемся $mol_object, помогающем генерировать правильные имена, но вы можете его и не использовать, определяя метод toString объекта вручную):
class App extends $mol_object {
@ $mol_mem()
userName( next? : string ) { return next || 'Anonymous' }
@ $mol_mem()
showName( next? : boolean ) { return next || false }
greeting() {
if( !this.showName() ) return 'Hello!'
return `Hello, ${ this.userName() }!`
}
@ $mol_mem()
presenting() {
console.log( this.greeting() )
}
}
Как можно заметить свойство greeting
не реактивное, так что этот метод будет вызываться каждый раз при обращении к нему. А вот showName
— реактивное, так что метод будет вызываться лишь при первом чтении значения по умолчанию и при передаче ему нового значения.
Мы могли бы объявлять свойства в духе MobX, но для этого пришлось бы писать более громоздкий код с дублированием имени свойства:
class App {
@observable
get userName() { return 'Anonymous' }
set userName( next : string ) { return next }
@observable
get showName() { return false }
set showName( next : boolean ) { return next }
get greeting() {
if( !this.showName ) return 'Hello!'
return `Hello, ${ this.userName }!`
}
@computed
get presenting {
console.log( this.greeting )
}
}
Кроме того, нельзя было бы также легко и просто перегружать свойство целиком извне, как в следующем примере:
class My extends $mol_object {
@ $mol_mem()
static instance() {
return new this
}
name(){ return `Jin #${ Date.now() }` }
@ $mol_mem()
showName( next ) {
return ( next === void null ) ? true : next
}
@ $mol_mem()
app() {
const app = new App
app.userName = ()=> this.name()
app.showName = ( next )=> this.showName( next )
return app
}
}
My.instance().app().presenting()
//Hello, Jin #1481383086982!
Попробовать онлайн. Ещё пример.
Тут мы использовали перегрузку свойств для элегантного создания одностороннего биндинга для свойства userName
и двустороннего для свойства showName
. Это очень мощная техника, позволяющая детально настроить поведение любого объекта, сразу после его создания, без риска его поломать.
Стоит обратить внимание на исключительно понятные логи, по которым чётко видно какие состояния как изменялись:
$mol_log.filter( '' )
My.instance().app().showName( false )
$mol_log.filter( null )
//11:27:58 My.instance().showName() ["push", false, true]
//11:27:58 My.instance().app().presenting() ["obsolete"]
//Hello!
Часто требуется работать не с одним значением, а с целым семейством значений, различаемых некоторым ключом, но по одной и той же логике. Для этих случаев используется следующий интерфейс:
{
< Key , Value >( key : Kay ) : Value
< Key , Vlaue >( key : Key , nextValue? : Value ) : Value
}
Для примера, создадим простейший класс, позволяющий использовать REST ресурсы:
class Rest extends $mol_object {
@ $mol_mem_key()
static resource( uri : string , next? : any , force : $mol_atom_force ) {
debugger
const request = new XMLHttpRequest
const method = ( next === void null ) ? 'get' : 'put'
request.onload = ( event : Event )=> {
this.resource(
uri ,
request.responseText ,
$mol_atom_force
)
}
request.onerror = ( event : ErrorEvent )=> {
setTimeout( ()=> {
this.resource(
uri ,
event.error || new Error( 'Unknown HTTP error' ) ,
$mol_atom_force
)
} )
}
request.open( method , uri )
request.send( next )
throw new $mol_atom_wait( `${ method } ${ uri }` )
}
}
Для разных uri будут создаваться отдельные атомы. Но при обращении к одному и тому же uri — будет использован один и тот же атом. Как можно заметить, логика получения данных и установки значения полностью совпадает. Разница лишь в том, что при запросе данных будет использован http-метод "get", а при передаче данных — "put".
Так как атомы умеют адекватно работать с исключительными ситуациями, то тут мы использовали это их свойство для абстрагирования кода приложения от асинхронности. Так как данные ещё не загружены, то мы не можем сразу же их вернуть. Вместо этого кидается специальное исключение и вычисление зависимых от этого свойства свойств прерывается в ожидании каких-либо изменений. Когда приходит ответ от сервера, он устанавливается в качестве значения свойства и зависимые атомы "оживают", но на этот раз их вычисление не прерывается. Стоит отметить, что обработчик ошибок может быть вызван браузером синхронно, поэтому устанавливается свойству значение отложенно, чтобы гарантировать, что будет установленно именно оно, а не $mol_atom_wait, который кидается далее.
Давайте реализуем приложение, рисующее все эмодзи, которые поддерживает гитхаб:
class App extends $mol_object {
@ $mol_mem()
static presenting() {
const emojis = JSON.parse( Rest.resource( 'https://api.github.com/emojis' ) )
document.body.innerHTML = ''
for( let id in emojis ) {
const image = document.createElement( 'img' )
image.src = emojis[ id ]
document.body.appendChild( image )
}
}
}
App.presenting()
Как видно, нам не пришлось плясать с колбэками, обещаниями, генераторами, стримами, асинхронными функциями и прочими адскими созданиями. Вместо этого наш код остался простым и понятным.
Но я ведь только внедрил..
RxJS
Облегчённая версия — всего лишь 250КБ.
Всё это только для того, чтобы вместо последовательного кода писать комбинаторы комбинаторов кучи мелких замыканий. Мейнтейнеры AngularJS@2 ведь не могут ошибаться. Спикер с *JsConfTalksMeetUpDays убедительно размахивал об этом руками. Именно так нужно писать код в 2k16:
const greeting = showName
.select( showName => {
if( showName ) return userName.map( userName => `Hello, ${ userName }!` )
return Rx.Observable.from([ 'Hello!' ])
} )
.switch()
А за такое устаревшее поделие, нужно руки отрывать:
greeting() {
if( this.showName() ) return `Hello, ${ this.userName() }!`
else return 'Hello!'
}
Promises
Встроенные уже почти в каждый браузер, они позволяют вам писать… цепочки из цепочек мелких замыканий. Ведь именно так выглядит код настоящего синьора:
let _config
const getConfig = ()=> {
if( _config ) return _config
return _config = $.get( 'config.json' ).then( JSON.parse )
}
let _profile
const getProfile = ()=> {
if( _profile ) return _profile
return _profile = $.get( 'profile.json' ).then( JSON.parse )
}
const getGreeting = ()=> getConfig()
.then( config => {
if( !config.showName ) return 'Hello!'
return getProfile()
.then( profile => `Hello, ${profile.userName}!` )
} )
Этот же код просто невозможно поддерживать:
@ $mol_mem()
config() {
return JSON.parse( Rest.resource( 'config.json' ) )
}
@ $mol_mem()
profile() {
return JSON.parse( Rest.resource( 'profile.json' ) )
}
@ $mol_mem()
greeting() {
if( !this.config().showName ) return 'Hello!'
return `Hello, ${ this.profile().userName }!`
}
Async functions
Вы на самом острие технологий. На столь остром, что многие браузеры, не понимают то, что вы пишете:
let _config
const getConfig = async ()=> {
if( _config ) return _config
return _config = JSON.parse( await $.get( 'config.json' ) )
}
let _profile
const getProfile = async ()=> {
if( _profile ) return _profile
return _profile = JSON.parse( await $.get( 'profile.json' ) )
}
const getGreeting = async ()=> {
if( !( await getConfig() ).showName ) return 'Hello!'
return `Hello, ${ ( await getProfile() ).userName }!`
}
Поэтому вы используете webpack и babel, которые во мгновение ока преобразуют ваш код, понимаемый лишь двумя передовыми браузерами, в код для нижнего интернета:
'use strict';
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
var _config = void 0;
var getConfig = function () {
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee() {
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
if (!_config) {
_context.next = 2;
break;
}
return _context.abrupt('return', _config);
case 2:
_context.t0 = JSON;
_context.next = 5;
return $.get('config.json');
case 5:
_context.t1 = _context.sent;
return _context.abrupt('return', _config = _context.t0.parse.call(_context.t0, _context.t1));
case 7:
case 'end':
return _context.stop();
}
}
}, _callee, undefined);
}));
return function getConfig() {
return _ref.apply(this, arguments);
};
}();
var _profile = void 0;
var getProfile = function () {
var _ref2 = _asyncToGenerator(regeneratorRuntime.mark(function _callee2() {
return regeneratorRuntime.wrap(function _callee2$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
if (!_profile) {
_context2.next = 2;
break;
}
return _context2.abrupt('return', _profile);
case 2:
_context2.t0 = JSON;
_context2.next = 5;
return $.get('profile.json');
case 5:
_context2.t1 = _context2.sent;
return _context2.abrupt('return', _profile = _context2.t0.parse.call(_context2.t0, _context2.t1));
case 7:
case 'end':
return _context2.stop();
}
}
}, _callee2, undefined);
}));
return function getProfile() {
return _ref2.apply(this, arguments);
};
}();
var getGreeting = function () {
var _ref3 = _asyncToGenerator(regeneratorRuntime.mark(function _callee3() {
return regeneratorRuntime.wrap(function _callee3$(_context3) {
while (1) {
switch (_context3.prev = _context3.next) {
case 0:
_context3.next = 2;
return getConfig();
case 2:
if (_context3.sent.showName) {
_context3.next = 4;
break;
}
return _context3.abrupt('return', 'Hello!');
case 4:
_context3.next = 6;
return getProfile();
case 6:
_context3.t0 = _context3.sent.userName;
_context3.t1 = 'Hello, ' + _context3.t0;
return _context3.abrupt('return', _context3.t1 + '!');
case 9:
case 'end':
return _context3.stop();
}
}
}, _callee3, undefined);
}));
return function getGreeting() {
return _ref3.apply(this, arguments);
};
}();
И нет, этот код всё-равно никуда не годится:
@ $mol_mem()
config() {
return JSON.parse( Rest.resource( 'config.json' ) )
}
@ $mol_mem()
profile() {
return JSON.parse( Rest.resource( 'profile.json' ) )
}
@ $mol_mem()
greeting() {
if( !this.config().showName ) return 'Hello!'
return `Hello, ${ this.profile().userName }!`
}
Тут ведь не понятно, асинхронный метод config
или нет, а ведь это очень важно знать!
Но если всё же..
$mol_atom является основным строительным кирпичиком фреймворка $mol. Он обеспечивает надёжную и гибкую динамическую взаимосвязь между всеми компонентами, позволяя описывать их предельно простым синхронным кодом. Асинхронность не выпячивается наружу, а полностью (да-да, именно полностью) инкапсулируется внутри асинхронных модулей, делая работу с ней простой и приятной. Ошибки не рушат всё приложение, а корректно обрабатываются. А дебаг на редкость удобен, благодаря человекопонятным идентификаторам, синхронному коду и быстрому доступу из консоли к любому состоянию. Независимая сборка $mol_atom+$mol_mem весит всего 25KB и может быть использована с любым другим фреймворком.
Комментарии (14)
Odrin
12.12.2016 11:14+6Правильно, зачем нам Angular, React, RxJS и т.д., ведь_есть_отличный_фреймворк_$mol, лишенный фатального недостатка, хорошо знакомый каждому разработчику и имеющий подробную документацию, ссылки в которой ведут на 404.
justboris
12.12.2016 12:46Начало с рассказом про атомы интересное, а вот наезды на разработчиков, не пользующихся
$mol_mem
странные.
Всегда можно использовать lodash.memoize или @memoize декоратор, ничего уникального здесь я не вижу
saggid
12.12.2016 13:24+6Вроде идея интересная. Что-там даже мелькает в плане сравнения с другими реализациями… Но вот метод подачи материала… Я даже не знаю как это назвать. Куча какого-то непонятного кода, куча каких-то умных фраз. Большинство текста видимо можно понять только изучив гору каких-то книг, и перековыряв внутренности других библиотек.
Что это за примеры кода? На чём они вообще написаны? Это не чистый JavaScript. TypeScript? Dart? Вы хоть объясните, на чём вы пишете свои примеры. Объясните как работают методы вашей библиотеки. Объясните что это вообще за атомы, о которых вы постоянно говорите.
Почему зарубежные авторы всегда умудряются писать ясный и простой для понимания текст, который даже после перевода приятно читать? Почему-то статьи российских разработчиков всегда ярко выделяются тем, что они пишут какую-то заумную фигню, понятную только небольшому узкому кругу таких же просвящённых, как и они сами. Они как-будто ленятся написать действительно понятную статью. Разложить по полочкам то, что они хотят донести до других. Вся суть статьи как-будто сводится к примитивному желанию показать свою крутоту и унизить все остальные инженерные решения этого мира. Непонятно за что вам ставить плюс. Непонятно за что вас благодарить. Ибо я до сих пор не понимаю 90% всего, что представляет из себя ваш фреймворк. Непонятно зачем вы вообще пишете все эти статьи.
Вы понимаете, что при таком методе подачи информации, ваш фреймворк всё равно обречён быть среди никому неизвестных аутсайдеров? Пока у вас нет нормальной документации и ясного API, пока библиотеку невозможно понять, никто не будет её использовать, никому она не будет нужна в реальной практике.
Просто смотрю на пример с гитхаба вашей библиотеки:
$my_hello $mol_viewer childs / < namer $mol_stringer hint \Name value > name < message \
Блин, ну что всё это вообще означает?) Что это за синтаксис? Как с этим работать?) Хватит хотя-бы унижать другие инженерные решения в своих статьях. По крайней мере, они в сотни раз более понятные, чем то, что придумали вы.
RubaXa
12.12.2016 23:31+3У автора есть статья про атомы на хабре, и только она объемнее чем эта ;]
Вы поймите на хабре ровно два человека, которые хоть как-то освещают эту тему, это vintage и Riim, за рубежом вообще никто, если не считать KO (да и то его с этой стороны не рассматривают). И да, у этих ребят есть некоторые проблемы с подачей материала и им вечно сливают карму (а зря, очень зря).
Тема сложная, чтобы её понять, наверно проще самому запрограммировать свой «атом» и пропустить через себя идею. Это не просто очередная «либа», это совсем другой уровень мышления, это не какой-нибудь Rx.
Но как бы спорно она не писали, эти ребята своей работой показывают альтернативное решение, которым практически никто не занимается и лично мне интересно наблюдать за их результатами. Потому что сам я так и не смог приручить реактивность :]
saggid
13.12.2016 06:06и им вечно сливают карму
Насчёт этого я полностью согласен, что просто так брать и сливать человеку карму, особенно когда он пишет действительно какие-то важные вещи — это что-то из разряда высших степеней невежества.
Scf
13.12.2016 14:31Тема и правда очень интересная, достаточно взять и написать самому простенькую реализацию атома на 150 строк. Штука безумно интересная для любого UI, сейчас я пытаюсь её "втянуть" в известную концепцию событий и промисов.
ForNeVeR
14.12.2016 14:00Примеры кода, кажется, на обычном ES2016 (или какой там сейчас, уже 2017?) — стрелочные функции, классы, декораторы. Не то чтобы совсем каждодневные фичи, конечно, но и без особой экзотики.
Scf
12.12.2016 19:52+1А как решается проблема с событиями. Не любые переходы интерфейса можно описать атомами.
Как, к примеру, будет выглядеть пример с двумя radio button-s? Т.е. есть две кнопки, каждая обернута в двусторонний биндингatom<boolean>
. Как изобразить условие, что при нажатии на свободную кнопку вторая должна быть отключена?
Scf
12.12.2016 19:59+1https://jsbin.com/ligibuqayi/1/edit?js,console,output
Открываем консоль, вводим два раза быстроRest.resource('http://ya.ru')
После второго раза ошибка:"Cyclic atom dependency of Rest.resource(\"http://ya.ru\")"
Сыровато.
raveclassic
12.12.2016 22:55Странно, а у меня что-то не воспроизводится. Какая-то ошибка, с безумным трейсом:
"$mol_atom_wait: get google.com at new $mol_atom_wait (https://eigenmethod.github.io/mol/mem/-/web.js:663:25) at Function.resource (ligibuqayi.js:42:19) at $mol_atom.pull (https://eigenmethod.github.io/mol/mem/-/web.js:470:29) at $mol_atom.actualize (https://eigenmethod.github.io/mol/mem/-/web.js:463:33) at $mol_atom.get (https://eigenmethod.github.io/mol/mem/-/web.js:426:18) at $mol_atom.value (https://eigenmethod.github.io/mol/mem/-/web.js:594:29) at Function.descr.value (https://eigenmethod.github.io/mol/mem/-/web.js:732:29) at eval (eval at a.eval (https://static.jsbin.com/js/prod/runner-3.40.2.min.js:1:11964), <anonymous>:1:6) at eval (<anonymous>) at Object.a.eval (https://static.jsbin.com/js/prod/runner-3.40.2.min.js:1:11964)"
yogurt1
Про Babel обманка, здесь у вас включен regenerator, который не очень нужен, если используется firefox > 45 или chrome > 50
Также async/await, генераторы, Promise входят в стандарт ECMAScript, и даже самый передовой async/await доступен в Node 7 и Chrome 55
А сколько кода придется писать, что бы впихнуть $mol_object в React, Vue, Angular2 и т.д.?
Лучше пускай будет +10 строчек читабельного boilerplate-кода, чем какое-нибудь скрытое непонятное нечто
arvitaly
Не инкапсулируется, а прячется за конкретной реализацией, применимой в конкретных ситуациях, каков процент этих ситуаций?
Мало того, binding к VanilaJS становится просто невозможным (попросту не используются стандартные интерфейсы), а значит мы обязаны писать всю логику внутри и средствами $mol и возникает логичный вопрос: а зачем нам, в таком случае, бороться с JavaScript, если мы все равно пользуемся новым языком?