Статьи

3 облегченных варианта React: Preact, VirtualDom и Deku

Эта статья была рецензирована Крейгом Билнером и Бруно Мота . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

Декларативные компоненты React и виртуальный рендеринг DOM взяли штурмом мир разработки внешнего интерфейса, но это не единственная библиотека, построенная на этих идеях. Сегодня мы рассмотрим, как создать приложение в трех других альтернативах, подобных React.

Мы предполагаем, что вы уже знакомы с React и терминологией, которая используется в его экосистеме. Если вам нужно встать с нуля или просто обновить, то ознакомьтесь с одной из наших предыдущих статей .

обзор

Давайте начнем с общего обзора библиотек, которые мы будем сравнивать.

Deku (2.0.0-rc15)

Деку на нпм

Deku стремится стать более функциональной альтернативой React. Он предотвращает локальное состояние компонентов, что позволяет записывать все компоненты в виде чистых функций, которые взаимодействуют с решением для управления внешним состоянием, таким как Redux .

Preact (4.1.1)

Preact на нпм

Preact — это попытка эмулировать основные функции React, используя как можно меньше кода. Предполагая, что вы будете использовать ES2015, Preact использует несколько ярлыков и обрезает оригинальный набор функций React, чтобы создать крошечную библиотеку, которая весит всего 3 КБ.

Виртуальный ДОМ (2.1.1)

виртуал-дом на нпм

В то время как React, Deku и Preact предоставляют вам абстракцию компонентов над виртуальным DOM, пакет virtual-dom предоставляет вам инструменты более низкого уровня, которые вам понадобятся для самостоятельного создания, сравнения и визуализации деревьев виртуальных DOM-узлов. ( Это не то же самое, что виртуальный DOM, на котором построены React и Preact! )

Низкоуровневая библиотека, такая как Virtual-DOM, может показаться странной альтернативой React, но если вы заинтересованы в написании высокопроизводительных мобильных веб-приложений, тогда стоит посмотреть JS Pocket-size . Фактически, этот разговор является причиной, по которой мы включили Virtual-DOM в качестве сравнения.

Мы будем использовать каждую из этих библиотек для создания компонента, структурирования нашего потока данных и, наконец, посмотрим на размер и производительность каждого приложения.

Компоненты

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

import React from 'react'; import marked from 'marked'; const Markdown = React.createClass({ propTypes: { text: React.PropTypes.string }, getDefaultProps() { return { text: '' }; }, render() { return ( <div dangerouslySetInnerHTML={{ __html: marked(this.props.text) }}> </div> ); } }); 

Мы используем проверку реквизита, чтобы компонент предупреждал нас, если он получает реквизит неправильного типа. Он также реализует метод getDefaultProps() который позволяет нам предоставлять значения по умолчанию для нашего компонента, если они не переданы. Наконец, мы реализуем метод рендеринга, который возвращает пользовательский интерфейс для этого компонента.

Чтобы React не мог избежать нашей Markdown, когда мы ее визуализируем, нам нужно передать ее свойству dangerouslySetInnerHTML .

Deku

Далее мы реализуем тот же компонент с Deku.

 /** @jsx element */ import { element } from 'deku'; import marked from 'marked'; const Markdown = { render({ props: { text='' } }) { return <div innerHTML={marked(text)}></div>; } }; 

Первая строка — это прагма компилятора, которая говорит нашему компилятору преобразовать JSX, например, <h1>Hello</h1> в element('h1', null, 'Hello') а не React.createElement('h1', null, 'Hello') , что позволяет нам использовать JSX с Deku вместо React. Эту опцию также можно настроить с помощью файла .babelrc .

По сравнению с React наш компонент Deku определенно проще. У компонентов Deku нет экземпляра, на который вы можете ссылаться, this означает, что все данные, которые могут понадобиться компоненту, будут переданы в метод как объект с именем model . Этот объект содержит props нашего компонента, и мы можем использовать синтаксис деструктурирования для извлечения text реквизита.

Deku не имеет проверки правильности, но мы можем, по крайней мере, смоделировать getDefaultProps() , предоставив значения по умолчанию в этих назначениях деструктурирования.

упреждения

Следующим будет Preact.

 /** @jsx h */ import { h, Component } from 'preact'; import marked from 'marked'; class Markdown extends Component { render() { const { text='' } = this.props; return ( <div dangerouslySetInnerHTML={{ __html: marked(text) }}> </div> ); } } 

