Сейчас на работе я использую Vue, поэтому твердо знаю, как он работает. Однако мне всегда было любопытно проверить, какой же была трава на другой стороне реки - и травой в моем случае является React.
Я прочитал массу различных материалов про React, смотрел обучающие видео. Все это было круто, но мне было очень интересно, в чем же React отличается от Vue. Под отличиями я не имею в виду наличие в Vue или React Virtual DOM или то, как в них происходит рендеринг страниц. Мне хотелось, чтобы кто-нибудь рассказал мне, что происходит в коде! Я хотел найти статью, в которой объяснялись бы эти отличия, чтобы любой новичок в React или Vue (или в целом в разработке веб-приложений) смог бы лучше разобраться в этой теме.
Но я не смог найти ничего путного, что бы все это охватывало. Благодаря этому я понял, что придется сделать все самому, чтобы увидеть все сходства и отличия. С этой целью я решил описать весь процесс, чтобы такая статья наконец появилась.
Я решил создать стандартное приложение списка дел, которое позволяет пользователю добавлять и удалять пункты списка. Оба приложения созданы при использовании CLI: create-react-app в React, vue-cli в Vue. Кстати, CLI - это интерфейс командной строки.
Вступление вышло дольше, чем я ожидал. И так, начнем с того, что взглянем на внешний вид наших двух приложений:
CSS код для обоих приложений совершенно одинаков, разница только в том, где он расположен. С учетом этого взглянем на файловую структуру обоих приложений:
Можно заметить, что структуры практически идентичны. Единственная разница в том, что написанное на React приложение содержит 3 CSS файла, а Vue приложение - ни одного. Причина состоит в том, что в create-react-app к компоненту React идет дополнительный файл, в котором хранятся стили. У Vue CLI охватывающий весь подход: стили описываются в самом файле компонента.
В конечном счете достигается один и тот же результат. Можно выбрать другой путь и структурировать CSS другим образом в React или Vue. Здесь все дело в личных предпочтениях, и можно найти много дискуссий среди разработчиков о том, как нужно структурировать CSS. Ну а мы пока будем следовать той структуре, которая изложена в CLI.
Прежде чем продолжить, посмотрим, как выглядит типичный компонент Vue (слева) и React (справа):
С этим мы закончили, и теперь углубимся в детали!
Как мы изменяем данные?
Сначала определимся, что вообще понимается под изменением данных. Звучит довольно технически, не так ли? По сути изменять - значит менять хранящиеся данные. Если бы мы хотели изменить атрибут имени человека с Джона на Марка, мы бы произвели процесс изменение данных. Вот здесь и лежит основное различие между React и Vue. Если Vue создает объект данных, где они могут свободно обновляться, то React создает объект состояний, где для введения обновлений понадобится еще немного работы. На это у React есть причина, и скоро мы до этого дойдем. Но сначала посмотрите на объект данных из Vue (первая картинка) и объект состояния из React (вторая):
Вы можете заметить, что данные переданы одинаковые, но работа с ними производится разная. Так что процесс передачи данных в компоненты очень похож между собой. Однако, как уже упоминалось, процесс изменения данных в этих фреймворках отличаются.
Допустим, у нас есть элемент данных name: 'Sunil'
.
В Vue мы будем обращаться к этому элементу как к this.name
. Обновить данные можно так: this.name = 'John'
. Тогда мое имя станет Джон. Не уверен, что за чувства от этого испытываю, но всякое случается!
Если рассматривать React приложение, к этому же элементу мы бы обращались как к this.state.name
. Главное отличие в том, что мы не можем просто так написать this.state.name = 'John'
, потому что в React есть ограничения, которые предотвращают такие простые изменения. В React придется писать что-то типа this.setState({name: 'John'})
.
По сути эта команда сделает то же самое, чего мы уже добились в Vue, но приходится писать немного больше. Это сделано для того, чтобы разработчик случайно не переписал this.state
, потому что есть большая разница между this.state
и this.setState
. Для различия в процессе изменения в React и Vue есть причина, которую объяснил Ревант Кумар:
Все дело в том, что React хочет переписать хуки жизненного цикла, componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, рендер, componentDidUpdate каждый раз, когда меняется state. Он узнает об изменении state, когда вы вызываете функцию setState. Если вы провели изменения state напрямую, React-ту придется проделать гораздо больше работы, чтобы отследить их, выбирать, какие хуки жизненного цикла запускать и так далее. Так что, чтобы все упростить, React приложение использует setState.
Раз уж разобрались с процессом изменения, можно углубиться в детали. Рассмотрим добавление пунктов в наши приложения со списками дел.
Как добавлять новые пункты в список дел?
React:
createNewToDoItem = () => {
this.setState(({ list, todo }) => ({
list: [
...list,
{
todo
}
],
todo: ''
})
);
};
Как React это сделал?
В React в поле ввода есть атрибут value. Он автоматически обновляется с использованием пары функций, которые вместе образуют двунаправленную привязку данных. Если вы никогда не слышали о таком термине, более подробное описание будет в разделе «Как Vue это сделал?». Эта форма двусторонней привязки создается путем присоединения дополнительного слушателя событий onChange к полю ввода. Быстро взглянем на поле ввода, чтобы вы понимали, что происходит:
<input type="text"
value={this.state.todo}
onChange={this.handleInput}/>
Функция handleInput начинает работать каждый раз, когда меняется value. Она обновляет todo, который находится в объекте состояний, и меняет его значение на то, что находится в поле ввода. Эта функция выглядит следующим образом:
handleInput = e => {
this.setState({
todo: e.target.value
});
};
Каждый раз, когда пользователь нажимает плюс на странице, чтобы добавить новый пункт, функция createNewToDoItem, по сути, запускает this.setState и передает ему функцию. Она принимает два параметра: первый - весь массив list из объекта состояний; второй - todo, который обновляется handleInput. Затем эта функция возвращает новый объект, который содержит list, и добавляет todo в его конец. Весь list добавляется при помощи оператора Spread (если вы о таком никогда не слышали - это синтаксис ES6).
Наконец, мы меняем значение todo на пустую строку, и это автоматически обновляет value в поле ввода.
Vue:
createNewToDoItem() {
this.list.push(
{
'todo': this.todo
}
);
this.todo = '';
}
Как Vue это сделал?
В Vue в поле ввода содержится v-model. Это позволяет нам сделать двустороннюю привязку. Быстренько взглянем на поле ввода, затем я объясню, что происходит:
<input type="text" v-model="todo"/>
V-Model привязывает содержимое поля к ключу из объекта данных, который называется toDoItem. Когда страница загружается, значение toDoItem меняется на пустую строку вот так: todo: ''
. Если бы там были данные (например, todo: 'add some text here'
), поле ввода бы загрузилось так, что этот текст бы находился в нем. Вернемся к тому, что мы установили для него значение пустой строки. Все, что мы пишем в поле ввода, становится значением todo. Это и есть двустороннее привязывание, то есть поле ввода может обновлять объект данных и наоборот.
Вернемся к блоку кода createNewToDoItem(). Можно заметить, что мы отправляем содержимое todo в массив list, а затем придаем todo значение пустой строки.
Как удалять пункты из списка дел?
React:
deleteItem = indexToDelete => {
this.setState(({ list }) => ({
list: list.filter((toDo, index) => index !== indexToDelete)
}));
};
Как React это сделал?
Так как функция deleteItem находится в ToDo.js, я легко смог сделать на него ссылку в ToDoItem.js, сначала передав функцию deleteItem() в <ToDoItem/>
следующим образом:
<ToDoItem deleteItem={this.deleteItem.bind(this, key)}/>
Во-первых, этот код передает функцию так, чтобы она была доступна дочернему элементу. Также можно заметить, что еще мы привязываем this и передаем параметр key. Key - то, что функция будет использовать, чтобы выбирать, какое ToDoItem удалять, когда пользователь кликает. Затем внутри компонента ToDoItem делаем следующее:
<div className="ToDoItem-Delete" onClick={this.props.deleteItem}>-</div>
Все, что мне потребовалось сделать, чтобы сослаться на функцию, которая находится в материнском компоненте, - это добавить ссылку на this.props.deleteItem.
Vue:
onDeleteItem(todo){
this.list = this.list.filter(item => item !== todo);
}
Как Vue это сделал?
В Vue нужен немного другой подход. По сути нужно сделать три вещи.
Во-первых, в элементе, который должен вызвать функцию:
<div class="ToDoItem-Delete" @click="deleteItem(todo)">-</div>
Затем нужно создать функцию emit в качестве метода внутри дочернего элемента (в нашем случае ToDoItem.vue), которая выглядит так:
deleteItem(todo) {
this.$emit('delete', todo)
}
Также вы могли заметить, что мы ссылаемся на функцию, когда добавляем ToDoItem.vue в ToDo.vue:
<ToDoItem v-for="todo in list"
:todo="todo"
@delete="onDeleteItem" // <-- this :)
:key="todo.id" />
Это называется пользовательским прослушивателем событий. Он ищет любое изменение состояния, когда emit вызывается строкой «delete». Если такое событие наступает, он вызывает функцию onDeleteItem. Она находится в ToDo.vue, а не в ToDoItem.vue. Как было написано ранее, эта функция фильтрует массив todo в объекте данных, чтобы удалить пункт, на который кликнул пользователь.
Стоит также заметить, что в примере для Vue я мог бы просто прописать в прослушивателе @click часть с $emit, например:
<div class="ToDoItem-Delete" @click="$emit('delete', todo)">-</div>
Тогда шагов бы было три, а не два. Это зависит от личных предпочтений.
Короче говоря, дочерние компоненты в React имеют доступ к материнским функциям через this.props (учитывая, что вы передаете свойства, что довольно стандартно; вы еще много раз столкнетесь с таким в других примерах программ на React). В Vue придется выделять события из дочернего элемента, причем обычно они собираются в материнском компоненте.
Как передавать прослушиватели событий?
React:
Прослушиватели событий (например, кликов) довольно просты в исполнении. Вот пример того, как создать событие, когда пользователь кликает на кнопку, которая создает новый пункт в списке дел:
<div className="ToDo-Add" onClick={this.createNewToDoItem}>+</div>
Здесь все очень просто. Аналогично бы мы поступили со встроенным onClick в Vanilla JS. Как упоминалось ранее в разделе Vue, понадобилось немного больше времени, чтобы прописать слушатель событий, который бы реагировал каждый раз, когда нажимается кнопка ввода. Для этого нужно событие onKeyPress, которым займется тэг input:
<input type="text" onKeyPress={this.handleKeyPress}/>
Эта функция будет вызывать createNewToDoItem каждый раз, когда будет нажиматься клавиша ввода.
handleKeyPress = (e) => {
if (e.key === 'Enter') {
this.createNewToDoItem();
}
};
Vue:
В Vue все чрезвычайно просто. Мы просто используем символ @, затем прописываем тип слушателя событий. Например, чтобы добавить слушатель событий, который будет реагировать на клики, нужно написать следующее:
<div class="ToDo-Add" @click="createNewToDoItem()">+</div>
Важно: @click - короткий вариант записи v-on:click. Самое крутое в прослушивателях событий в Vue - то, что к ним можно добавить кучу разных вещей, например, .once (тогда слушатель событий должен сработать не более одного раза). Есть также несколько сокращений для написания слушателей событий, которые следят за нажатиями на клавиши. Я узнал, что на создание прослушивателя, который создает новые пункты списка, когда нажимается кнопка ввода, в React уходит больше времени, чем в Vue. Здесь нужно просто написать:
<input type="text" v-on:keyup.enter="createNewToDoItem"/>
Как передать данные дочернему компоненту?
React:
В React данные передаются дочернему компоненту в момент его создания. Пример:
<ToDoItem key={key} item={todo} />
Здесь компоненту ToDoItem было передано два элемента. Теперь можно ссылаться на них в дочернем элементе при помощи this.props. Чтобы получить доступ к item.todo, нужно просто вызвать this.props.item.
Vue:
В Vue данные также передаются дочернему компоненту в момент его создания. Пример:
<ToDoItem v-for="todo in list"
:todo="todo"
:key="todo.id"
@delete="onDeleteItem" />
Затем они передаются в массив props в дочернем компоненте, например: props: ['todo']
. На них можно ссылаться по их названиям «todo».
Как вернуть данные в материнский компонент?
React:
Сначала мы передаем функцию в дочерний компонент, ссылаясь на нее в месте, где вызывается этот компонент. Затем вызываем функцию там же любым способом: onClick или this.props.whateverTheFunctionIsCalled. Тогда сработает функция, которая находится в материнском компоненте. Пример такого процесса можно найти в разделе «Как удалять пункты из списка дел?».
Vue:
В дочернем компоненте мы прописываем функцию, которая возвращает данные в материнскую функцию. В материнском компоненте нужно создать функцию, которая отслеживает момент извлечения данных (он вызовет функцию). Пример такого процесса можно найти в разделе «Как удалять пункты из списка дел?».
Вот и все!
Мы узнали, как добавлять, удалять и менять данные, передавать их от материнского компонента в дочерний и обратно. Конечно, существует еще много небольших различий между React и Vue и особенностей этих фреймворков, но, надеюсь, эта статья смогла заложить фундамент понимания их работы.