Атом — реализация на TypeScript

    Здравствуйте, меня зовут Дмитрий Карловский и я… профессиональный велосипедист. За свою жизнь я перепробовал множество железных коней, но в конечном счёте остановился на самодельном. Не то чтобы мне очень нравилось работать напильником, тратя кучу свободного времени на изобретение колеса, но конечный результат, где каждая кочка не отдаётся болью в нижней половине туловища, того стоит. А теперь, когда вы знаете, что я затеял всё это не просто так, а чтобы сделать мир лучше, позвольте представить вам TypeScript/JavaScript модуль $jin.atom.

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

    Почему именно TypeScript?


    Первая реализация модуля была на чистом JavaScript, но недавно она была переписана на TypeScript. TypeScript — это практически тот же JavaScript, но с классами, выведением типов и нормальными лямбдами. Больше он практически ничего дополнительно не меняет и, как следствие, очень хорошо интегрируется с обычным JavaScript кодом. Вы можете напрямую обращаться к TypeScript модулям из JavaScript и наоборот. Разве что, для JS желательно всё же написать так называемые «декларации окружения», чтобы не терять тех преимуществ, что даёт статическая типизация. А даёт она следующие бонусы:
    * Подсказки в IDE избавляют программиста от необходимости держать в памяти документацию по всем методам и свойствам всех классов.
    * Поиск всех мест использования сущности — незаменимо при рефакторинге.
    * Выявление несогласованности по типам между различными участками приложения на этапе редактирования/сборки.
    К сожалению есть и минусы:
    * Иногда приходится плясать с бубном, объясняя компилятору, что ты имеешь ввиду.

    Альтернатив у TypeScript две:

    JSDoc — крайне не выразительный формат статического описания динамического кода в коментариях. Зачастую объём JSDoc-комментариев (без учёта словесного описания) получается больше собственно полезного кода. Показательный пример:

    	/**
    	 * @callback onTitleChange_handler
    	 * @param {string} next
    	 * @param {string} prev
    	 */
    
    	/**
    	 * @param {onTitleChange_handler} handler
    	 */
    	function onTitleChange( handler ){
    	    // ...
    	}
    
    	onTitleChange(
    	    /**
    	     * @type {onTitleChange_handler}
    	     */
    	    function( next, prev ){
    	        // ...
    	    }
    	)
    

    Dart — сосем другой язык, который, однако, рассчитан на трансляцию в JavaScript. Использует совсем иные идиомы, из-за чего имеет множество ограничений при интеграции с JavaScript кодом, существенно большее потребление памяти, проблемы при отладке с использованием встроенных в браузеры средств, а генерируемый из Dart код представляет из себя довольно объёмистую лапшу. Приведённый выше пример, на нём выглядит так:

    	typedef void onTitleChange_handler( String next , String prev );
    
    	onTitleChange( onTitleChange_handler handler ){
    	    // ...
    	}
    
    	void main() {
    		onTitleChange( ( next, prev ) => {
    			// ...
    		});
    	}
    

    Уже лучше, но также требует введения слишком большого числа именованных интерфейсов/типов. Это основной недостаток номинативной типизации. В TypeScript же используется структурная:

    	function onTitleChange(
    		handler : ( next : string , prev : string ) => void
    	){
    	    // ...
    	}
    
    	onTitleChange( ( next, prev ) => {
    	    // ...
    	});
    

    Но есть возможность и давать интерфейсам имена, если это необходимо:

    	interface onTitleChange_handler {
    		( next : string , prev : string ) : void
    	}
    
    	function onTitleChange( handler : onTitleChange_handler ){
    	    // ...
    	}
    
    	onTitleChange( ( next, prev ) => {
    	    // ...
    	});
    

    Итого, при переходе на TypeScript:
    + уменьшается объём кода
    + улучшается интеграция со средой разработки
    + появляются дополнительные проверки правильности по мере ввода
    — добавляется необходимость трансляции в JavaScript перед исполнением

    Мифы и легенды FRP


    Реактивные библиотеки можно разделить на два основных типа:
    1. Собственно FunctionalRP, где всё приложение описывается, как множество чистых функций.
    2. ProceduralRP, которые часто путают с FRP. В них приложение описывается императивно в виде потоков событий (стримов).

    Во втором случае приложение описывается в виде набора процедур вида: взять данные из разных мест, последовательно применить к ним определенные преобразования, после чего записать их в другие места.

    	this.message = Bacon.combine( [ this.mouseTarget, this.mouseCoords ] , function( target, coords ) {
    		return target + ' ' + coords
    	} )
    	.map( trimSpaces )
    	.map( htmlEncode )
    	.map( htmlParse )
    	.onValue( function( messaage ){
    		document.getElementById( 'log' ).appendChild( message )
    	} )
    

    Сравните с тем же самым, написанным в менее завуалированной форме:

    	this.onChange( [ 'mouseCoords', 'mouseTarget' ] , function( ){
    		var message = this.mouseTarget + ' ' + this.mouseCoords
    		message = trimSpaces( message )
    		message = htmlEncode( message )
    		message = htmlParse( message )
    		this.message = message
    		document.getElementById( 'log' ).appendChild( message )
    		this.fireChange( 'message' )
    	}
    

    Известные PRP библиотеки (Rx, Bacon) в соответствии с PRP архитектурой имеют довольно сложный API. Сложность заключается как в огромном числе методов, реализующих всевозможные операторы над стримами, так и в том, как описываются простейшие операции. Например, вот так будет выглядеть правильное условное ветвление:

    	var message = config.flatMapLatest( function( config ) {
    	    if( config ) {
    			return mouseCoords.map( function( coords ) {
    				return 'Mouse coords is ' + coords
    			}
    		} else {
    			return mouseTarget.map( function( target ) {
    				return 'Mouse target is ' + target
    			}
    		}
    	} )
    

    А вот так неправильное:

    	var message = Bacon.combineWith( function( config, coords, target ) {
    	    if( config ) {
    			return 'Mouse coords is ' + coords
    		} else {
    			return 'Mouse target is ' + target
    		}
    	}, config, mouseCoords, mouseTarget )
    

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

    Забегая вперёд, покажу для сравнения правильный код на атомах:

    	var message = $jin.atom.prop( {
    		pull : function( ) {
    			if( config.get() ) {
    				return 'Mouse coords is ' + coords.get()
    			} else {
    				return 'Mouse target is ' + target.get()
    			}
    		}
    	} )
    

    Говоря простым языком, в PRP удобно описывать зависимости, где источников данных сравнительно не много и их состав практически не меняются, а в FRP наоборот, набор источников может быть произвольным и динамичным без потери выразительности. С потребителями данных всё наоборот: в PRP одно и то же состояние может меняться множеством различных стримов, а в FRP за одно состояние отвечает ровно одна функция, по которой всегда понятно как формируется значение и от чего оно непосредственно зависит.

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

    Свойства


    Прежде чем браться за реализацию атомов стоит разграничить два понятия: значение (RValue) и контейнер (LValue).

    Самый известный контейнер — это переменная. Переменная поддерживает всего три интерфейса:

    	var count // создать контейнер если ещё не создан
    	count = 2 //записать значение
    	return count // вернуть значение
    

    Другой, не менее известный контейнер — это поле объекта. Оно поддерживает все интерфейсы переменной:

    	obj.count = 2 // создать контейнер (если ещё не создан) и записать в него значение
    	return obj.count // вернуть значение контейнера
    

    Но в дополнение к ним поле поддерживает ещё пару:

    	delete obj.field // уничтожение контейнера
    	'field' in obj // проверка на существование контейнера
    

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

    	var count = new $jin.prop.vary({}) // создать контейнер
    	count.set( 2 ) // записать значение
    	count.get() // прочитать значение
    

    image

    С одной стороны мы поменяли шило на мыло: контейнер (переменная count) хранит в себе другой контейнер (инстанс класса $jin.prop.vary) который хранит собственно значение. С другой, объект-контейнер, в отличие от обычной переменной, уже является сущностью «первого класса», то есть может быть передан в качестве аргумента функции или возвращён из неё в качестве результата и тп. Это иногда полезно, но в подавляющем большинстве случаев — излишне. Куда больше пользы, если реализации интерфейсов отличаются от стандартных:

    	var title = new $jin.prop.proxy({
    		put : function( next ) {
    			document.title = next
    		},
    		pull : function( ) {
    			return document.title
    		},
    	})
    	title.set( 'Hello!' ) // записать значение
    	title.get() // прочитать значение
    

    image

    $jin.prop.proxy — реализация контейнера без состояния, который может быть как «обычной переменной» так и «свойством объекта»:

    	var doc = {
    		get title( ) {
    			return new $jin.prop.proxy({
    				put : function( next ) {
    					document.title = next
    				},
    				pull : function( ) {
    					return document.title
    				},
    			})
    		}
    	}
    	doc.title.set( 'Hello' ) // записать значение
    	doc.title.get() // прочитать значение
    

    image

    В данном случае интерфейс get вызывает обработчик pull, а set — put. Такая замена сделана не спроста — в общем случае это действительно совершенно разные интерфейсы. Чтобы понять разницу достаточно ввести состояние и добавить очевидные условия:
    1) get вызывает pull только если значение ещё не установлено, иначе просто возвращает его — так называемая «ленивая инициализация»
    2) set вызывает put только если устанавливаемое значение отличается от текущего — это предотвращает исполнение put вхолостую.

    Например, если мы работаем с названием документа только через наш контейнер, то можем определить его так, чтобы лишний раз не обращаться к медленному браузерному api:

    	var doc = {
    		get title( ) {
    			return new $jin.prop.vary({
    				owner : this,
    				name : '_title',
    				put : function( next ) {
    					document.title = next
    				},
    				pull : function( ) {
    					return document.title
    				},
    			})
    		}
    	}
    	doc.title.set( 'Hello' ) // создать контейнер и записать значение
    	doc.title.get() // прочитать значение
    	doc.title.update() // принудительно актуализировать значение
    

    image

    Если в последних двух примерах вас смутило столь громоздкое определение свойства, то позвольте рассказать почему оно именно такое. В данном случае его можно было бы определить и проще:

    	var doc = {
    		title : new $jin.prop.vary({
    			put : function( next ) {
    				document.title = next
    			},
    			pull : function( ) {
    				return document.title
    			},
    		})
    	}
    	doc.title.set( 'Hello' )
    

    image

    Так и стоит делать для свойств, которым не нужна возможность наследования. Но если вы так объявите свойство в прототипе класса, то все инстансы будут работать с одним и тем же контейнером, что обычно совсем не то, что надо. А надо, чтобы у каждого инстанса были свои контейнеры. Для этого мы создаём контейнер через геттер и передаём ему ссылку на объект и имя поля в нём — именно в него контейнер и будет сохранять свои данные (или сохранит самого себя — зависит от реализации). Другой яркий пример использования подобного геттера — ленивый реестр, с произвольным числом ключей:

    	var info = {
    		item : function( key ) {
    			return new $jin.prop.vary({
    				owner : this,
    				name : '_item:' + key,
    				pull : function( ) {
    					return 0
    				},
    			})
    		}
    	}
    	info.item( 'foo' ).get() // 0
    	info.item( 'bar' ).set( 123 )
    	info.item( 'bar' ).get() // 123
    

    image

    И, наконец, частая ситуация — делегирование другому свойству:

    var user = {
        get name ( ) {
            return new $jin.prop.vary({
                owner : this ,
                name : '_name' ,
                pull : function( prev ) {
                    return 'Anonymous'
                }
            })
        }
    }
    
    var app = {
        get userName ( ) {
            return user.name
        }
    }
    
    app.userName.get() // Anonymous
    app.userName.set( 'Alice' ) // Anonymous
    app.userName.get() // Alice
    

    image

    Реактивные свойства


    Итак, теперь мы готовы создать свой первый атом:

    	var message = new $jin.atom.prop( {
    		notify : function( next, prev ) {
    			document.body.innerText = next
    		},
    		fail : function( error ) {
    			document.body.innerText += ' ' + error.message
    		},
    	} )
    	
    	message.push( 'Hello' ) // записать значение
    	message.fail( new Error( 'Exception' ) ) // записать объект исключения
    

    image

    Тут всё просто — когда мы меняем значение атома, немедленно вызывается функция notify (или fail), в которой мы можем императивно отразить изменение состояния на ооп-рантайм. В норме, код FRP приложения практически не нуждается в подобных ручных синхронизациях — от большинства из них легко избавиться путём декларативного описания вёрски, по которому уже автоматически и генерируются подобные синхронизирующие атомы. Но это тема отдельной большой статьи, так что далее мы сконцентрируемся на возможностях самих атомов.

    Атом является обобщением над «обещанием», так что не удивительно, что он поддерживает и thenable интерфейс:

    	var message = new $jin.atom.prop({})
    	message.then( function( next ) {
    		document.body.innerText = next
    	}, function( error ) {
    		document.body.innerText += ' ' + error.message
    	} )
    	
    	message.push( 'Hello' ) // записать значение
    	message.fail( new Error( 'Exception' ) ) // пока никто не заметил, поменять значение на объект исключения
    

    image

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

    Метод then возвращает атом, который слушает исходный атом и когда тот принимает не undefined значение — вызывает обработчик и самоуничтожается.

    А теперь, наконец, FRP в действии:

    	var user = {
    		firstName : new $jin.atom.prop({ value : 'Alice' }),
    		lastName : new $jin.atom.prop({ value : 'McGee' }),
    		getFullName : function(){ // по хорошему тут для единообразия надо использовать fullName : new $jin.prop.proxy(...)
    			return user.firstName.get() + ' ' + user.lastName.get()
    		}
    	}
    	
    	var message = new $jin.atom.prop( {
    		pull : function( ) {
    			return 'Hello, ' + user.getFullName()
    		},
    		notify : function( next , prev ) {
    			document.body.innerText = next
    		},
    		reap : function( ) { }
    	} )
    	message.get()
    
    	user.firstName.push( 'Alice' ) // установить значение
    	setTimeout( function( ) {
    		user.lastName.push( 'Bob' ) // обновить значение
    	}, 1000 )
    

    image

    Тут в целом всё просто: message неявно объявляется как функция от свойств user.firstName и user.lastName, и, когда хотябы одно из них меняется, то меняется и message, и это отражается на документе. Особенностей тут две:
    1. Атомы ленивы. Пока их кто-нибудь не дёрнет (через get или pull) — они будут неактивны.
    2. Атомы склонны к суициду. Если не переопределить поведение reap, то атомы будут уничтожать себя, высвобождая память, когда не остаётся ни одного, зависящего от них атома.

    Давайте реализуем атом, который будет следить за координатами указателя:

    	// провайдер координат указателя
    	var pointer = {
    		handler : function( event ) {
    			
    			var point = event.changedTouches ? event.changedTouches[0] : event
    			
    			// координаты указателя из события сохраняем в атом
    			pointer.position.push([ point.clientX , point.clientY ])
    			
    			event.preventDefault()
    			
    		},
    		position : new $jin.atom.prop( {
    			pull : function( prev ) {
    				
    				// подписываемся на все необходимые события
    				document.body.addEventListener( 'mousemove' , pointer.handler , false )
    				document.body.addEventListener( 'dragover' , pointer.handler , false )
    				document.body.addEventListener( 'touchmove' , pointer.handler , false )
    				document.body.addEventListener( 'pointermove' , pointer.handler , false )
    				
    				// возвращаем дефолтное значение, пока нет актуальных данных
    				return [ -1, -1 ]
    				
    			},
    			reap : function( ) { // когда никто не подписан на изменения
    				
    				// отписываемся от дом-событий
    				document.body.removeEventListener( 'mousemove' , pointer.handler , false )
    				document.body.removeEventListener( 'dragover' , pointer.handler , false )
    				document.body.removeEventListener( 'touchmove' , pointer.handler , false )
    				document.body.removeEventListener( 'pointermove' , pointer.handler , false )
    				
    				// очищаем значение, что приведёт к вызову pull при следующем запросе значения координат
    				pointer.position.clear()
    				
    			}
    		} )
    	}
    
    	// принтер координат в документ
    	var title = new $jin.atom.prop( {
    		pull : function( ) {
    			return 'Mouse coords: ' + pointer.position.get()
    		},
    		notify : function( next , prev ) {
    			document.body.innerText = next
    		},
    		reap : function( ) { }
    	} )
    	title.pull()
    	
    	// через 5 секунд перестаём обновлять коодинаты
    	setTimeout( function( ) {
    		title.disobeyAll()
    	}, 5000 )
    

    image

    Типизированные атомы


    Иногда при изменении значения атома требуется особая логика, отличная от базовой «новое зачение заменяет старое». Например, если в атоме хранится инстанс Date, то при вставке в атом было бы не плохо проверить. а действительно ли он указывает на другую метку времени. Делается это через переопределение интерфейса merge:

    	var lastUpdated = new $jin.atom.prop( {
    		merge : function( next , prev ) {
    			if( !prev ) return next
    			if( prev.getTime() === next.getTime() ) return prev
    			return next
    		},
    		notify : function( next , prev ) {
    			document.body.innerText += next.getFullYear()
    		}
    	} )
    	
    	lastUpdated.push( new Date( 2014 , 1 , 1 ) ) // добавит в документ 2014
    	lastUpdated.push( new Date( 2014 , 1 , 1 ) ) // будет проигнорировано
    	lastUpdated.push( new Date( 2015 , 1 , 1 ) ) // добавит в документ 2015
    

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

    	var userInfo = new $jin.atom.prop( {
    		value : {},
    		merge : function( next , prev ) {
    		
    			// обновляем данные
    			var updated = false
    			for( var key in next ) {
    				if( prev[ key ] === next[ key ] ) continue
    				prev[ key ] = next[ key ]
    				updated = true
    			}
    			
    			// уведомляем подписчиков, что есть изменения
    			if( updated ) this.notify()
    			
    			return prev
    		}
    	})
    	
    	userInfo.push({ firstName : 'Alice' })
    	userInfo.push({ lastName : 'McGee' })
    	userInfo.get() // { firstName: "Alice", lastName: "McGee" }
    

    В главе про свойства были перечислены основные интерфейсы переменных и свойств, но есть и множество других:

    	a ++ // получить значение увеличить его на 1 и записать обратно
    	a += N // получить значение увеличить его на N и записать обратно
    	// аналогичные интерфейсы для других математических операций
    

    Эти интерфейсы предназначены для примитивов. Поведение их жёстко задано и не поддаётся переопределению. Но у нас то кастомные контейнеры! Давайте напишем свой контейнер для числовых значений:

    	module $jin.atom {
    		
    		export class numb < OwnerType extends $jin.object > extends $jin.atom.prop < number , OwnerType > {
    			
    			summ( value ) {
    				this.set( this.get() + value )
    			}
    	
    			multiply( value ) {
    				this.set( this.get() * value )
    			}
    	
    			// и другие клёвые методы
    		}
    		
    	}
    
    	var count = new $jin.atom.numb({ value : 5 }) // создаём контейнер со значением
    	count.summ( -1 ) // уменьшили значение на 1
    	count.multiply( 2 ) // затем увеличили вдвое
    	count.get() // получили текущее значение (8)
    

    Тут в примере уже используется TypeScript так как наследование в JavaScript не слишком наглядно из-за чего в каждом фреймворке есть свой хелпер реализующий оное. Вы можете использовать и их, так как и $jin.atom.prop и $jin.atom.numb и все остальные — самые обычные яваскриптовые «функции с прототипом».

    Но мы не ограничены одними примитивами — полезно, например, иметь атомы для коллекций:

    	module $jin.atom {
    		
    		// атом для списков
    		export class list<ItemType,OwnerType extends $jin.object> extends $jin.atom.prop<ItemType[],OwnerType> {
    			
    			// проверяем, а действительно ли новый список отличается от старого
    			merge( next : ItemType[] , prev : ItemType[] ) {
    				next = super.merge( next , prev )
    				
    				if( !next || !prev ) return next
    				if( next.length !== prev.length ) return next
    				
    				for( var i = 0 ; i < next.length ; ++i ) {
    					if( next[ i ] !== prev[ i ] ) return next
    				}
    				
    				return prev
    			}
    			
    			// добавляет элементы в конец списка
    			append( values : ItemType[] ) {
    				var value = this.get()
    				value.push.apply( value, values )
    				
    				this.notify( null , value ) // приходится вызывать вручную так как мы поменяли внутренности объекта
    			}
    	
    			// добавляет элементы в начало списка
    			prepend( values : ItemType[] ) {
    				var value = this.get()
    				value.unshift.apply( value, values )
    				this.notify( null , value )
    			}
    	
    			// и другие клёвые методы
    			
    		}	
    		
    	}
    	
    	var list = new $jin.atom.list({ value : [ 3 ] })
    	list.append([ 4 , 5 ])
    	list.prepend([ 1 , 2 ])
    	list.get() // [ 1 , 2 , 3 , 4 , 5 ]
    

    Резюме


    Ну что ж, пришло время попробовать самим. Но прежде, я должен предупредить, что проект живёт на чистом энтузиазме, разработывается в свободное от основной работы время, одним человеком, без какого-либо комьюнити или инвестиций, так что не имеет исчерпывающей документации, кучи примеров, мануалов и ответов на StackOverflow. Если вас заинтересовала эта тема — не стесняйтесь задавать вопросы, сообщать о косяках, высказывать идеи или даже присылать патчи.

    Собранная JS библиотека ~ 27КБ без сжатия
    Исходники на TypeScript
    Заготовка на JSFiddle

    Основные классы:
    $jin.prop.proxy — свойство без состояния
    $jin.prop.vary — свойство с состоянием
    $jin.atom.prop — реактивное свойство

    Параметры конструктора (все опциональны):
    owner — владелец атома, который должен иметь глобальный уникальный идентификатор в поле objectPath
    name — имя атома, уникальное в рамках владельца
    value — исходное значение
    get( value: T ): T — вызывается при каждом запросе значения, по умолчанию проксирует параметр
    pull( prev: T ): T — вызывается для «втягивания» значения из ведущих состояний (например, из сервера), по умолчанию возвращает текущее значение
    merge( next: T, prev: T ): T — вызывается для валидации и/или слияния нового значения с текущим, по умолчанию возвращает новое значение
    put( next: T, prev: T ): void — обратная к pull операция, передача нового значения в ведущие состояния (например, на сервер), по умолчанию записывает новое значение в атом
    reap(): void — вызывается. когда на атом никто не подписан и его можно безболезненно удалить, что и делает по умолчанию
    notify( next: T, prev: T ): void — вызывается, когда текущее значение меняется, по умолчанию ничего не делает
    fail( error: Error ): void — вызывается, когда вместо текущего значения сохранен объект исключения

    Основные методы атомов:
    get() — получить значение
    pull() — принудительно вычислить значение
    update() — запланировать обновление значения
    set() — предложить новое значение (которое он может не в себя записать а в ведущее состояние)
    push() — принудительно записать новое значение
    fail( error ) — принудительно записать объект исключения
    mutate( ( prev: T ) => T ) — применить функцию трансформации
    then( ( next: T1 ) => T2 ) — выполнить функцию, когда атом примет актуальное значение
    catch( ( error: Error ) => T2 ) — выполнить функцию, когда атом примет объект исключения

    image
    В заключение хотелось бы спросить, о чём лучше написать далее?
    • 13.1%Внутреннее устройство атомов с разбором реального кода8
    • 68.8%Сравнение с другими популярными решениями (Angular, KO, Meteor, Backbone, Basis, предлагайте свои)42
    • 18%Реактивный MVP и разделение приложения на слои11

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 58
    • +4
      Глядя на содержимое репозитория и на скорость обновления компонентов: nin-jin мсье действительно профессиональный велосипедист и знает толк… Снимаю шляпу.
      • 0
        Сама концепция мне чем-то напоминает подход Reflux. Только есть еще дополнительные фичи. Где $jin.atom.prop это Reflux.store, а $jin.method это Reflux.action. Интересно, можно ли эту схему использовать для резолва моделей на бекенде, которые ожидают загрузки данных от других моделей.
        • 0
          Конечно можно, я активно это использую как на клиенте так и на сервере. Правда $jin.atom не очень дружит с node-fibers.
        • 0
          Альтернатив у TypeScript три: есть еще AtScript
          • +3
            Покажите мне хотя бы один репозиторий на github, написанный Atscript, кроме Atscript-playground. А пока только еще один buzz-word, наравне с другими *script (coffee, live, pure).
          • +3
            Это нечестно! После первой фразы я ожидал чего-то вроде «как приспособить веб-камеру для тюнинга заднего переключателя передач»…
            • 0
              А название статьи не смутило?
            • 0
              Вот еще очень интересный подход использования реактивных патернов в MV* staltz.com/mvi-freaklies/
              • 0
                На мой взгляд, MVP лучше справляется с задачей разделения приложения на слои.
              • –1
                TypeScript — это практически тот же JavaScript, но с классами, выведением типов и лямбдами.
                В JS есть классы и лямбды.
                • 0
                  Логически, безусловно есть, но синтаксически их нет.
                  • 0
                    Здрасьте. Как нет? Вот вам лямбда (у неё нет имени):

                    alert(1 + function() { return 1 });

                    Вот объект (у него есть собственный контекст):

                    new function() { alert(this) }
                    • 0
                      Лямбда — не просто анонимная функция, у неё есть ещё ряд свойств: ru.wikipedia.org/wiki/%D0%9B%D1%8F%D0%BC%D0%B1%D0%B4%D0%B0-%D0%B2%D1%8B%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F

                      Про объекты я и не спорю. Но классов синтаксически (чтобы прямо так и написать «объявляю такой-то класс с такими-то полями») — нет. Безусловно на логическом уровне классы всё же есть.
                      • +1
                        Итак, каких же свойств лямбд не хватает в JS? Из ссылки, которую вы мне дали, мне выводов об отсутствии чего-либо сделать не удалось.

                        Ну ок, пусть синтаксически классов там нет, не понимаю почему это плохо.
                        • 0
                          Плохо это исключительно отсутствием вменяемого автодополнения в IDE. И если тот факт, что «положенные» в объект свойства потом можно в нем же найти, IDE запомнить еще может — то на чем-то вроде атомов, где объект одного и того же типа (атом) может содержать произвольные значения, автодополнение станет совершенно бесполезным.
                          • 0
                            Давайте для начала с лямбдами разберёмся. Что с ними не так?
                            • 0
                              С лямбдами все так, особенно в последней редакции.
                              • –1
                                С лямбдами с самого начала так всё было. Теперь, что касается классов. Изначально я не согласился с утверждением, что их нет, с этой точкой зрения я готов спорить и дальше. Что какое-то IDE не умеет что-то там подсказывать в языке, мне на это наплевать, если честно. Я за точность формулировок.

                                И фраза «TypeScript — это практически тот же JavaScript, но с классами, выведением типов и лямбдами» попросту лжёт в отношении JS. В JS есть и классы, и лямбды, да и выведение типов, кстати, тоже есть (что-то я первый раз пропустил этот момент).
                                • 0
                                  Я думаю имеется в виду подобное описание интерфейса:
                                  github.com/borisyankov/DefinitelyTyped/blob/master/node/node-0.11.d.ts
                                  Которое позволяет максимально точно дополнять код с учетом типа аргументов и подсказыванием какие именно аргументы ожидаются на вход.
                                  • 0
                                    Имеются ввиду вот такие клёвые штуки:
                                    image
                            • 0
                              Согласен, тезис про короткую запись в виде одного выражения находится в разделе .NET. Но в JS меня всё же смущает, что this начинает указывать в стратосферу. Лямбды на мой взгляд должны быть полностью эквивалентны выполнению того же кода без создания анонимной функции.

                              Это не то чтобы плохо, но наличие соответствующего синтаксиса здорово упрощает жизнь.
                    • 0
                      Никак не соберусь написать статью, поэтому просто оставлю тут ссылку: моя реализация атомов для c#

                      Ключевые отличия: из-за многопоточной природы c# нельзя так просто сначала обновить все затронутые атомы — а потом разрешить продолжить работу. Поэтому приходится сначала переводить в состояние DIRTY все потенциально затронутые атомы — а потом уже разбираться, какие из них надо обновлять, а какие нет.

                      Зато есть и преимущества: наличие в языке слабых ссылок позволяет сборщику мусора собрать неиспользуемые атомы, в то время как в Js надо уничтожать атомы явно.
                      • 0
                        Потенциально затронутые — это включая те, что зависят от данного атома даже через промежуточные? Без этого точно никак во многопоточной среде?

                        Это да, мягкие ссылки бы капитально упростили реализацию.
                        • 0
                          Фактически, надо сделать выбор между однопоточным пересчетом всех измененных атомов — или многопоточным, но с предварительной расстановкой состояния DIRTY. Второй может оказаться как быстрее, так и медленнее, в зависимости от особенностей приложения. Я еще подумаю, можно ли как-нибудь безболезненно между этими режимами переключаться.
                          • 0
                            Зачем? Пусть первый обратившийся к атому поток начнёт его пересчёт, пометив, что он пересчитывается и другим потокам нужно просто подождать. А когда идёт реактивное обновление — аналогично. Один поток пересчитывает, а другие ждут. При этом каждый атом может пересчитываться разными потоками. Или я опять что-то не догоняю и лучше всё же дождаться статьи?
                            • 0
                              В таком случае не удается добиться согласованности атомов.

                              Допустим, один поток мы вычисляет атом A, при этом он обратился к атому B, который неявно зависит от C. Атом C изменился — но B об этом еще не знает, поэтому A получает старое значение B.
                              Теперь атом A запросил значения атома D, который тоже зависит от C, но уже успел измениться. Итог — на вход атому A поступили несогласованные друг с другом значения атомов B и D.

                              Поэтому надо либо выполнять обновление всех атомов «снизу», запрещая любые посторонние вычисления состояний атомов во время процесса пересчета (однопоточная модель, как сделано у вас) — либо немедленно уведомлять «верхние» атомы о возможных изменениях (как сделано у меня). Третьего пути нет.
                              • 0
                                На самом деле, в синхронном варианте есть та же проблема — если есть два пути (длинный и короткий) и мы вычисляем атом, то по одному пути мы получим актуальное значение, а по другому возможно устаревшее. Но каскадно помечать все ведомые атомы грязными — как-то брутально. В идеале было бы при вычислении атома быстро определять, что среди ведущих на есть неактуализированные и сначала вычислить их и все промежуточные до текущего, а потом уже продолжать вычисление текущего.
                                • 0
                                  Если мы делаем обновление атомов снизу — то эта проблема решаема. Достаточно запретить возвращать значение атома, пока существуют невычисленные атомы ниже уровнем.

                                  PS вы что, не так сделали?
                                  • 0
                                    Разумеется так, но бывают случаи когда кто-то сразу ломится в далекий ведомый атом, до которого обновления ещё не дошли и получается такая фигня.
                                    • 0
                                      Достаточно запретить возвращать значение атома, пока существуют невычисленные атомы ниже уровнем.
                                      • 0
                                        Ну да. Только как быстро определять что есть невычисленные низкоуровневые атомы?
                                        • 0
                                          Как-как, по номеру уровня же…
                                          • 0
                                            По номеру уровня мы найдем вообще все атому ниже уровнем, а не те, что хоть как-то влияют на текущий.
                                            • 0
                                              А нам и не требуется находить строго влияющие на текущий. Нам же в любом случае вычислять все атомы — так какая разница, в каком порядке это делать?
                                              • 0
                                                Тогда у нас KO может получиться с немедленным обновлением всех зависимостей.
                                                • 0
                                                  Тогда встречный вопрос: в каком вообще случае может понадобиться не обновлять атом?
                                                  • 0
                                                    Только в одном случае — когда не все новые изменения пропушены в ведущие атомы. В данном случае это достаточно редкий кейс, да.
                                                  • 0
                                                    PS я не говорю, что атомы должны обновляться немедленно после изменения одного из них — они должны обновляться либо при простое — либо при добавлении нового атома.
                        • –1
                          Привет, Дима!
                          • –1
                            Черт, Валя меня опередил :)
                            • 0
                              Привет, Валя, давай сольём наши кармы личной перепиской в комментах! :-)
                            • 0
                              А как вы добились что из такой функции автоматом вычисляется от кого зависит атом в данный момент?

                              function( ) {
                                          if( config.get() ) {
                                              return 'Mouse coords is ' + coords.get()
                                          } else {
                                              return 'Mouse target is ' + target.get()
                                          }
                                      }
                              
                              • 0
                                Предположу что так: если вызвать этот метод, то при этом вызовутся 2 из 3-х влияющих метода, например «config.get() -> coords.get()», после чего строим «зависимость» от этих 2-х. После того как config.get() изменит значение, происходит очередной вызов этого метода и обработка может пройти через «config.get() -> target.get()», — меняем зависимость на эти два, и т.д.
                                Т.е. после каждого изменения значения в атомах от которых зависит текущий, вызывается текущий атом для вычисления значения и при этом проверяются зависимости и перестраиваются при необходимости.

                                Подобным образом работает (работал в 1.x) ko.computed из Knockout.js. Это создает много работы, и может проигрывать по скорости методу «dirty checking» который используется в Angular.
                                • 0
                                  Зря вы предполагаете — если бы вы прочитали прошлую статью автора, то совершенно точно знали бы, что так оно и есть.
                                  • 0
                                    В текущей версии KO мало что на эту тему поменялось. Там действительно делается много работы, особенно при массовых обновлениях данных. В Angular, однако, не меньше — digest может прогоняться по многу раз до стабилизации состояния. Схематично я изобразил это тут: nin-jin.github.io/slide/#slide=induction
                                  • 0
                                    Видимо мне стоило более явно выделить ссылку на предыдущую статью, где разбираются основные принципы работы атомов: habrahabr.ru/post/235121/

                                    Если вкратце — в глобальной переменной $jin.atom.currentSlave сохраняется текущий вычисляемый атом и когда у другого атома запрашивают значение — он смотрит в эту переменную и связывает себя с ним.
                                  • +1
                                    По поводу классификации FRP библиотек есть очень хороший доклад от автора Elm www.youtube.com/watch?v=Agu6jipKfYw
                                    • 0
                                      Тут важно иметь ввиду ограничения обещаний:
                                      1. обработчик вызывается отложенно


                                      Вопрос, что в этом плохого? Как работает ваша реализация атомов? Изменения распространяются синхронно без отложенных вызовов?
                                      • 0
                                        Ничего, просто это надо иметь ввиду. В моей реализации notify вызывается синхронно, но при инвалидации атомы обновляют свои значения отложенно.
                                      • 0
                                        vintage Будет ли нормальная документация с примерами?
                                        • 0
                                          Будет полностью другая реализация :-)
                                          • 0
                                            ждемс =)
                                            • 0
                                              Что скажете про это? Похоже тоже атомы) github.com/ds300/havelock
                                              • 0
                                                Это тот же KnockOut, только на ES6. Всё построено на замыканиях, а значит будет большое потребление памяти и тормоза от GC при большом числе атомов. Обновления происходят немедленно, а не отложенно (чтобы отложить обновление приходится заворачивать код в транзакцию, но это частичный костыль). Динамичесное изменение зависимостей я так понял реализуется лишь через спец-функции типа ifThenElse, or, and, not.
                                                • 0
                                                  Так и не смог сделать адекватную реализацию автосоздания субклассов с поддержкой статической типизации. Всё, что получается — либо теряет информацию о типах, либо требует писать много типового кода. Так что в следующей версии атомов тоже будут замыкания.
                                                  • 0
                                                    что там с вашим атомом?) просто вменяемой доки нет
                                                    • 0
                                                      Вылизываю :-) Надо ещё идею свойств доработать, чтобы компоненты удобно создавать.

                                                      Riim так и не обновил результаты бенчмарков :-(
                                                    • 0

                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.