Опять же, нам нужно сказать компилятору превратить JSX во что-то, что понимает Preact. Компоненты Preact очень похожи на компоненты класса React ES2015, и мы смогли скопировать большую часть нашего кода рендеринга ранее. Как и Deku, Preact не поддерживает проверку свойств или свойства по умолчанию, но мы снова можем имитировать свойства по умолчанию с назначениями деструктурирования.

Virtual-DOM

Наконец, мы посмотрим на Virtual-DOM.

 /** @jsx h */ import { h } from 'virtual-dom-util'; import marked from 'marked'; function Markdown({ text='' }) { return <div innerHTML={marked(text)}></div>; } 

Нам не предоставлены какие-либо инструменты для структурирования наших компонентов, поэтому вы не увидите здесь такие конструкции, props или state . Фактически, эти «компоненты» являются просто функциями, которые возвращают деревья виртуальных DOM-узлов.

Собственный способ создания виртуальных узлов DOM несовместим с JSX, поэтому мы используем пакет virtual-dom-util , чтобы предоставить нам альтернативу, совместимую с JSX. На самом деле нам не нужно импортировать пакет virtual-dom , пока мы не отобразим наш компонент.

Рендеринг компонента

Далее мы рассмотрим, как визуализировать компонент в DOM. Все эти библиотеки отображаются в целевом узле, поэтому мы создадим его в нашем HTML-файле.

 <div id="app"></div> 

реагировать

 import { render } from 'react-dom' render( <Markdown text='Hello __world__' />, document.getElementById('app') ); 

Для рендеринга компонента React нам нужно использовать пакет react-dom , который предоставляет функцию render которая понимает, как превратить дерево компонентов React в дерево узлов DOM.

Чтобы использовать его, мы передаем экземпляр компонента React и ссылку на узел DOM. ReactDOM обрабатывает все остальное.

Deku

 /** @jsx element */ import { createApp, element } from 'deku'; const render = createApp( document.getElementById('app') ); render( <Markdown text='Hello __world__' /> ); 

У Deku есть немного другой способ рендеринга компонента. Поскольку компоненты Deku не сохраняют состояние, они не будут автоматически перерисовываться. Вместо этого мы используем createApp() для построения функции рендеринга вокруг узла DOM, которую мы можем вызывать каждый раз, когда изменяется наше внешнее состояние.

Теперь мы можем передать экземпляры компонентов Deku, чтобы отобразить их в этом узле.

упреждения

 /** @jsx h */ import { h, render } from 'preact'; render( <Markdown text='Hello __world__' />, document.getElementById('app') ); 

Preact предоставляет нам аналогичный интерфейс для рендеринга компонентов в узлы DOM, однако он находится внутри основного пакета Preact, в отличие от ReactDOM. Как и большая часть Preact API, нет ничего нового для изучения, и концепции из React легко переносимы.

Virtual-DOM

 /** @jsx h */ import { create } from 'virtual-dom'; import { h } from 'virtual-dom-util'; const tree = <Markdown text='Hello __world__' />; const root = create(tree); document .getElementById('app') .appendChild(root); 

Virtual-DOM дает нам гораздо больше гибкости в том, как мы создаем и используем наш компонент. Сначала мы создаем экземпляр виртуального дерева, которое мы реализуем как узел DOM с функцией create . Наконец, мы можем добавить этого ребенка в DOM любым удобным для нас способом.

Поток данных

В трех рассматриваемых нами библиотеках есть два разных подхода к управлению состоянием нашего приложения.

внутри

Как и React, Preact также позволяет компонентам управлять своим собственным состоянием.

Компоненты управляют своим собственным состоянием

Каждый компонент отслеживает ссылку на неизменяемый объект состояния, который может обновляться через специальный метод компонента setState . Когда эта функция вызывается, компонент будет считать, что что-то было изменено, и попытается выполнить повторную визуализацию. Любые компоненты, которые получают реквизиты от компонента, состояние которого было обновлено, также будут перерисованы.

Preact также предоставляет нам механизм для переопределения поведения по умолчанию с мелкозернистым элементом управления в форме shouldComponentUpdate .

за пределами

Deku принимает осознанное решение перенести управление состоянием за пределы компонентов, а Virtual-DOM находится на слишком низком уровне, чтобы заниматься такими абстракциями, как состояние. Это означает, что если мы хотим создавать приложения с его помощью, то нам нужно сохранить состояние в другом месте.

Государственное управление вне компонентов

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

