Хочу рассказать о небольшом кейсе, связанном с работой реактивности во Vue 3. Кейс касается взаимосвязи ref/reactive, v-for/v-if, :class, функций и того, что у нас находится в <template>. Сразу оговорюсь, что под капотом не смотрел, поэтому детальных объяснений не ждите. Наоборот, хотелось бы услышать ваши мнения, сталкивались ли вы с подобными сайд эффектами.
Представим что у нас есть реактивная переменная, внутри которой находится пустой массив.
const arr = ref([]);
В этот массив по клику на кнопку добавляется один рандомный элемент.
const add = () => {
const random = Math.floor(Math.random() * 10);
arr.value.push(random);
}
<button @click="add">Add</button>
Если мы используем arr напрямую в разметке внутри тега <template>, то нашим ожидаемым поведением является изменение в разметке и вызов апдейта компонента на каждый клик по кнопке. А если мы не используем arr в том или ином виде в разметке, то, соответственно, апдейта компонента не происходит, хотя сами данные, хранящиеся в arr изменяются.
// Вызов onUpdated при каждом добавление нового элемента.
onBeforeUpdate(() => {
console.log('before update');
})
onUpdated(() => {
console.log('updated');
})
<span>{{ arr }}</span>
Логично, что мы можем распространить наши знания на v-if. Так, если мы используем его в разметке и проверяем результат вызова функции на true/false, то вызов самой функции происходит один раз при добавлении компонента в DOM.
const fn = () => {
console.log('fn');
console.log(arr.value);
return true;
}
<span v-if="fn()">fn</span>
Однако, если немного видоизменить функцию add, которая будет присваивать в arr пустой массив, то fn будет вызываться при каждом клике на кнопку вместе с апдейтом компонента. При этом такое не работает с примитивами, в этом случае апдейт будет только один раз, если значение переменной не изменяется на втором и последующих кликах.
// Вызов onUpdated при каждом добавление нового элемента.
const add = () => arr.value = [];
// Вызов onUpdated только при первом вызове функции add.
const add = () => arr.value = 1;
В случае с v-if важно подчеркнуть, что вызов функции fn при апдейте будет ровно столько раз, сколько <span v-if="fn()">fn</span> у нас есть на странице.
С каким сайд эффектом описанной выше логики столкнулся на одном из рабочих проектов лично я. Представим, что у нас есть список ul, каждый элемент li с директивой :class. При клике на li добавляется элемент в массив arr. А функция check, соответственно возвращает true/false при проверке наличия элемента в массиве. Если check вернул true, то к соответствующему li добавляется класс .active, который меняет цвет на red.
const check = (idx) => {
console.log('check');
const found = arr.value.find((item) => item === idx);
if(found) return true;
else return false;
}
const add = (el) => arr.value.push(el);
<ul>
<li :class="{'active': check(1)}" @click="add(1)">1</li>
<li :class="{'active': check(2)}" @click="add(2)">2</li>
<li :class="{'active': check(3)}" @click="add(3)">3</li>
</ul>
// Косоль при клике
[Log] before update
[Log] check
[Log] check
[Log] check
[Log] updated
Первый раз функция check закономерно вызывается три раза при маунте компонента. Поскольку arr пустой, то и класса .active нет ни у одного элемента li. Но при этом мы получаем изменение класса у элемента li при клике на него. Происходит это потому, что при клике меняется значение arr, затем срабатывает апдейт компонента. После onBeforeUpdate отрабатывает функция check равно столько раз, сколько у нас элементов li. В результате проверки чек у нас добавляется класс там, где check вернул true.
В итоге задача, поставленная перед разработчиком была выполнена. Тестировщик проверил и отметил, что все работает корректно - класс меняется при клике на элемент. Однако, на мой взгляд, такое решение не оптимальное. И даже не столько из-за вызова функций на каждом элементе li . По сути, мы сталкиваемся с сайд эффектом реактивности и жизненного цикла компонента, изменения в которых зависят от самого движка Vue. Логично, что сделав arr не реактивным, мы лишаемся апдейта и изменения класса по клику.
Насколько вы считаете подобное поведение явным или неявным? Стоит ли прибегать к таким практикам в рабочем коде? С какими подобными сайд эффектами сталкивались вы?
Комментарии (5)

Vadiok
12.10.2023 11:51Чтобы понять, почему так происходит, стоит посмотреть, как выглядит скомпилированная из шаблона render функция.

stvoid
12.10.2023 11:51+2Ну, полагаю вы можете просто создать некоторый один computed, где составите карту карту по значениями классов для элементов списка (в реальности например по каким нибудь taskId и т.п. что будет в этом списке), и тогда по сути вы избежите таких массовых вызовов.
Но хозяин барин, в именно таком кейсе не вижу подвоха в своем предложении.

Tyusha
12.10.2023 11:51+1На мой взгляд использовать функцию для :class это моветон с концептуальной точки зрения, для этого есть computed свойства. Функция — это действие. Значение :class — это свойство.
Создатели Vue обо всëм позаботились. Не надо смешивать тëплое с мягким, и не будет ненужных головоломок с реактивным поведением. Видишь в шаблоне @ — используй функцию, видишь : — приписывай свойство.
Я не большая программистка (я не программистка вообще), но на мой взгляд, если вам понадобились хуки, то стоит остановиться и подумать, возможно вы что-то делаете не так.

danilovmy
12.10.2023 11:51@Tyusha- Вот спасибо, буду цитировать:
Видишь в шаблоне @ — используй функцию, видишь : — приписывай свойство.
@yudeek - а что мешало создать компонент "элемент списка". Хранит свой стейт внутри, при необходимости перерендер только одного компонента, DRY опять же, код проще:
<template> <li :class="{'active': clicked}" @click="clicked = !clicked"> <slot></slot> </li> </template> <script> export default { data () { return { clicked: false } } } </script>нотация работает как в Vue.js 2, так и в Vue.js 3.
Ksey_N
Нет тут сайд-эффекта.
Функции во vue не кэшируют своего значения и поэтому, если мы в вёрстке используем фукнции, каждый раз при обновлении состояния они будут отрабатывать заново.
Для того чтобы этого не происходило во vue есть computed свойства.