Vue-FAQ
943 subscribers
583 photos
93 videos
568 links
Канал сайта https://vue-faq.org
Информация о Vue.js, фронтенд разработке и не только

Contacts: @RuslanMakarov
Download Telegram
Создатель Solid.js Ryan Carniato год назад написал статью об эволюции Signals в JavaScript (чья концепция лежит и в основе реактивности Vue.js)

#reactivity #solidjs
🔥153
Попались на глаза как-то исходники одного расширения к VS Code.
Автор использует reactive, ref и watch из Vue, чтобы организовать в нём реактивность. UI части нет.

Еще один пример использования Vue реактивности в Node.js приложении.

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

#reactivity
👍4
Во Vue 3 есть важная и нередко используемая конструкция, у которой нет имени. Это то, что обычно называют "композабл с глобальными рефами".

Но:
1. Это не композабл по определению
2. Там не обязательно рефы
3. Они не "глобальны"

В общем случае структура данного объекта - экспортируемые из ES модуля реактивные данные и функции для работы с ними.

Функционально они заменяют "сторы" Pinia. Называть их тоже stores двусмысленно и нелогично. Вообще, молиться на "глобальный стейт" после появления идеи JavaScript signals и их всевозможных реализаций, включая Vue Reactivity API, архаично.

Мне кажется, самое подходящее название для данной конструкции - [реактивный] бизнес объект (РБО). В них инкапсулируется логика предметной области и приложения, они не привязаны к конкретным компонентам, и по аналогии с другими языками и фреймворками, этот паттерн - Business Object - выглядит вполне подходящим.

Кроме того, позиционирование именно как "бизнес объект" будет требовать явного отделения от него инфраструктурного слоя - работы с Backend API, например. То есть, стимулировать использование лучших практик и наработок из других сфер разработки ПО, еще более переводя Vue.js из фреймворка для небольших проектов в разряд enterprise level решений.

#architecture #vuejs #reactivity #rbo #composables
👍7
Мы уже писали пару раз о применении Reactivity API пакета вне Vue фронтенд приложений.

Вот новая библиотека/starter-kit, которая использует Vue Reactivity API для упрощения создания VS Code расширений. Путем отображения VSCode extension API (которая основана на event driven архитектуре) в композаблы. Несколько понижается производительность, значительно повышается DX.

Пока что Vue Reactivity API выглядит лучшей "реализацией" JS Signals.

#vscode #reactivity
🔥6
Я уже писал, как в 30 строк сделать глобальный прелоадер AppLoader на всё приложение.
В компонентах он включается/выключается так:

const { startLoading, stopLoading } = useAppLoader();

onMounted(async () => {
startLoading();
xr.value = await loadXr(props.vid);
stopLoading();
});


Всё работало прекрасно. Тут понадобилось кэшировать приходящие данные. Верней, приходили они партиями, и при выборе одного элемента через props.vid он мог как оказаться уже в кэше, так и его нужно было грузить по сети. Для этого была сделана промежуточная утилита (js модуль), которая экспортировала асинхронный метод loadXr().

После этого пришла мысль - а где должны быть startLoading() / stopLoading() для глобального прелоадера? Зачем им срабатывать, когда данные берутся из кэша? Перенес их в ту самую утилиту. В итоге компонент стал выглядеть так:

onMounted(async () => {
xr.value = await loadXr(props.vid);
});

// или в корне setup-a:

loadXr(props.vid).then((data) => {
xr.value = data;
})


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

<Suspense>
, который уже много лет пытаются внедрить во Vue, выглядит совсем не как решение для серьезных приложений, где нужно явно контролировать потоки данных, а, скорей, уровня Nuxt.

Мощь Vue Reactivity API сильно недооценена, - в том числе, в официальной документации. Не надо бояться ее использовать.

#reactivity
👍6
Во Vue реактивными могут быть не только обычные переменные, но также ссылка на функцию, что вполне в духе JS:

<script setup>
import { computed, ref } from 'vue'

const f = ref(() => "Hello");

const v = computed(() => {
return f.value();
});

setTimeout(() => {
f.value = () => "World";
}, 3000 );
</script>

<template>
<h1>{{ v }}</h1>
</template>


Реактивность будет не глубокой (shallow)

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