Чтобы обновить состояние, компоненты должны сообщить об изменениях в контейнере состояния. В Flux-подобных системах это общение часто происходит в форме действий .

Важно помнить, что хотя локальные состояния компонентов поддержки React и Preact также могут использоваться с решением для управления внешними состояниями.

Структура приложения

В этом разделе мы рассмотрим, как мы реализуем эти идеи о состоянии, потоке данных и повторном рендеринге как фактического кода. В ходе этого мы собираемся встроить наш компонент Markdown в редактор Markdown в реальном времени. Вы можете увидеть демонстрацию готовых компонентов в следующем разделе .

Deku

Приложение Deku обычно состоит из двух основных частей: дерева компонентов и магазина .

Подписаться на рассылку модели

Мы будем использовать Redux в качестве магазина, так как он отлично работает с Deku из коробки. Компоненты в дереве диспетчеризируют действия, которые наши редукторы Redux будут использовать для изменения состояния, и мы будем использовать механизм подписки для повторного рендеринга дерева компонентов при каждом изменении состояния.

Сначала мы настроим простой магазин Redux.

 import { createStore } from 'redux'; const initState = { text: '' }; const store = createStore((state=initState, action) => { switch(action.type) { case 'UPDATE_TEXT': return { text: action.payload }; default: return state; } }); 

Не вдаваясь в подробности, хранилище Redux состоит из функции-редуктора, которая принимает текущее состояние и действие в качестве аргументов. Функция должна возвращать новое состояние на основе данных в действии.

Теперь мы вернемся к коду рендеринга, чтобы сообщить Deku о нашем магазине Redux.

 const render = createApp( document.getElementById('app'), store.dispatch ); 

Поскольку Deku ожидает от вас использования внешнего решения для управления состоянием, его функция createApp принимает функцию диспетчеризации в качестве второго параметра. В свою очередь, Deku предоставит эту функцию диспетчеризации всем своим компонентам, чтобы они могли общаться с хранилищем Redux.

Мы также передадим текущее состояние нашего магазина в функцию рендеринга. Deku предоставит это значение каждому компоненту в качестве context , позволяя любому компоненту нашего дерева читать из хранилища.

 render( <MarkdownEditor />, store.getState() ); 

Мы можем использовать метод store.subscribe() для прослушивания изменений состояния, чтобы мы могли заново визуализировать дерево компонентов.

 store.subscribe(() => { render( <MarkdownEditor />, store.getState() ); }); 

Чтобы обновить состояние, компоненты должны передать действия своей функции диспетчеризации. Однако создание наших действий внутри наших компонентов может легко привести к раздутому коду компонента, поэтому вместо этого мы создадим функции-посредники, которые отправляют нам параметризованные действия. Эти функции часто называют «создателями действий».

 const actions = { updateText: dispatch => text => { dispatch({ type: 'UPDATE_TEXT', payload: text }); } }; 

Создатель действия принимает функцию диспетчеризации и параметр, а затем использует их для создания и отправки соответствующего объекта действия. Для удобства мы разрабатываем наши действия таким образом, чтобы они соответствовали стандартным действиям Flux .

Чтобы связать это в целом, наш компонент будет читать из состояния в context и отправлять действия с использованием нового создателя действий.

 const MarkdownEditor = { render({ context, dispatch }) { return ( <main> <section> <label>Markdown</label> <hr /> <Editor onEdit={actions.updateText(dispatch)} /> </section> <section> <label>Preview</label> <hr /> <Markdown text={context.text} /> </section> </main> ); } }; 

упреждения

После визуализации компонента Preact он самостоятельно управляет повторным рендерингом, прослушивая изменения его внутреннего состояния.

 import { Component } from 'preact'; import { bind } from 'decko'; class MarkdownEditor extends Component { constructor() { super() this.state = { text: '' }; } @bind onEdit(text) { this.setState({ text }); } render() { return ( <main> <section> <label>Markdown</label> <hr /> <Editor onEdit={this.onEdit} /> </section> <section> <label>Preview</label> <hr /> <Markdown text={this.state.text} /> </section> </main> ); } } 

Мы используем конструктор для инициализации состояния этого компонента. Затем мы создаем метод onEdit для обновления состояния на основе параметра. Вы также можете заметить, что мы использовали декоратор @bind здесь.

Этот декоратор происходит из библиотеки Decko (не Deku!), И мы используем ее, чтобы убедиться, что метод onEdit имеет правильное значение, даже если он вызывается извне компонента.

