Это небольшая ответная статья на публикацию «Сравнение JS-фреймворков: React, Vue и Hyperapp». Вообще я не большой фанат подобных сравнений. Однако раз уж речь зашла о таком маргинальном фреймворке, как Hyperapp, в сравнении с мастодонтами, типа React и Vue, я подумал, почему бы не рассмотреть все те же примеры на Svelte. Так сказать, для полноты картины. Тем более, это займет буквально 5 минут. Поехали!
Если вдруг вы не знакомы со Svelte и концепцией исчезающих фреймворков, можете ознакомиться со статьями «Магически исчезающий JS фреймворк» и «Исчезающие фреймворки».
Для удобства читателей, я скопировал примеры из оригинальной статьи под спойлеры, чтобы было удобнее сравнивать. Что ж приступим.
Пример №1: приложение-счётчик
import React from "react";
import ReactDOM from "react-dom";
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0};
}
down(value) {
this.setState(state => ({ count: state.count - value }));
}
up(value) {
this.setState(state => ({ count: state.count + value }));
}
render() {
return (
<div>
<h1>{this.state.count}</h1>
<button onClick = {() => this.down(1)}>-</button>
<button onClick = {() => this.up(1)}>+</button>
</div>
);
}
}
ReactDOM.render(<Counter />, document.querySelector("#app"));
import Vue from "vue";
new Vue({
data: { count: 0 },
methods: {
down: function(value) {
this.count -= value;
},
up: function(value) {
this.count += value;
}
},
render: function(h) {
return(
<div>
<h1>{this.count}</h1>
<button onClick={() => this.down(1)}>-</button>
<button onClick={() => this.up(1)}>+</button>
</div>
);
},
el: "#app"
});
import { h, app } from "hyperapp";
const state = {
count: 0
};
const actions = {
down: value => state => ({ count: state.count - value}),
up: value => state => ({ count: state.count + value})
};
const view = (state, actions) => (
<div>
<h1>{state.count}</h1>
<button onclick={() => actions.down(1)}>-</button>
<button onclick={() => actions.up(1)}>+</button>
</div>
);
app(state, actions, view, document.querySelector("#app"));
▍Svelte
<div>
<h1>{count}</h1>
<button on:click="set({count: count - 1})">-</button>
<button on:click="set({count: count + 1})">+</button>
</div>
▍Анализ
Компонент Svelte — это html-файл, который имеет небезызвестный формат Single File Component (SFC), в том или ином виде, уже применяющийся в Vue, Ractive, Riot и некоторых других фреймворках. Кроме самого html-шаблона, компонент может иметь поведение и логику, описанную на javascript, а также scoped-стили компонента.
Ни одна часть компонента не является обязательной, поэтому компонент счетчика может состоять лишь из html-шаблона самого счетчика. Для изменения значения переменной count, обработчик кликов использует встроенный метод компонента set(), описанный в документации.
Пример №2: работа с асинхронным кодом
import React from "react";
import ReactDOM from "react-dom";
class PostViewer extends React.Component {
constructor(props) {
super(props);
this.state = { posts: [] };
}
getData() {
fetch(`https://jsonplaceholder.typicode.com/posts`)
.then(response => response.json())
.then(json => {
this.setState(state => ({ posts: json}));
});
}
render() {
return (
<div>
<button onClick={() => this.getData()}>Get posts</button>
{this.state.posts.map(post => (
<div key={post.id}>
<h2><font color="#000">{post.title}</font></h2>
<p>{post.body}</p>
</div>
))}
</div>
);
}
}
ReactDOM.render(<PostViewer />, document.querySelector("#app"));
import Vue from "vue";
new Vue({
data: { posts: [] },
methods: {
getData: function(value) {
fetch(`https://jsonplaceholder.typicode.com/posts`)
.then(response => response.json())
.then(json => {
this.posts = json;
});
}
},
render: function(h) {
return (
<div>
<button onClick={() => this.getData()}>Get posts</button>
{this.posts.map(post => (
<div key={post.id}>
<h2><font color="#000">{post.title}</font></h2>
<p>{post.body}</p>
</div>
))}
</div>
);
},
el: "#app"
});
import { h, app } from "hyperapp";
const state = {
posts: []
};
const actions = {
getData: () => (state, actions) => {
fetch(`https://jsonplaceholder.typicode.com/posts`)
.then(response => response.json())
.then(json => {
actions.getDataComplete(json);
});
},
getDataComplete: data => state => ({ posts: data })
};
const view = (state, actions) => (
<div>
<button onclick={() => actions.getData()}>Get posts</button>
{state.posts.map(post => (
<div key={post.id}>
<h2><font color="#000">{post.title}</font></h2>
<p>{post.body}</p>
</div>
))}
</div>
);
app(state, actions, view, document.querySelector("#app"));
▍Svelte
<div>
<button on:click="getData()">Get posts</button>
{#each posts as {title, body}}
<div>
<h2><font color="#000">{title}</font></h2>
<p>{body}</p>
</div>
{/each}
</div>
<script>
export default {
methods: {
getData() {
fetch('https://jsonplaceholder.typicode.com/posts')
.then(res => res.json())
.then(posts => this.set({ posts }));
}
}
};
</script>
▍Анализ
В отличие от JSX, который, как под копирку, применяется во всех 3-х фреймворках из оригинального сравнения, и по сути расширяет javascript код html-подобным синтаксисом, Svelte использует более привычные возможности — внедрение js и css кода в html с помощью тегов <script> и <style>.
Скрипт компонента экспортирует простой JS объект, разделенный на секции. В секции methods описываются методы компонента, которые могут быть использованы через инстанс компонента и в обработчиках событий. Так, при нажатии на кнопку, вызывается метод getData(), внутри которого запрашиваются данные и по завершению операции, данные просто устанавливаются в стейт компонента и сразу же отрисовываются в шаблоне.
Обратите внимание, на использование деструктуризации объекта публикации (post) на каждом шаге итерации списка публикаций:
{#each posts as {title, body}}
Этот трюк позволяет избежать избыточности в шаблонах типа {post.title} и визуально упростить шаблоны, используя более короткую запись {title}.
Пример №3: компонент элемента списка для To-Do-приложения
function TodoItem(props) {
return (
<li class={props.done ? "done" : ""} onclick={() => props.toggle(props.id)}>
{props.value}
</li>
);
}
class TodoItem extends React.Component {
render () {
return (
<li class={this.props.done ? "done" : ""} onclick={() => this.props.toggle(this.props.id)}>
{this.props.value}
</li>
);
}
}
var TodoItem = Vue.component("todoitem", {
props: ["id", "value", "done", "toggle"],
template:
'<li v-bind:class="{done : done}" v-on:click="toggle(id)">{{value}}</li>'
});
const TodoItem = ({ id, value, done, toggle }) = (
<li class={done ? "done" : ""} onclick={() => toggle(id)}>
{value}
</li>
);
▍Svelte
<li class="{done ? 'done' : ''}" on:click="set({done: !done})">{value}</li>
▍Анализ
Тут все довольно банально. Выставляем css-класс в зависимости от значения done и меняем это значение на противоположное при клике на элемент списка.
Сравнение методов жизненного цикла компонентов
Disclaimer: С этого момента я решил опустить сравнение с Hyperapp, потому что иначе таблицы будут просто не читаемые.
Событие | React | Vue | Svelte |
Инициализация | constructor | beforeCreate, created |
onstate |
Монтирование | componentDidMount | beforeMount, mounted | oncreate, onupdate |
Обновление | componentDidUpdate | beforeUpdate, updated | onstate, onupdate |
Размонтирование | componentWillUnmount | — | ondestroy |
Уничтожение | — | beforeDestroy, destroyed | — |
▍Анализ
Svelte крайне минималистичен, в том числе в плане хуков жизненного цикла. Существует всего 4 хука:
- onstate — вызывается до создания компонента и каждое изменение стейта до обновления DOM.
- oncreate — вызывается момент создания компонента.
- onupdate — вызывается сразу после монтирования в DOM и каждое изменение стейта после обновления DOM.
- ondestroy — вызывается при уничтожении компонента и удаления из DOM.
Сравнение производительности фреймворков
Честно говоря, не знаю что тут комментировать. Сами по себе бенчмарки и способ их исполнения всегда вызывает много споров, поэтому не думаю что имеет смысл излишне заострять внимание.
Однако других данных у нас все равно нет.
▍Работа с таблицами
▍Загрузка, запуск, размеры кода
▍Работа с памятью
▍Анализ
Cудя по цифрам, Svelte довольно быстрый, «жрет» мало памяти (в том числе потому что не использует VirtualDOM), быстро запускается и имеет небольшие размеры.
Вообще, сказать что результаты бенчмарков этих 3-х фреймворков отличались координально, я не могу. Однако, только Svelte имеет исключительно «зеленый» столбик, т.е. он достаточно хорош сразу по всем показателям, а значит отлично сбалансирован и не имеет явных перекосов ни в скорости, ни в потреблении памяти и иных метриках. В общем, с ним смело можно начинать любой проект, от обычного веба, до мобильных устройств, Смарт ТВ и более экзотичных систем.
Итоги
Svelte — отличный инструмент для создания компонентов практически любых веб-приложений. Он такой же мощный как React и хотя пока имеет значительно меньшее сообщество, при этом требует меньше усилий для освоения. Он гибок, как Vue, при этом значительно более минималистичен и выверен, так как не пытается ухватиться сразу за все идеи современной веб-разработки.
Кроме того, он единственный использует концепцию исчезающих фреймворков. Иными словами он не имеет никакого специфического рантайма, кроме самого браузера.
Приложения на нем получаются быстрыми, не требовательными к ресурсам и маленькими в размере. Кроме того, Svelte может «бесшовно» использоваться в приложениях, написанных на других фреймворках, о чем я планирую написать в следующий раз.
Если вы впервые познакомились с этим замечательным фреймворком, возможно вам будет интересно прочитать и другие статьи о нем:
→ Магически исчезающий JS фреймворк
→ 1Kb autocomplete
→ SvelteJS: Релиз второй версии
Если у вас остались вопросы по Svelte, вступайте в русскоязычный телеграм канал SvelteJS. Там вы всегда сможете задать вопрос и получить совет, узнать последние новости, а также просто приятно пообщаться. Будем вам рады!
Автор: PaulMaly