Хочу рассказать о небольшом кейсе, связанном с работой реактивности во 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)


  1. Ksey_N
    12.10.2023 11:51
    +4

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

    Для того чтобы этого не происходило во vue есть computed свойства.


  1. Vadiok
    12.10.2023 11:51

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


  1. stvoid
    12.10.2023 11:51
    +2

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

    Но хозяин барин, в именно таком кейсе не вижу подвоха в своем предложении.


  1. Tyusha
    12.10.2023 11:51
    +1

    На мой взгляд использовать функцию для :class это моветон с концептуальной точки зрения, для этого есть computed свойства. Функция — это действие. Значение :class — это свойство.

    Создатели Vue обо всëм позаботились. Не надо смешивать тëплое с мягким, и не будет ненужных головоломок с реактивным поведением. Видишь в шаблоне @ — используй функцию, видишь : — приписывай свойство.

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


    1. 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.