Перед заменой функции происходит изменение некоего состояния компонента/приложения. Вот на него лучше и ориентироваться.

#reactivity
👍1
Чтобы выделить из компонента отдельно некий функционал с реактивным состоянием, были созданы composable функции (называемые "функциями", хотя по сути это объект созданный через js замыкание - closure).

Чтобы использовать функционал с реактивным состоянием между несколькими компонентами, можно задействовать js модули либо Pinia/Vuex сторы.

Иногда нужно нечто среднее - подобную сущность с реактивным стейтом и функционалом на нем, но используемую в нескольких связанных компонентах (например, в Tabs или каких-то виджетах), - то есть, в некотором локальном контексте. Для этого можно в общем родительском компоненте создать composable, который передать потомкам - либо через provide/inject (лучше), либо через props (не надо).

В этом случае данный реактивный объект будет жить в своем ограниченном контексте и не будет конфликтовать с оставшейся частью проекта.

#reactivity #bestpractices #architecture
👍5🔥5🤔21
Во Vue 3 вышла Reactive Props Destructure - деструктуризация пропсов

Основной плюс - возможность указывать дефолтные значения более человеческим способом:

const { count = 0, msg = "hello" } = defineProps<{
count?: number;
message?: string;
}>();

const double = computed(() => count * 2);


Как это делалось ранее (и можно делать сейчас):

const props = withDefaults(
defineProps<{
count?: number;
msg?: string;
}>(),
{
count: 0,
msg: "hello",
}
);


Деструктуризация пропсов - это осколки невзлетевшего Reactivity Transform

По сути деструктуризация пропсов это просто макрос, скрывающий под собой обычную работу с пропсами:

// на входе
const { count } = defineProps(["count"]);
console.log(count);

// на выходе компилятора
const __props = defineProps(["count"]);
console.log(__props.count);


Минусы:

- Деструктурированные пропсы могут быть случайно переданы в функцию, что приведет к потере реактивности

- Пропсы перестают быть явными (становятся неотличимы от обычных переменных)

- Путаница для новичков с магией компилятора

Решение:

Не забывать работать с ними как с props.X либо не использовать деструктуризацию.

#props #reactivity
👍15👎1
Поговорим о Shared composables

Страшный термин, который многие понимают, как хотят.

Во Vue 3 у компонента есть, по большому счету, три варианта работы с внешними реактивными данными

1. Composable

Это технически (JS closure) и по факту (ООП) объект с данными и поведением. Компонент его создает, пользуется, при уничтожении компонента обнуляется ссылка на объект, и он подлежит уборке Garbage Collector-ом (GC).

2. "Модульный реф" / Pinia store

Тоже можно считать объектом в смысле ООП, но реализующим паттерн Singleton. Создается при первом вызове его кем-либо, затем ссылка на него записывается в некий глобальный реестр, и любой другой компонент, обращающийся к этой структуре, получает эту ссылку и работает с тем же самым инстансом.

3. Shared composable как в примерах в документации во VueUse и Vue

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

Некоторые разработчики (не будем показывать пальцем) уверены, что использование shared composable именно как реактивного стейта вместо модульного рефа/стора помогает сделать программу эффективней.

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

Вот у нас есть модульный реф с рефом. Вот он используется, потом все компоненты со ссылкой на него, уничтожаются. Что происходит? Ничего. У него нет внешних зависимостей, он просто лежит мертвым грузом в памяти, занимая свои несчастные пару килобайт. Появился использующий его компонент - он заработал.

Как с shared composable? Вот он создается, используется, потом все компоненты, его использующие, кончаются - и он должен типа уничтожиться. Вызывается код по обнулению, но у Garbage CollectorJS VM своя логика работы, и он не будет чистить эту память сразу. А если и будет - потратит на это ресурсы CPU. Затем снова появляется компонент, которому нужен этот объект - и он снова создается, тратятся ресурсы CPU, выделяется новая память. И так по кругу. В итоге, через 10 циклов у нас в куче памяти в 10 раз больше, чем нам надо, зарезервированного места, и мы потратили в десять раз больше ресурсов на создание/уничтожение структуры.

Так для чего нужен shared composable?

