Автор материала, перевод которого мы публикуем сегодня, говорит, что на работе ему приходится использовать Vue и он хорошо изучил этот фреймворк. Однако, ему всегда было любопытно узнать, как обстоят дела в других мирах, а именно, во вселенной React.
Он почитал документацию, посмотрел несколько учебных видео, и, хотя всё это показалось ему весьма полезным, ему хотелось по-настоящему понять, в чём заключается разница между React и Vue. Для него поиск различий между фреймворками заключался не в выяснении того, поддерживают ли они виртуальную объектную модель документа, или того, как именно они рендерят страницы. Ему хотелось, чтобы кто-нибудь объяснил бы ему особенности кода, показал бы, что в нём происходит. Он надеялся найти статью, которая посвящена раскрытию именно таких различий, прочтя которую тот, кто знал раньше лишь Vue или React (или совершенно новый в веб-разработке человек), мог бы лучше понять различия между этими фреймворками.
Однако такой статьи ему найти не удалось. Этот факт привёл его к пониманию того, что ему самому надо взять и такую статью написать, попутно разобравшись в сходствах и отличиях React и Vue. Собственно говоря, перед вами описание его эксперимента по сравнению этих двух фреймворков.
Общие положения
Vue или React?
Для проведения эксперимента я решил создать пару довольно стандартных To-Do-приложений, которые позволяют пользователю добавлять элементы в список дел и удалять их из него. Оба приложения разработаны с использованием стандартных CLI (create-react-app
для React и vue-cli
для Vue). CLI — это, если кто не знает, сокращение, которое расшифровывается как Command Line Interface, то есть — интерфейс командной строки.
Теперь предлагаю взглянуть на внешний вид приложений, о которых идёт здесь речь.
Приложения, созданные средствами Vue и React
Вот репозитории с кодом этих приложений: Vue ToDo, React ToDo.
И там и там используется абсолютно одинаковый CSS-код, единственная разница заключается в том, где именно размещены соответствующие файлы. Учитывая это, давайте взглянем на структуру проектов.
Структура проектов, использующих Vue и React
Как можно заметить, структура этих двух проектов практически идентична. Единственное серьёзное различие заключается в том, что у React-приложения имеется три CSS-файла, в то время как у Vue-приложения их нет совсем. Причина подобного заключается в том, что, при использовании create-react-app
, компоненты React оснащаются сопутствующими им CSS-файлами, а CLI Vue использует другой подход, когда стили объявляются внутри конкретного файла компонента.
В итоге и тот и другой подход позволяют достичь одной и той же цели, при этом, при желании, ничто не мешает организовать стили иначе, что в Vue, что в React. На самом деле, тут всё сводится к личным предпочтениям того, кто создаёт веб-проект. Например, тема структурирования CSS постоянно обсуждается в сообществах разработчиков. Сейчас мы просто будем следовать стандартным подходам к работе с CSS, заложенным в CLI рассматриваемых фреймворков.
Однако прежде чем мы пойдём дальше, давайте взглянем на то, как выглядит типичный компонент Vue и React.
Вот код компонента Vue (в нашем проекте он находится в файле ToDoItem.vue
).
<template>
<div class="ToDoItem">
<p class="ToDoItem-Text">{{todo.text}}</p>
<div class="ToDoItem-Delete"
@click="deleteItem(todo)">-
</div>
</div>
</template>
<script>
export default {
name: "to-do-item",
props: ['todo'],
methods: {
deleteItem(todo) {
this.$emit('delete', todo)
}
}
}
</script>
<style>
.ToDoItem {
display: flex;
justify-content: center;
align-items: center;
}
.ToDoItem-Text {
width: 90%;
background-color: white;
border: 1px solid lightgrey;
box-shadow: 1px 1px 1px lightgrey;
padding: 12px;
margin-right: 10px;
}
.ToDoItem-Delete {
width: 20px;
padding: 5px;
height: 20px;
cursor: pointer;
background: #ff7373;
border-radius: 10px;
box-shadow: 1px 1px 1px #c70202;
color: white;
font-size: 18px;
margin-right: 5px;
}
.ToDoItem-Delete:hover {
box-shadow: none;
margin-top: 1px;
margin-left: 1px;
}
</style>
Вот код React-компонента (файл ToDoItem.js
).
import React, {Component} from 'react';
import './ToDoItem.css';
class ToDoItem extends Component {
render() {
return (
<div className="ToDoItem">
<p className="ToDoItem-Text">{this.props.item}</p>
<div className="ToDoItem-Delete" onClick={this.props.deleteItem}>-</div>
</div>
);
}
}
export default ToDoItem;
Теперь пришло время погрузиться в детали.
Как выполняется изменение данных?
Изменение данных ещё называют «мутацией данных». Речь идёт об изменениях, вносимых в данные, которые хранит наше приложение. Так, если нам нужно изменить имя некоего человека с «Джон» на «Марк», то речь идёт о «мутации данных». Именно в подходе к изменению данных и находится ключевое различие между React и Vue. А именно, Vue создаёт объект data
, в котором находятся данные, и содержимое которого можно свободно менять. React же создаёт объект state
, в котором хранится состояние приложения, и при работе с которым для изменения данных требуются некоторые дополнительные усилия. Однако в React всё устроено именно так не без причины, ниже мы об этом поговорим, а для начала рассмотрим вышеупомянутые объекты.
Вот как выглядит объект data
, который используется в Vue.
data() {
return {
list: [
{
todo: 'clean the house'
},
{
todo: 'buy milk'
}
],
}
},
Вот как выглядит объект state
, применяемый в React:
constructor(props) {
super(props);
this.state = {
list: [
{
'todo': 'clean the house'
},
{
'todo': 'buy milk'
}
],
};
};
Как видите, в обоих случаях мы описываем одни и те же данные, они просто по-разному оформлены. В результате можно сказать, что передача изначальных данных компонентам в Vue и React выглядит очень и очень похоже. Но, как уже было сказано, подходы к изменению существующих данных в этих фреймворках различаются.
Предположим, у нас имеется элемент данных вроде name: ‘Sunil’
. Тут я присвоил свойству name
моё собственное имя.
В Vue обратиться к этим данным можно с помощью конструкции this.name
. А вот как их поменять: this.name = ‘John’
. Не знаю точно, как бы я себя чувствовал, если бы моё имя и правда изменилось, но в Vue это работает именно так.
В React же обратиться к тем же данным можно с помощью конструкции this.state.name
. А вот поменять их, написав нечто вроде this.state.name = ‘John’
, нельзя, так как в React действуют ограничения, предотвращающие подобные изменения данных. Поэтому в React приходится пользоваться чем-то наподобие this.setState({name: ‘John’})
.
Результатом подобной операции является то же самое, что получается после выполнения более простой операции в Vue. В React приходится писать больше кода, но в Vue существует что-то вроде особой версии функции setState
, которая вызывается при, как кажется, простом изменении данных. Поэтому, если подвести итоги, React требует использования команды setState
с передачей ей описания данных, которые надо изменить, а Vue работает, исходя из предположения, что разработчик хотел бы воспользоваться чем-то подобным, меняя данные внутри объекта data
.
Теперь зададимся вопросами о том, почему в React всё устроено именно так, и зачем вообще нужна функция setState
. Ответы на эти вопросы можно узнать у Реванта Кумара: «Это так потому что React стремится повторно выполнить, при изменении состояния, определённые хуки жизненного цикла, такие, как componentWillReceiveProps
, shouldComponentUpdate
, componentWillUpdate
, render
, componentDidUpdate
. Он узнаёт о том, что состояние изменилось, когда вы вызываете функцию setState
. Если бы вы меняли состояние напрямую, React пришлось бы выполнять гораздо больше работы для отслеживания изменений, для определения того, какие хуки жизненного цикла надо запускать, и так далее. В результате React, для того, чтобы облегчить себе жизнь, использует setState
».
Теперь, когда с изменениями данных мы разобрались, поговорим о том, как, в обеих версиях нашего приложения, добавлять новые элементы в список дел.
Добавление новых элементов в список дел
▍React
Вот как это делается в React.
createNewToDoItem = () => {
this.setState( ({ list, todo }) => ({
list: [
...list,
{
todo
}
],
todo: ''
})
);
};
Здесь у поля служащего для ввода данных (input
), имеется атрибут value
. Этот атрибут обновляется автоматически благодаря использованию пары взаимосвязанных функций, которые формируют то, что называется двусторонней привязкой данных (если вы о таком раньше не слышали — подождите немного, мы поговорим об этом в разделе, посвящённом добавлению элементов в Vue-приложении). Эту разновидность двусторонней связи мы создаём благодаря наличию дополнительного прослушивателя событий onChange
, прикреплённому к полю input
. Взглянем на код этого поля для того, чтобы вам было понятнее то, что здесь происходит.
<input type="text"
value={this.state.todo}
onChange={this.handleInput}/>
Функция handleInput
вызывается при изменении значения поля input
. Это приводит к обновлению элемента todo
, который находится внутри объекта state
, путём установки его в то значение, которое имеется в поле input
. Вот как выглядит функция handleInput
.
handleInput = e => {
this.setState({
todo: e.target.value
});
};
Теперь, когда пользователь нажимает на странице приложения кнопку +
для добавления в список новой записи, функция createNewToDoItem
вызывает метод this.setState
и передаёт ему функцию. Эта функция принимает два параметра. Первый — это весь массив list
из объекта state
, а второй — это элемент todo
, обновляемый функцией handleInput
. Затем функция возвращает новый объект, который содержит прежний массив list
, и добавляет новый элемент todo
в конец этого массива. Работа со списком организована с использованием оператора spread
(если вы с ним раньше не встречались — знайте, что это одна из новых возможностей ES6, и поищите подробности о нём).
И наконец, в todo
записывается пустая строка, что автоматически обновляет значение value
в поле input
.
▍Vue
Для добавления нового элемента в список дел в Vue используется следующая конструкция.
createNewToDoItem() {
this.list.push(
{
'todo': this.todo
}
);
this.todo = '';
}
В Vue у поля ввода имеется директива v-model
. Она позволяет организовать двустороннюю привязку данных. Взглянем на код этого поля и поговорим о том, что тут происходит.
<input type="text" v-model="todo"/>
Директива v-model
привязывает поле к ключу, который имеется в объекте данных, который называется toDoItem
. Когда страница загружается, в toDoItem
записана пустая строка, выглядит это как todo: ‘’
.
Если тут уже имеются какие-то данные, нечто вроде todo: ‘add some text here’
, то в поле ввода попадёт такой же текст, то есть — ‘add some text here’
. В любом случае, если вернуться к примеру с пустой строкой, текст, который мы введём в поле, попадёт, за счёт привязки данных, в свойство todo
. Это и есть двусторонняя привязка данных, то есть, ввод новых данных в поле приводит к записи этих данных в объект data
, а обновление данных в объекте приводит к появлению этих данных в поле.
Теперь вспомним функцию createNewToDoItem()
, о которой мы говорили выше. Как можно видеть, мы помещаем содержимое todo
в массив list
, а затем записываем в todo
пустую строку.
Удаление элементов из списка
▍React
В React эта операция выполняется так.
deleteItem = indexToDelete => {
this.setState(({ list }) => ({
list: list.filter((toDo, index) => index !== indexToDelete)
}));
};
В то время как функция deleteItem
находится в файле ToDo.js
, обратиться к ней без проблем можно и из ToDoItem.js
, сначала передав эту функцию как свойство в <ToDoItem/>
. Вот как это выглядит:
<ToDoItem deleteItem={this.deleteItem.bind(this, key)}/>
Тут мы сначала передаём функцию, что делает её доступной дочерним компонентам. Кроме того, мы осуществляем привязку this
и передачу параметра key
. Этот параметр используется функцией для того, чтобы отличить элемент ToDoItem
, который нужно удалить, от других элементов. Затем, внутри компонента ToDoItem
, мы делаем следующее.
<div className="ToDoItem-Delete" onClick={this.props.deleteItem}>-</div>
Всё, что нужно сделать для того, чтобы обратиться к функции, находящейся в родительском компоненте — это воспользоваться конструкцией this.props.deleteItem
.
▍Vue
Удаление элемента списка в Vue выполняется так.
onDeleteItem(todo){
this.list = this.list.filter(item => item !== todo);
}
В Vue требуется несколько иной подход к удалению элементов, чем тот, которым мы пользовались в React. А именно, тут надо выполнить три действия.
Во-первых, вот что надо сделать в элементе, для которого нужно будет вызвать функцию его удаления.
<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
, находящийся в объекте data
для того, чтобы удалить из него элемент, по которому щёлкнули.
Кроме того, стоит отметить, что в примере с использованием Vue можно было бы просто написать код, относящийся к функции emit
, внутри прослушивателя @click
. Выглядеть это может так.
<div class="ToDoItem-Delete" @click="$emit(‘delete’, todo)">-</div>
Это уменьшило бы число шагов, необходимых для удаления элемента списка, с трёх до двух. Как именно поступить — вопрос предпочтений разработчика.
Если подвести краткие итоги этого раздела, то можно сказать, что в React доступ к функциям, описанным в родительских компонентах, организуется через this.props
(учитывая то, что props
передаётся дочерним компонентам, что является стандартным приёмом, который можно встретить буквально повсюду). В Vue же дочерние компоненты должны вызывать события с помощью функции emit
, а эти события уже обрабатываются родительским компонентом.
Работа с прослушивателями событий
▍React
В React прослушиватели событий для чего-то простого, вроде события щелчка, весьма просты. Вот пример создания обработчика события click
для кнопки, создающей новый элемент списка дел.
<div className="ToDo-Add" onClick={this.createNewToDoItem}>+</div>
Тут всё устроено весьма просто, это очень похоже на обработку подобных событий с использованием чистого JavaScript.
Надо отметить, что здесь настройка прослушивателей событий нажатий на кнопки клавиатуры, например, на Enter
, занимает немного больше времени, чем в Vue. Тут нужно, чтобы событие onKeyPress
обрабатывалось бы следующим образом. Вот код поля ввода.
<input type="text" onKeyPress={this.handleKeyPress}/>
Функция handleKeyPress
вызывает функцию createNewToDoItem
, когда распознаёт нажатие на Enter
. Выглядит это так.
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
В React сначала дочернему компоненту передаётся функция, как свойство, там, где вызывается дочерний компонент. Затем выполняется планирование вызова этой функции, например, путём добавления её в качестве обработчика onClick
, или её вызов, путём обращения к this.props.whateverTheFunctionIsCalled
. Это приводит к вызову функции, находящейся в родительском компоненте. Именно этот процесс описан в разделе об удалении элементов из списка.
▍Vue
При использовании Vue, в дочернем компоненте достаточно написать функцию, которая передаёт данные родительской функции. В родительском компоненте пишется функция, которая прослушивает события передачи значения. При возникновении подобного события такая функция вызывается. Как и в случае с React, описание этого процесса можно найти в разделе об удалении элементов из списка.
Итоги
Мы поговорили о том, как добавлять, удалять и изменять данные в приложениях, основанных на Vue и React, о том, как передавать данные, в виде свойств, от родительских компонентов дочерним, и как отправлять данные от дочерних компонентов родительским. Полагаем, что после анализа примеров сходства и различия Vue и React видны, что называется, невооружённым глазом.
Существует, конечно, множество других небольших различий между React и Vue, но хочется надеяться, что то, что мы рассмотрели здесь, послужит хорошей основой для понимания того, как работают эти фреймворки. Фреймворки часто анализируют при выборе подходящей платформы для нового проекта. Надеемся, этот материал поможет такой выбор сделать.
Уважаемые читатели! Какие различия между React и Vue, на ваш взгляд, являются самыми главными, влияющими на выбор того или иного фреймворка, например, в качестве основы для некоего проекта?
Автор: ru_vds