Наконец, мы this.state.text нашему компоненту <Markdown /> в качестве реквизита. Каждый раз, когда onEdit обратный вызов onEdit , мы будем обновлять состояние, и компонент будет повторно отображаться.

Virtual-DOM

В отличие от React, Deku и Preact, Virtual-DOM не делает никаких предположений о том, как вы управляете состоянием или где виртуальные узлы получают свои данные. Это означает, что нам придется проделать дополнительную работу, чтобы настроить это.

К счастью, Redux недостаточно открыт, чтобы мы могли использовать его и здесь. Фактически, мы можем позаимствовать код для создания магазина из примера Deku.

 import { createStore } from 'redux'; const store = createStore((state = initState, action) => { switch (action.type) { case 'UPDATE_TEXT': return { text: action.payload }; default: return state; } }); 

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

 const actions = { updateText(text) { store.dispatch({ type: 'UPDATE_TEXT', payload: text }); } } 

Это может показаться проще, чем наши другие создатели действий, но это делает их намного сложнее изолировать и тестировать, так как все они имеют неприемлемую зависимость от хранилища Redux.

Мы передадим начальное состояние нашему компоненту для первого рендера.

 let tree = <MarkdownEditor state={store.getState()} />; let root = create(tree); document .getElementById('app') .appendChild(root); 

Затем мы будем использовать механизм подписки для прослушивания изменений состояния.

 import { diff, patch } from 'virtual-dom'; store.subscribe(function() { let newTree = <MarkdownEditor state={store.getState()} />; let patches = diff(tree, newTree); root = patch(root, patches); tree = newTree; }); 

Вместо простого рендеринга нового дерева, мы выполняем diff вручную, затем мы использовали возвращенный набор патчей, чтобы применить минимальное количество изменений, необходимое для того, чтобы визуализированные DOM-узлы отражали виртуальные DOM-узлы в нашем newTree .

Наконец мы перезаписываем наше старое дерево, готовое для следующего рендера.

демос

Мы соединили эти компоненты и создали простой разделительный экран, редактор Markdown в реальном времени с каждой платформой. Вы можете увидеть код и поиграть с готовыми редакторами на Codepen.

Размер

Когда мы разрабатываем легкие приложения, предназначенные для использования на настольных и мобильных устройствах, объем данных, которые мы должны передавать с сервера, является важным фактором при выборе уровня представления.

В каждом случае мы создаем минимизированный пакет, содержащий как код нашего приложения, так и наши зависимости, для сравнения.

4. Реагировать

  • Строки кода : 61
  • Зависимости : react , react-dom , marked
  • Размер пакета: 154.1kb
  • Gzipped : 45.3kb

Согласно рекомендации команды React, мы используем предварительно собранные производственные версии React, а не минимизируем их сами. Автономная минимизированная версия Marked входит в ~ 17kb. Вместе минимизированные версии React и ReactDOM синхронизируются в ~ 136 КБ.

3. Деку

  • Строки кода : 80
  • Зависимости : deku , deku , marked
  • Размер пакета: 51.2kb
  • Gzipped : 15,3 КБ

Наш пакет Deku уже на 100 КБ легче, чем React, и мы также включили полноценного государственного менеджера в форме Redux. Вместе Redux и Marked весят примерно ~ 30kb. Оставив код нашего приложения и нашу зависимость от Deku в ~ 21kb.

2. Виртуал-ДОМ

  • Строки кода : 85
  • Зависимости : virtual-dom , virtual-dom-util , redux , marked
  • Размер пакета: 50.5kb
  • Gzipped : 15,2 КБ

Несмотря на свой минималистский, низкоуровневый характер, наш пакет Virtual-DOM весит ~ 50 КБ (примерно такой же размер, как у Deku). Опять же, Redux и Marked отвечают за ~ 30 КБ этого размера. Вместе пакеты virtual-dom и код приложения отвечают за ~ 20 КБ.

1. Преакт

  • Строки кода : 62
  • Зависимости : preact , decko , marked
  • Размер пакета: 30.6kb
  • Gzipped : 10,5 КБ

Оставаясь верным своей цели, наш комплект Preact имеет внушительные 30,6 КБ. Вместе Decko и Marked ответственны за ~ 19 КБ, оставив Preact и код нашего приложения всего на 11 КБ.

Производительность