Посмотрим на примеры в документациях. Мы создаем реф с отслеживанием движения мышки. А вот это уже важно - если мы в каждом требующем координаты мышки компоненте будем создавать composable с этой логикой, мы будем множить слушателей этого события, и это дорогая операция. Держать слушателя постоянно в глобальном сторе - тоже ненужная трата ресурсов.

Таким образом,

состояние в shared composable выполняет не самостоятельную функцию, а является просто адаптером для некого внешнего сервиса / Web API.

И суть shared composable - создать единую реактивную точку доступа к внешнему сервису.

Даже чисто логически если подумать (по описанному выше юз-кейсу в пункте 3), то shared composable не может использоваться для непосредственного обмена данными между компонентами. Он может только передавать данные во внешний сервис, и получать их оттуда. Даже vueuse/useLocalStorage так работает. И если передача не удалась - это ошибка выполнения программы.

Но, опять же, если подумать, то данная логика (оптимизация работы с внешним сервисом) очень похожа на логику работы глобального лоадера. И реализована может быть так же в несколько строк, безо всяких effectScope и прочих заморочек - простым и понятным кодом. Нету там ничего из Composition API, что надобилось бы для реализации данной логики. Обычный модульный реф и синхронный код для отслеживания числа подписчиков с подключением/отключением этого рефа к внешнему сервису.

Ref, reactive, shallowRef, watch и computed должны покрывать 99.9% потребностей прикладного программирования на Vue.js. Не надо делать простое сложным.

Shared composable как он есть - изначально неудачное название и ненужная структура. Предлагается не использовать.

#composable #reactivity
👍14👎7
Мэйнтенер Volar Джонсон со своим помощником последние недели носятся с alien-signals - своей реализацией Signals в JS

Уверяют, что она самая быстрая, и пытаются встроить ее везде где могут - в системы стейт менеджмента для Solid.js, Dart, Lua. Движение unjs выходит за рамки js.

Пока наталкиваются на "непонимание".

Также хотят встроить ее во Vue. Эван, вроде, одобряет.

Но по поводу увеличения скорости на проценты хотелось бы спросить Эвана - зачем же он тогда перешел на js Proxy в своей системе реактивности и ухудшил производительность в десятки и сотни раз, если скорость так важна?

#vuejs #performance #reactivity
🤡5💩4🥴3
// useUserService.ts

function isAuthenticated() {
return !!user.value;
}


// Component A

<script>
const { isAuthenticated } = useUserService();
</script>

<template>
<p v-if="isAuthenticated()">hello</p>
</template>


Функции во Vue ведут себя не совсем как функции

В данном примере функция isAuthenticated() ведет себя как computed и будет вызываться каждый раз, когда user изменится или компонент А будет перерисовываться.

Происходит это из-за того, что Vue определяет все реактивные зависимости внутри тела функции и перерендивает компонент, когда кто-то из них меняется.

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

#reactivity #tip #optimization
10💩3👍2😁2👎1🌚1
Вышла вторая часть курса по Vue 3 Ильи Климова - "Реактивность"

Толковое объяснение "на пальцах". Яркий пример отличия учебника от документации.

Что не понравилось - смешивание понятия реактивности и связывания с шаблоном. Реактивные конструкции не приколочены к UI и могут использоваться и в других языках. Хотя, конечно, основное применение - динамичный пользовательский интерфейс.

#learning #klimov #rectivity
11💩7🔥2🤮2🤡2👍1👀1
При использовании сторов иногда получаются проблемы из-за перекрестных ссылок, которые дают ReferenceEror

При использовании модульных рефов может возникнуть аналогичная ситуация - например, когда переменная refA определяется в модуле А, который использует модуль B, а B вешает watch на refA. Причем это применимо только к системе реактивности Vue, потому что с обычными объектами такой проблемы нет.

#store #reactivity #architecture
Решения для проблемы выше

1. Переделать. Перекрестное использование сущностей - архитектурно плохая практика. Каждая должна содержать в себе только свое состояние и логику работы с собой. Если А использует B, значит B - утилитарен по отношению к А (например, А - какой-то бизнесовый стор, B - отвечает за открытие диалогов). Но тогда коду из А нечего делать в B.

К созданию сторов / модульных рефов надо подходить так же ответственно, как к проектированию схемы реляционной базы данных. Это координатный базис, и в нем не может быть перекрестных зависимостей.

