ReactJs. Разбор кода todomvc

Для того, чтобы лучше понять как выглядит реальное приложение на React я решил разобрать код todomvc. Рассматриваю самую свежую версию кода, которая доступна на момент написания статьи. Если ссылка не работает, то можно скачать архив отсюда. Для чтения статьи очень желательно ознакомится хотя бы с базовой информацией и примерами с данной страницы официального сайта.  Ну и лично мне сильно помог кусок данной книги. Надеюсь у нее будет продолжение (пытался найти оригинал — не нашел).

Реализация todomvc для ReactJs состоит из index.html, папки node_modules и папки js. Индексный файл практически пуст. В нем есть только подключение  других файлов и главная секция

<section class="todoapp"></section>

Которая совершенно не информативна. В node_modules содержатся нужные библиотеки, а само сердце приложение находится в папке js. Там у нас имеются следующие файлы

  • app.jsx
  • footer.jsx
  • todoItem.jsx
  • todoModel.js
  • utils.js

Начнем с главного файла — app.jsx. Читать его пожалуй лучше с конца. А там у нас следующее

var model = new app.TodoModel('react-todos');
 	 
function render() {
 	React.render(
 	<TodoApp model={model}/>,
 	document.getElementsByClassName('todoapp')[0]
 	);
}
 	 
model.subscribe(render);
render();

Как видно здесь идет создание модели, рендеринг TodoApp компонента и подписка компонента на события модели. Но только ведь нет в React двустороннего связывания и моделей тоже нет. И если мы посмотрим в файл todoModel.js, то увидим, что подписка не совсем честная. Callback-функции вызываются в методе модели inform(), который вызывается вручную в каждом методе, где модель меняется свое состояние так, что представлению нужно сделать обновленный рендеринг.

Используя тег <TodoApp> мы вызываем метод render() для компонента TodoApp , описанного внутри React.createClass({ … }).  Сам метод render() так же через теги JSX создает экземпляры компонентов TodoItem (отдельная запись в TODO) и TodoFooter (футер списка, в котором есть кнопки-фильтры и показа число пунктов в списке). Таким образом, здесь демонстрируется, что если у нас описаны компоненты через React.createClass(), то мы можем создавать их экземпляры через одноименные теги JSX и передавать параметры через атрибуты тегов.

Так в TodoItem передается большой список параметров

<TodoItem
 	key={todo.id}
 	todo={todo}
 	onToggle={this.toggle.bind(this, todo)}
 	onDestroy={this.destroy.bind(this, todo)}
 	onEdit={this.edit.bind(this, todo)}
 	editing={this.state.editing === todo.id}
 	onSave={this.save.bind(this, todo)}
 	onCancel={this.cancel}
 	/>

Вот как мы видим здесь происходит привязка событий onToggle (смена состояния пункта списка), onEdit (переход в режим редактирования пункта) и onSave (сохранение пункта списка). Но ведь мы знаем, что таких вот высокоуровневых событий нет в React. Верно. События эти опять не совсем честные (как и в случае с моделью). Чтобы удостовериться в этом давайте поищем в файле с описанием компонента todoItem.jsx вхождение этих строк. Мы увидим, что this.props.onSave(val) вручную вызывается в методе handleSubmit()this.props.onEdit() — в handleEdit(), а this.props.onToggle привязано к событию onChange для чекбокса пункта todo-списка. С последним понятно — это просто алиас onChange. А как дела с остальными? И тут

<label onDoubleClick={this.handleEdit}>
 	{this.props.todo.title}
</label>

мы видим, что handleEdit() — это обработчик двойного клика по label-у пункта списка, а здесь

<input
 	ref="editField"
 	className="edit"
 	value={this.state.editText}
 	onBlur={this.handleSubmit}
 	onChange={this.handleChange}
 	onKeyDown={this.handleKeyDown}
 	/>

видно, что handleSubmit — обработчик события потери фокуса (onBlur) и события нажатия клавиши Enter (через более сложную связь onKeyDown={this.handleKeyDown}).