Для мобильного интернета мы должны одинаково понимать, что не все процессоры мобильных устройств созданы равными. Мы посмотрим, как быстро наше приложение выводит свой первый кадр на экран.

4. Реагировать

Хронология реакции

Браузер начинает оценивать JavaScript около 30 мс. Затем после пересчета стиля, перекомпоновки и обновления дерева слоев мы получаем событие рисования со скоростью 173,6 мс, затем слои компонуются и, наконец, первый кадр попадает в браузер со скоростью 183 мс . Итак, мы смотрим примерно на 150 мс.

3. Деку

Deku Timeline

Браузер начинает оценивать JavaScript около 55 мс. Затем мы видим тот же пересчет стиля, перекомпоновку и обновление дерева слоев, прежде чем мы видим событие рисования на 111 мс, слои комбинируются, а первый кадр приземляется на 118 мс . Deku более чем вдвое сокращает время обработки React, сокращая его примерно до 70 мс.

2. Преакт

Preact Timeline

Мы видим, что браузер начинает оценивать сценарии примерно через 50 мс, а событие рисования появляется в 86,2 мс, а первый кадр попадает в 102 мс с временем оборота 50 мс.

1. Виртуал-ДОМ

Виртуальная DOM Timeline

Браузер начинает оценивать 32 мс, а событие рисования — 80,3 мс (интересно, что для компоновки слоя броузеру требуется почти в 10 раз больше, чем другим фреймворкам), а затем фрейм — 89,9 мс . Оборот составляет почти 60 мс. Таким образом, хотя Virtual-DOM имеет самый быстрый интервал между кадрами, процесс рендеринга кажется более медленным, чем Preact.

Конечно, здесь мы рассматриваем производительность в микромасштабе и общий вывод, что все эти библиотеки очень быстрые (для этого приложения). Все они имеют свой первый кадр на экране в течение 200 мс.

Эти результаты теста также были записаны на Chromebook, а не на мобильном устройстве, поэтому они предназначены только для сравнения относительной производительности между этими библиотеками.

Вы можете найти код для этих тестов здесь, на GitHub .

Вывод

React изменил всю картину того, как мы думаем о разработке приложений. Без React у нас не было бы ни одной из этих фантастических альтернатив, и она остается неоспоримой, когда речь идет об экосистеме, инструментах и ​​сообществе.

Уже есть сотни, если не тысячи пакетов React, доступных по npm, при этом организация сообщества ReactJS создала коллекцию из более чем 20 высококачественных проектов с открытым исходным кодом, чтобы обеспечить их долгосрочную поддержку и обслуживание.

React обслуживает большинство стилей программирования, которые мы видели в других библиотеках. Если вы хотите переместить свое состояние в хранилище, такое как Redux, и работать с компонентами без сохранения состояния, React позволит вам сделать это. Кроме того, React также поддерживает функциональные компоненты без сохранения состояния .

Сама библиотека прошла боевые испытания: огромное количество прогрессивных технологических компаний (включая Facebook), использующих React в производстве, и пакет npm получают сотни тысяч загрузок в неделю.

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

Deku

Если Redux является важной частью вашего рабочего процесса, вы можете поэкспериментировать с Deku. Он весит меньше и (в нашем случае) работает немного быстрее, чем React, с продуманным подходом, который позволяет вырезать большую часть исходного набора функций.

Deku отлично подходит для программистов, которые хотят, чтобы в React был более функциональный стиль.

Virtual-DOM

Virtual-DOM идеально подходит для создания собственных абстракций. Инструментов, которые он предоставляет «из коробки», недостаточно для структурирования полных приложений, и обидно, что он не поддерживает JSX по умолчанию, но эти качества делают его идеальным в качестве цели для абстракций более высокого уровня, которые не подходят для Реагируй сам.

Virtual-DOM по-прежнему будет отличной целью для разработчиков языков, которые хотят работать с декларативными, основанными на компонентах моделями, не беспокоясь о том, что они не справятся с манипуляциями с DOM. Например, в настоящее время он используется с большим эффектом как часть вяза .

упреждения

Preact является сюрпризом здесь. Мало того, что он входит в самое маленькое приложение, но он также имеет очень низкий оборот для вывода кадров на экран.

Он легкий, имеет небольшую, но растущую экосистему, и растет число пакетов React, которые можно использовать оптом с Preact. Независимо от того, создаете ли вы высокопроизводительные приложения или страницы, которые необходимо доставлять по низкоскоростным сетевым соединениям, Preact — отличный проект, за которым нужно следить.