- PVSM.RU - https://www.pvsm.ru -
Идея написания статьи появилась в этой ветке [1], может кому-то будет интересно и её почитать. Сразу скажу, писатель (в том числе кода) из меня так себе, но я буду стараться.
Писать будем как обычно тудулист, надоел конечно до чёртиков, но что-то лучшее для демонстрации придумать сложно. Сразу ссылка на работающее приложение: жмяк [2] (код [3]).
И сразу в бой, начнём с хранилища. Единственный тип необходимый для этого приложения — Todo:
import { EventEmitter } from 'cellx';
import { observable } from 'cellx-decorators';
export default class Todo extends EventEmitter {
@observable text = void 0;
@observable done = void 0;
constructor(text, done = false) {
super();
this.text = text;
this.done = done;
}
}
Тут всё предельно просто, парочка наблюдаемых полей, одно содержит текст задачи, другое статус её выполнения.
Наследование от cellx.EventEmitter
необходимо на случай если в дальнейшем понадобится подписаться на изменения какого-то поля:
todo.on('change:text', () => {/* ... */});
В данном приложении такого нет и наследование можно убрать, я просто зачем-то всегда пишу его заранее.
Теперь напишем корневое хранилище:
import { EventEmitter, cellx } from 'cellx';
import { observable, computed } from 'cellx-decorators';
import Todo from './types/Todo';
class Store extends EventEmitter {
@observable todos = cellx.list([
new Todo('Primum', true),
new Todo('Secundo'),
new Todo('Tertium')
]);
@computed doneTodos = function() {
return this.todos.filter(todo => todo.done);
};
}
export default new Store();
Здесь уже поинтереснее. Используется cellx.list [4] (алиас для new cellx.ObservableList
) — наблюдаемый список, наследует от cellx.EventEmitter
и при любом своём изменении генерирует событие change
. Наблюдаемое поле получая в качестве значения что-то наследующее от cellx.EventEmitter
подписывается на его change
и тоже изменяется при этом событии. Всё это значит, что не обязательно использовать встроенные коллекции, можно сделать свои унаследовав их от cellx.EventEmitter
. Из коробки есть cellx.list
и cellx.map [5]. Отдельным модулем есть индексируемые версии обоих коллекций: cellx-indexed-collections [6].
Ещё один новенький — декоратор computed
, вычисляемые поля — это самая суть cellx-a — вы просто пишите формулу вычисляемого поля, вам не нужно самому подписываться на done
каждого todo при его добавлении и отписываться от него же при удалении, всё это делает cellx пока вы не видите, вам остаётся расслабиться и получать удовольствие описывая самую суть. При этом описание происходит, можно сказать, в декларативном виде — уже не нужно думать о событиях и о том как изменения будут распространяться по системе, всё пишется так, как будто отработает лишь раз. Кроме того cellx очень умный и автоматически делает некоторые хитрые оптимизации: динамическая актуализация зависимостей [7] и схлопывание и отбрасывание событий [8] не допустят избыточных расчётов и обновлений интерфейса. Если делать всё это вручную, код получается довольно объёмным, но, что намного хуже — глючным. Отладкой же cellx-а заниматься приходиться раз в сто лет, он просто работает.
Переходим к слою отображения. Сначала компонент задачи:
import { observer } from 'cellx-react';
import React from 'react';
import toggleTodo from '../../actions/toggleTodo';
import removeTodo from '../../actions/removeTodo';
@observer
export default class TodoView extends React.Component {
render() {
let todo = this.props.todo;
return (<li>
<input type="checkbox" checked={ todo.done } onChange={ this.onCbDoneChange.bind(this) } />
<span>{ todo.text }</span>
<button onClick={ this.onBtnRemoveClick.bind(this) }>remove</button>
</li>);
}
onCbDoneChange() {
toggleTodo(this.props.todo);
}
onBtnRemoveClick() {
removeTodo(this.props.todo);
}
}
Здесь из новенького — декоратор observer
из модуля cellx-react [9]. Грубо говоря, он просто делает метод render
вычисляемой ячейкой и вызывает React.Component#forceUpdate [10] при её изменении.
Остаётся корневой компонент приложения:
import { computed } from 'cellx-decorators';
import { observer } from 'cellx-react';
import React from 'react';
import store from '../../store';
import addTodo from '../../actions/addTodo';
import TodoView from '../TodoView';
@observer
export default class TodoApp extends React.Component {
@computed nextNumber = function() {
return store.todos.length + 1;
};
@computed leftCount = function() {
return store.todos.length - store.doneTodos.length;
};
render() {
return (<div>
<form onSubmit={ this.onNewTodoFormSubmit.bind(this) }>
<input ref={ input => this.newTodoInput = input } />
<button type="submit">Add #{ this.nextNumber }</button>
</form>
<div>
All: { store.todos.length },
Done: { store.doneTodos.length },
Left: { this.leftCount }
</div>
<ul>{
store.todos.map(todo => <TodoView key={ todo.text } todo={ todo } />)
}</ul>
</div>);
}
onNewTodoFormSubmit(evt) {
evt.preventDefault();
let newTodoInput = this.newTodoInput;
addTodo(newTodoInput.value);
newTodoInput.value = '';
newTodoInput.focus();
}
}
Здесь ещё парочка вычисляемых полей, отличаются от Store#doneTodos
они лишь тем, что поля из которых они вычисляются лежат не на текущем экземпляре (this
) а где-то в другом месте, cellx никак не ограничивает в этом плане, эти поля можно спокойно переместить в Store
и всё так же будет работать. Определять, где должно лежать поле лучше по его сути — если поле специфично для какого-то определённого компонента, то пусть в нём и вычисляется, светиться в общем хранилище ему нет смысла. В данном случае я бы #leftCount
перенёс в хранилище, а #nextNumber
вполне неплохо смотриться и здесь.
В экшенах cellx никак не используется, поэтому я максимально их упростил, получился даже не Flux, а какой-то MVC в терминах Flux-а. Надеюсь вы мне простите это упрощение.
В данном случае приложение совсем простое и написать его так же просто можно и без cellx-а (никаких подписок на каждый done
здесь не потребуется), при дальнейшем же усложнении связей в приложении сложность их описания на cellx-e растёт линейно, без него — обычно нет и в какой-то момент приходим к мешанине событий в которой без поллитра не разобраться. Для решения проблемы, кроме реактивного программирования, есть и другие подходы со своими плюсами и минусами, но их сравнение — уже другая история (если кратко, как минимум они проигрывают из-за большого количества лишних вычислений и, как результат, более низкой производительности).
В общем-то по коду это всё, ещё раз ссылка на результат: жмяк [2] (код [3]).
Чаще всего спрашивают отличия от MobX [11]. Это наиболее близкий аналог и отличий немного:
disposer
-ом. Из минусов autorun
-а — инициализирующий запуск колбека часто вообще не в тему. Тут отличия более существенны. Отставание в скорости ещё больше, но важнее не это. Эти библиотеки предлагают создавать вычисляемые ячейки несколько иначе, в, наверное, более функциональном виде. То, что на cellx-e будет выглядеть так:
var val2 = cellx(() => val() + 1);
На этих библиотеках превратиться в что-то вроде (псевдокод, как там точно я не помню, да и не суть):
var val2 = val.lift(add(1));
Плюс в более красивом, человекочитаемом коде, минус в заметно большем пороге входа, так как теперь нужно запомнить 100500 методов на все случаи жизни (можно конечно обходиться и каким-то минимальным набором).
В тоже время в cellx-е есть возможность добавить ячейкам свои методы и ничто не мешает довести его до уровня этих библиотек, можно сказать, что он более низкоуровневый.
Вопросы по библиотеке и идеи по её дальнейшему развитию принимаются на гитхабе [19].
Благодарю за внимание.
Автор: Riim
Источник [20]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/200569
Ссылки в тексте:
[1] этой ветке: https://habrahabr.ru/post/308782/#comment_9779664
[2] жмяк: https://riim.github.io/react-cellx-todo-app/
[3] код: https://github.com/Riim/react-cellx-todo-app
[4] cellx.list: https://github.com/Riim/cellx/blob/master/README.ru.md#cellxobservablelist
[5] cellx.map: https://github.com/Riim/cellx/blob/master/README.ru.md#cellxobservablemap
[6] cellx-indexed-collections: https://github.com/Riim/cellx-indexed-collections
[7] динамическая актуализация зависимостей: https://github.com/Riim/cellx/blob/master/README.ru.md#Динамическая-актуализация-зависимостей
[8] схлопывание и отбрасывание событий: https://github.com/Riim/cellx/blob/master/README.ru.md#Схлопывание-и-отбрасывание-событий
[9] cellx-react: https://github.com/Riim/cellx-react
[10] React.Component#forceUpdate: https://facebook.github.io/react/docs/component-api.html#forceupdate
[11] MobX: https://github.com/mobxjs/mobx
[12] быстрее: https://github.com/Riim/cellx/blob/master/README.ru.md#Тест-производительности
[13] атомы: http://habrahabr.ru/post/235121/
[14] синхронизация значения с синхронным хранилищем: https://github.com/Riim/cellx/blob/master/README.ru.md#Синхронизация-значения-с-синхронным-хранилищем
[15] синхронизация значения с асинхронным хранилищем: https://github.com/Riim/cellx/blob/master/README.ru.md#Синхронизация-значения-с-асинхронным-хранилищем
[16] про pull: https://github.com/Riim/cellx/issues/7#issuecomment-245661130
[17] пассивный режим: https://github.com/Riim/cellx/blob/master/README.ru.md#dispose-или-как-убить-ячейку
[18] autorun: https://mobxjs.github.io/mobx/refguide/autorun.html
[19] гитхабе: https://github.com/Riim/cellx/issues/new
[20] Источник: https://habrahabr.ru/post/313038/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.