По сути мы сейчас разобрали метод render() компонента TodoItem. Это метод будет вызываться каждый раз при отрисовке каждого из пунктов todo-списка. Так же здесь демонстрируется обращение через this.props ко всем данным, что были переданы через атрибуты тега TodoItem.

Отмечу еще интересную привязку обработчиков для событий TodoItem в TodoApp. Вид примерно следующий: onSave={this.save.bind(this, todo)}

Этот код выполняется внутри метода render() нашего TodoApp и привязывает его метод save() обработчиком для события компонента TodoItem. При изменении одного пункта списка сохраняется весь список. Поэтому это оправдано.

Кстати, о сохранении. Посмотрим на метод TodoApp.save()

save: function (todoToSave, text) {
 	this.props.model.save(todoToSave, text);
 	this.setState({editing: null});
}

Здесь вызывается метод save() для модели и затем устанавливается через setState() выводим список из состояния редактирования.

Модель же перерабатывает состояние данных, которые описывают состояние списка и вызывает метод inform()

app.TodoModel.prototype.save = function (todoToSave, text) {
 	this.todos = this.todos.map(function (todo) {
 	return todo !== todoToSave ? todo : Utils.extend({}, todo, {title: text});
 	});
 	 
 	this.inform();
};

Помните нашши псевдособытия модели?

Вот inform() и сохраняет наши данные в хранилище через базовый объект Utils

app.TodoModel.prototype.inform = function () {
 	Utils.store(this.key, this.todos);
 	this.onChanges.forEach(function (cb) { cb(); });
};

и вызывает по очереди все обработчики. Но в данном случае обработчик у нас один.

В TodoApp есть еще один интересный метод

componentDidMount: function () {
 	var setState = this.setState;
 	var router = Router({
 	'/': setState.bind(this, {nowShowing: app.ALL_TODOS}),
 	'/active': setState.bind(this, {nowShowing: app.ACTIVE_TODOS}),
 	'/completed': setState.bind(this, {nowShowing: app.COMPLETED_TODOS})
 	});
 	router.init('/');
}

Метод componentDidMount() — стандартный, так же как и render(), и вызывается если рендеринг прошел успешно, и настоящий DOM построен. Может использоваться для самый различных манипуляций. В том числе с сырым DOM. Но в данном случае этот метод используется для привязки роутинга. Как мы помним в React нет роутинга. Так и есть. Здесь используется отдельная библиотека director. Не трудно догадаться как это работает. Для каждого варианта url прописывается свой обработчик, который меняет состояние nowShowing для TodoApp и заставляет его перерисоваться снова. Да, еще один вызов render() после render(). Как в примере с таймером с официального сайта ReactJs.

В целом пример todomvc пропитан духом старого доброго ООП. При каких-либо событиях меняется состояние объекта, а отображение объекта обязано перерисоваться.

Вот при двойном клике на label отдельного пункта в todo-списке

<label onDoubleClick={this.handleEdit}>
   {this.props.todo.title}
</label>

У нас вызывается handleEdit(), который обращается к методу edit() для TodoApp, а в нем прописывается изменение состояния editing.

edit: function (todo) {
   this.setState({editing: todo.id});
}

Далее в handleEdit() идет изменение состояния editText для TodoItem

handleEdit: function () {
   this.props.onEdit();
   this.setState({editText: this.props.todo.title});
}

Итак пару переменных определяющих состояние приложения изменены. Теперь посмотрим на метод render() в TodoItem. И там у нас реакция на одну переменную (добавление класса для тега LI конкретного пункта списка)

<li className={React.addons.classSet({
   completed: this.props.todo.completed,
   editing: this.props.editing
})}>

И реакция на другую переменную (значение value для поля редактирования названия пункта todo-списка)

<input
   ref="editField"
   className="edit"
   value={this.state.editText}
   onBlur={this.handleSubmit}
   onChange={this.handleChange}
   onKeyDown={this.handleKeyDown}
/>

Изменилось состояние и код знает как для этого нового состояния перерисовать отображение.

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

Пока что результат, который получен при создании приложения todomvc на ReactJs смотрится довольно неплохо. Но чтобы понять картину в целом придется разобрать код приложения в реализации с другими JS библиотеками и фреймворками.

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *