Привет
Я давно пишу код, а React использую более пяти лет.
За это время у меня возникло несколько идей о том, как можно было бы улучшить React.
К реализации этих идей я приступил около трех лет назад. Сначала проверил концепцию, потом решил оформить всё в виде библиотеки.
А о том, что из этого вышло, я бы хотел рассказать в этой статье.
Чем хорош React
Во-первых, это подход React в объединении Javascript и HTML в одном коде. У остальных это получилось не так хорошо.
Например, для некоторых фреймворков были изобретены свои микроязыки программирования шаблонов, которые дублируют конструкции языка JavaScript, как: if
, else
, each
...
По моему мнению, это лишняя когнитивная нагрузка. Благо, некоторые с тех пор обзавелись поддержкой JSX.
Другая важная, но не столь очевидная черта React - это односторонний поток данных и соответствующая ментальная модель, что помогает структурировать код, оставаясь при этом гибким.
Что не так с React
Создание компонента
В React рекомендуется использовать функциональные компоненты вместо классовых. Это, безусловно, облегчает работу.
Рассмотрим такой пример:
function CounterButton() {
const [count, setCount] = useState(0);
const handleClick = () => setCount(count + 1);
return (
<button onClick={handleClick}>
You clicked {count} times
</button>
);
}
Функция CounterButton
является конструктором компонента и функцией, обновляющей данные. Такое смешение сфер ответственности - плохая практика, и это порождает множество проблем, о которых сейчас и поговорим.
Обновление компонента
React вызывает функцию CounterButton
каждый раз при обновлении данных. Все объекты, созданные внутри этой функции, будут создаваться заново при каждом ее вызове.
В примере функция handleClick
является таким объектом. Можно было бы сэкономить ресурсы, если бы эта функция создавалась один раз в конструкторе. Но его нет в функциональных компонентах.
Также функция CounterButton
возвращает новый объект виртуального DOM при каждом вызове.
Новые объекты создаются на куче, а старые удаляются сборщиком мусора. При этом возникает фрагментация и перерасход памяти. Также периодически должна выполняться ее дефрагментация. Все эти процессы ресурсоёмкие.
Хочу отметить, что в нашем конкретном примере эта особенность работы React не имеет значения. Но для больших приложений, в которых используются сотни и тысячи компонентов, это становится заметным.
Поэтому команда React, начиная с версии 17, начала разработку параллельного режима, чтобы дать возможность обновлять пользовательский интерфейс во время обработки процессов React.
Хуки
Можно было бы возразить, что использование хуков помогло бы частично исправить ситуацию с созданием лишних объектов.
Кстати, в одной компании, где я работал, была политика - всегда использовать хуки.
Итак:
const handleClick = useCallback(
() => setCount(count + 1),
[count]
);
С одной стороны, хуки позволяют забыть об избыточных обновлениях компонентов, находящихся в дереве ниже. Но с другой стороны, они никак не решают проблемы создания "мусорных" объектов. Так, функция () => setCount(count + 1)
создается, чтобы быть отброшенной хуком, если count
не изменился. Более того, создается новый объект массива [count]
.
То же самое происходит и с другими хуками. Например, сравните: constructor(){code();}
и useEffect(() => {code();}, [])
. В первом случае код выполнится один раз в конструкторе. Во втором - на каждом обновлении будут создаваться два лишних объекта.
Также сам механизм хуков является дополнительной логикой, которая влияет на производительность. Но самый главный недостаток хуков, по моему мнению, это их многословность. Как же это невероятно скучно - писать обертки для простейших операции. А еще читабельность кода страдает.
Что в итоге
- Много лишних объектов создается и удаляется на куче, вызывая ее фрагментацию, перерасход памяти и дефрагментацию, что ухудшает производительность.
- Вводится больше логики для параллельного режима, которая ухудшает производительность.
- Дополнительная логика работы хуков также ухудшает производительность.
- Хуки делают код более многословным и сложным для понимания.
Как улучшить
Далее, я хотел бы предложить решение вышеуказанных проблем.
Итак, если упрощенно, нам надо:
- Улучшить производительность.
- Уменьшить многословность.
- Сделать код более явным и простым для понимания.
Конструктор компонента
Начнем с гипотетического примера:
function CounterButton() {
let count = 0;
const handleClick = () => {
count++;
btn.update();
};
const btn = button(
{ click$e: handleClick }, // props
() => `You clicked ${count} times`, // child
);
return btn;
}
Выглядит очень похоже на пример, написанный на React. Для простоты пока опустим JSX.
Единственная неизвестная составляющая здесь - это функция button
, в которой происходит вся "магия". Можно представить, как она могла бы работать, исходя из нашего примера.
Нужно, чтобы функция button
выполняла следующие действия:
- Создавала объект DOM
HTMLButtonElement
. - Устанавливала для него обработчик события клика мышки
handleClick
. - Инициировала текст кнопки возвращённым значением лямбда-функции
() => `You clicked ${count} times`
. - Предоставляла возможность обновлять текст кнопки результатом вызова лямбда-функции.
Логично, что функция button
должна создать и вернуть объект с двумя свойствами: element
и update
.
Класс этого объекта мог бы выглядеть так:
class Component {
get element() {}; // вернуть объект DOM Element
update() {}; // обновить динамические данные
}
При клике на кнопке происходит увеличение счетчика на единицу count++
. Затем вызывается метод btn.update()
, который выполняет лямбда-функцию и обновляет текст кнопки.
Использование компонента
Теперь присоединим этот компонент к дереву DOM:
document.body.append(
CounterButton().element,
);
Сначала вызваем функцию CounterButton
, которая создает и возвращает компонент, а затем присоединяем его элемент к дереву DOM.
Теперь кнопка со счетчиком должна отобразиться и корректно считать количество кликов.
Хорошо, еще предположим, что:
- Помимо
button
, есть весь набор HTML-функции:h1
,div
,span
... - Созданные этими функциями компоненты могут содержать дочерние компоненты и так до бесконечности.
- При обновлении родительского компонента будут обновлены его дочерние компоненты.
Всё! 🤗
Миссия выполнена.
Результат
Готов поспорить, вы ожидали нечто большее.
Эта концепция работы с компонентами решает все вышеизложенные проблемы, а также сохраняет хорошие черты React.
Не стоит переживать, что обновления компонентов делаются явно. В основном обновления триггерят компоненты верхнего порядка. Но можно и точечно обновить любую часть.
Кстати, в React также нужно вызывать обновления явно. Функция
setState
и хукuseState
служат этой цели. Только менее гибко и более ресурсозатратно. Например, вызовsetCount(count + 1)
вызовет установку переменной через механизм состояния, затем добавит в очередь необходимость обновления через механизм обновлений. Как видно, тут снова происходит смешение сфер ответственности.
Итого, вышеизложенная концепция помогает решить проблемы следующим образом:
- Во-первых, функция создания компонента является его конструктором и вызывается лишь один раз. Соответственно, объекты, созданные внутри функции, также создаются один раз и не нуждаются в таких хаках, как хуки.
- Нет хуков. Нет дополнительной логики. Все происходит наглядно и читаемо.
- Параллельный режим становится не нужен, так как, во-первых, логики стало значительно меньше, а во-вторых, мы полностью контролируем процессы создания и обновления и можем легко вставить прорисовку интерфейса там, где нужно.
Fusor (Фьюзор)
Что это такое
Fusor - это простая библиотека, помогающая декларативно создавать и обновлять элементы DOM.
Во Fusor нет дополнительных механизмов для:
- Свойств
- Состояния
- Контекста
- Жизненного цикла
Fusor - это максимально "легкий" и прозрачный подход "почти без библиотеки" по полной использующий конструкций самого языка Javascript и функции DOM.
Но тем не менее Fusor может полноценно заменить собой React! Как такое возможно? Давайте разбираться.
Экономика должна быть экономной
Fusor - это экономичная библиотека.
Fusor не порождает кучу лишних объектов на куче.
Если взять такой пример:
import { div, p } from '@fusorjs/dom/html';
const wrapper = div(
p('I am the static text')
);
То переменная wrapper
будет содержать объект HTMLDivElement
, а не Component
, как в примере с кнопкой-счетчиком, так как здесь нет динамических частей.
Если же взять пример с кнопкой и немного его изменить:
import { button } from '@fusorjs/dom/html';
function CounterButton() {
let count = 0;
const handleClick = () => {
count++;
btn.update();
};
const btn = button(
// props:
{ click$e: handleClick },
// child text nodes:
'You clicked ', // static
() => count, // dynamic
' times', // static
);
return btn;
}
То можно увидеть, что теперь только один из трех дочерних элементов кнопки является динамическим. А переменная btn
будет уже объектом класса Component
.
При обновлении точечно будет изменено значение только одной текстовой ноды, к которой привязана лямбда-функция () => count
, и только если значение будет отличаться от уже находящегося там.
Таким образом, дополнительный объект компонента создается только в том случае, если он содержит в себе динамические данные.
Также динамические данные могут быть в свойствах. Например {class: () => selected ? 'selected' : 'unselected'}
.
Жизненный цикл
Жизненный цикл компонентов - это единственный механизм, которого недостаёт для полноценной замены React.
Так как Fusor делает одну вещь и делает её хорошо, то в нём нет логики жизненного цикла. Зато такая логика есть в нативных кастомных элементах.
А во Fusor есть 100% поддержка всех вэб стандартов, в том числе и вэб компонентов. Поэтому можно использовать их для подключения событий жизненного цикла.
Тем не менее для удобства во Fusor добавлен кастомный элемент fusor-life
и его компонент-обёртка Life
:
import { Life } from '@fusorjs/dom/life';
const wrapper = Life(
{
connected$e: () => {},
disconnected$e: () => {},
// ... other props
},
// ... children
);
Сравните это с механикой жизненного цикла React и обходом дерева компонентов O(n)
.
Fusor | fusor-life | React | |
---|---|---|---|
Mounting | constructor | connected | constructor, getDerivedStateFromProps, render, componentDidMount |
Updating | update | attributeChanged | getDerivedStateFromProps, shouldComponentUpdate, render, getSnapshotBeforeUpdate, componentDidUpdate |
Unmounting | disconnected | componentWillUnmount |
Кастомизация
Необязательно использовать функции, находящиеся в html
, svg
, или life
. Они существуют, чтобы не пришлось создавать их самостоятельно, а также чтобы показать на их примере, как это делается.
Например, если нужно создать определенный набор HTML-тэгов, можно легко сделать это.
Если нужно использовать одну функцию для всех элементов, то можно использовать функции h
для HTML или s
для SVG. Например: h('div', props, children)
. Либо написать другие вариации.
Существует и более гибкая функция create(element, props, children)
. Используя ее, можно настроить работу JSX с Fusor.
О JSX
Поддержка JSX будет.
Функциональная нотация тоже хороша. Потому что:
- Это чистый JavaScript с обычными комментариями.
- Не нужно преобразования, сборки или компиляции.
- Можно использовать любое количество props и children в любой последовательности.
Ссылки
Полноценные приложения и другие ресурсы:
- Кнопка-счетчик - интерактивный пример приложения.
- Туториал с рецептами (код) - это интерактивное приложение с основными сценариями использования Fusor: жизненный цикл, запрос, роутинг...
- Имплементация TodoMVC (код) - приложение с помощью которого был разработан Fusor. Поэтому оно получилось не самым простым и красивым, но зато идеологически правильным. Также в этом приложении не нужны события жизненного цикла.
- Репозиторий Fusor и документация.
Заключение
Приложения есть. Примеры основных кейсов использования есть. Покрытие тестами тоже есть.
АПИ стабилизировался достаточно давно. Можно использовать Fusor в продакшене.
npm install @fusorjs/dom
PS: Спасибо всем, кто дочитал до конца! 🤗 ❤️
Fusor vs React
Fusor | React | |
---|---|---|
Component constructor | Explicit, function | Combined with updater in funtion components |
Objects in Component | Created once | Re-created on each update even with memoization |
State, effects, refs | Variables and functions | Complex, hooks subsystem, verbose |
Updating components | Explicit, flexible | Implicit, complex, diffing |
DOM | Real | Virtual |
Events | Native | Synthetic |
Life-cycle | Native, custom elements | Complex, tree walking |