Если есть логика, которая работает и с А, и с B, то, скорей всего, она прикладная для этих сторов, и должна лежать в отдельной сущности (компоненте, композабле, простом или реактивном модуле, сторе).

2. setTimeout / nextTick, как на картинке. Работает и с модульными рефами, но выглядит уродливым костылём

3. Шина событий (eventBus) для сообщений между сторами. Как самостоятельное решение возможно, но в данном случае опять же костыль.

4. Если refA в примере вынести в отдельный модуль, то ошибка пропадет. То же самое, скорей всего, справедливо и для сторов, но будет выглядеть неуклюже. Выносить надо не голый стейт, а разделять стор грамотно, по ответственности.

5. Не надо пихать реактивность туда, где можно обойтись без нее. Если переменная B зависит от А, и источников изменения А всего один-два, то необязательно ставить watch над А в B, можно обновлять B императивно напрямую. Это уберет прямую зависимость от А в B (если код в А уже как-то использует B), а также повысит читаемость и производительность. В первую очередь касается кода, который работает с бэкенд API.
Так же приоритетно, как и пункт 1.

#store #reactivity #architecture
👍5
Если у вас есть массив, в каждом элементе которого есть computed, то лучше создать один на весь массив, чем много для каждого элемента

// Bad
const rows = productRows.map(row => ({
...row,
total: computed(() => row.price * row.qty),
}));


// Good
const computedRows = computed(() =>
productRows.map(row => ({
...row,
total: row.price * row.qty,
}))
);


#tip #performance #reactivity
🗿19👍7🤯4
Глубокое клонирование реактивных объектов в Vue 3

Vue 3 использует Proxy для реактивности, что создает проблемы при попытке клонировать объекты. Стандартные методы работают не так, как ожидается:

const state = reactive({ user: { name: "Al" } });

// Проблемы:
const badCopy1 = { ...state }; // сохраняет Proxy-ссылки
const badCopy2 = JSON.parse(JSON.stringify(state)); // теряет методы и Proxy


3 рабочих способа

1. Комбинация toRaw + structuredClone

import { toRaw } from 'vue';

const original = reactive({ data: 123 });
const copy = structuredClone(toRaw(original));


2. Ручное глубокое копирование

function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
const clone = Array.isArray(obj) ? [] : {};
for (const key in obj) {
clone[key] = deepClone(obj[key]);
}
return clone;
}

const copy = reactive(deepClone(toRaw(original)));


3. Библиотечные решения

import { cloneDeep } from 'lodash-es';
const copy = reactive(cloneDeep(toRaw(obj)));


#tip #reactivity
👍11
Почему неконтролируемого использования watch лучше избегать?

1. Нарушение реактивного потока

watch часто скрывает логику реактивности. Например, отслеживание изменения переменной для вызова побочного эффекта (fetch) делает поток данных менее предсказуемым, особенно если эффекты затрагивают несколько компонентов.

Vue поощряет однонаправленную реактивность (данные -> шаблон). watch часто используется для обратной связи (данные -> данные), что усложняет отладку.

2. Плохая читаемость

Цепочки watch внутри компонента превращаются в плохо понимаемый в спагетти-код.

Плохо для рефакторинга - логика, разбросанная по watch, сложно переносится в composables.

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

watch с deep: true может создать нагрузку при отслеживании больших объектов или массивов.

watch часто дублирует логику, которую можно выразить через computed. Вычисляемые свойства кешируются и эффективнее.

Неотписанные watch в динамических компонентах (например, внутри v-if) могут накапливаться и вызывать утечки памяти.


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

#reactivity #watch
👍128
На vue-faq.org я написал, что JS классы во Vue лучше не использовать. Это не совсем правильно. Не надо использовать классы с реактивными свойствами. Но если через классы создаются объекты без реактивности внутри, которые ты уже помещаешь в реактивные массивы - то вполне вариант.

Для диаграммы на картинке я попробовал сперва построить структуру объектов на TS, получилось громоздко и запутанно. Переделал на классы - стало намного читабельней и меньше кода. Ну и типизация настоящая (почти), а не эрзац. Так что теперь это основной вариант для сложных структур данных.

#oop #reactivity
🥴43👍2