Правильный способ заставить Vue перендерить компонент
Эта публикация является любительским переводом статьи «The correct way to force Vue to re-render a component» от Майкла Тиссена.
Иногда бывает так, что реактивности Vue не хватает, и вам нужно перерендерить компонент. Или вам нужно сбросить DOM и начать сначала.
Так как же правильным образом заставить Vue перерендерить компонент?
Ответ прост. Правильный путь — использовать :key
в компоненте. Когда вам нужно перерендерить компонент, вы просто меняете значение ключа и Vue обновит компонент.
Довольно просто, не так ли? Есть также и другие способы:
- Худший вариант: обновить всю страницу,
- Плохой вариант: использовать
v-if
хак, - Чуть лучше: использовать встроенный в Vue метод
forceUpdate
, - Лучший вариант: использовать изменение ключа.
Обратите внимание! Если вы столкнулись с ситуацией, в которой вам нужно перерендерить компонент, вероятно, есть более правильный способ решить задачу. Скорее всего, вы не до конца разобрались с чем-то из этого: 1. Реактивность Vue, 2. Computed-свойства, 3. Watch-свойства, 4. Не используете :key
атрибут для v-for
.
А теперь рассмотрим доступные способы перерендерить компонент. Почти все из них заменяемы на вариант с изменением ключа, он рассмотрен в самом конце статьи.
Худший вариант: перезагрузить все страницу
Этот способ — примерно то же самое, что перезагрузка компьютера каждый раз, когда вы хотите закрыть какое-нибудь приложение. Звучит странно, правда?
Полагаю, это и правда может когда-то вам помочь. Однако это ужасное решение, поэтому не используйте его. Совсем.
Лучше посмотрите, что вы можете использовать ещё.
Плохой вариант: использовать v-if
хак
Vue содержит директиву v-if, которая позволяет рендерить компонент или часть компонента только тогда, когда условие возвращает истину. Если в условии ложь, то всё что внутри не отрендерится и не будет существовать в DOM.
Давайте посмотрим, как это выглядит на практике.
Добавьте v-if в ваш шаблон:
<template>
<my-component v-if="renderComponent" />
</template>
В секции script
добавьте метод, который использует функцию nextTick
:
<script>
export default {
data() {
return {
renderComponent: true,
};
},
methods: {
forceRerender() {
// Сначала скроем компонент
this.renderComponent = false;
this.$nextTick(() => {
// А потом покажем снова
this.renderComponent = true;
});
}
}
};
</script>
Вот что происходит в этом коде:
- Сначала
renderComponent
установлено в true, поэтому компонентmy-component
рендерится - Когда мы вызываем
forceRerender
, мы сразу устанавливаем renderComponent вfalse
- Мы прекращаем рендеринг
my-component
, потому чтоv-if
директива содержит ложь - На следующем шаге (
nextTick
) мы возвращаемrenderComponent
вtrue
- Теперь
v-if
содержит истину, поэтому мы снова рендерим компонентmy-component
В этом примере есть два важных момента, которые важны для понимания того, как это работает. Первое — мы должны дождаться nextTick, иначе мы не увидим никаких изменений.
В Vue tick (тик) — это один цикл обновления DOM. Vue собирает все обновления в одном тике, и в конце этого тика обновляет то, что отображается в DOM в процессе этих обновлений. Если мы не дождёмся следующего тика, то наши изменения свойства renderComponent
перезапишут сами себя и ничего не изменится. Поэтому мы используем nextTick
.
Второе, Vue создаст совершенно новый компонент, когда мы отрендерим его второй раз. Vue уничтожит первый компонент и создаст новый. Это значит, что наш новый my-component
пройдёт все хуки жизненного цикла компонента — created
, mounted
Кстати, вы можете использовать nextTick
с промисами:
forceRerender() {
// Удаляем компонент из DOM
this.renderComponent = false;
// Если предпочитаете промисы, можете использовать их так
this.$nextTick().then(() => {
// Возвращаем компонент обратно
this.renderComponent = true;
});
}
Тем не менее, это не очень хорошее решение. Скорее, это хак. Поэтому давайте посмотрим, что ещё мы можем использовать.
Способ получше: можно использовать forceUpdate
Это один из двух способов, который отлично решает проблему. При этом, он официально поддерживается Vue и не является хаком или чем-то подобным.
Обычно Vue реагирует на изменения в зависимостях, обновляя представление. Однако, когда вы запускаете forceUpdate, вы можете принудительно запустить обновление представления, даже если зависимости никак не изменились.
И вот здесь большинство людей делает множество серьёзных ошибок.
Если Vue автоматически обновляет всё когда что-то меняется, то зачем нам самим заставлять его что-то обновлять?
Причина в том, что иногда реактивность Vue может сбивать с толку. Мы думаем, что Vue отреагирует на изменения свойства или переменной, но этого не происходит. Есть также определённые ситуации, когда реактивность Vue вообще не распознаёт ваши изменения.
Стоит отметить, что forceUpdate
всё же не лучший способ решения задачи. Используйте его аккуратно.
There are two different ways that you can call forceUpdate
, on the component instance itself as well as globally:
// Глобально
import Vue from 'vue';
Vue.forceUpdate();
// Внутри вашего компонента
export default {
methods: {
methodThatForcesUpdate() {
// ...
this.$forceUpdate(); // Заметьте, что мы используем $ в качестве префикса
// ...
}
}
}
Важно: этот способ не обновляет computed-свойства. Вызов forceUpdate
только заставляет шаблон перерендерится.
Лучший способ: изменение ключа
Что за ключ? Сейчас во всё разберёмся. Вообще, существует много ситуаций, когда перерендеринг может действительно вам потребоваться.
Для того, чтобы сделать это надлежащим образом, мы свяжем наш компонент с атрибутом key
. Так Vue свяжет компонент с отдельным куском данных. И если ключ при изменениях останется прежним, то Vue не обновит компонент, а вот если ключ поменяется — Vue избавится от старого компонент и создаст новый. То, что нам нужно!
Давайте сперва пробежимся по тому, что такое key
и почему нам нужно его использовать.
Зачем нужен атрибут key в Vue?
Давайте представим, что вы рендерите список компонентов, у которых есть одно или несколько из этих свойств:
- У компонента есть локальный стейт,
- У компонента есть логика инициализации: что-то, что использует mounted или created хуки,
- Не реактивные DOM-манипуляции: обычно это jQuery-решения или стандартные JS API.
Если вы отсортируете этот список или обновите его каким-либо образом, вам нужно будет повторно отрендерить все части этого списка. Вряд вы захотите перерендерить вообще все элементы этого списка, лучше затронуть лишь то, что изменилось.
Для помощи Vue в отслеживании таких изменений мы внедряем атрибут key. Использование индекса массива тут не поможет, поскольку индекс не привязан к конкретным объектам в нашем списке.
Here is an example list that we have:
const people = [
{ name: 'Ваня', age: 34 },
{ name: 'Марина', age: 25 },
{ name: 'Николай', age: 51 },
];
Если мы отрендерим список с использованием индексов, то получим это:
<ul>
<li v-for="(person, index) in people" :key="index">
{{ person.name }} - {{ index }}
</li>
</ul>
// Outputs
Ваня - 0
Марина - 1
Николай - 2
Если мы удалим Марину, то получим:
Ваня - 0
Николай - 1
Индекс, который был связан с Николаем изменился, хотя Николай всё ещё Николай. При этом, Николай перерендерится, хотя мы этого не хотели.
В нашей ситуации подойдёт какой-то уникальный ID, давайте добавим его.
const people = [
{ id: 'this-is-an-id', name: 'Ваня', age: 34 },
{ id: 'unique-id', name: 'Марина', age: 25 },
{ id: 'another-unique-id', name: 'Николай', age: 51 },
];
<ul>
<li v-for="person in people" :key="person.id">
{{ person.name }} - {{ person.id }}
</li>
</ul>
Когда мы удаляли Марину из нашего списка, Vue удалил компоненты для Марины и Николая, а затем создал новый компонент для Николая. Теперь, когда Vue знает ID, он сохранит два компонента Вани и Николая, и удалит лишь компонент Марины.
Если мы добавим ещё кого-то в список, то Vue знает, что может сохранить текущие компоненты и просто создать один дополнительный. Это действительно полезно, когда у вас есть более сложные компоненты с логикой, собственным состоянием, имеют логику инициализации или выполняют какие-то манипуляции с DOM.
А теперь давайте продолжим с лучшим методом для принудительного повторного рендеринга компонентов в Vue.
Смена ключа для принудительного повторного рендеринга компонента
Наконец-то, лучший способ принудительного повторного рендеринга компонента (по мнению автора, конечно же).
Вы применяете эту стратегию назначения ключей дочерним элементам, но каждый раз, когда вы хотите повторно отрендерить компонент, вы просто обновляете ключ.
Вот простой пример того, как это работает:
<template>
<component-to-re-render :key="componentKey" />
</template>
<script>
export default {
data() {
return {
componentKey: 0,
};
},
methods: {
forceRerender() {
this.componentKey += 1;
}
}
}
</script>
Каждый раз, когда мы в компоненте вызываем forceRerender
, наш пропс componentKey
изменяется. Это в свою очередь приводит к тому, что Vue узнаёт когда нужно уничтожить инстанс компонента и создать новый.
В результате вы получаете дочерний компонент, который способен сам себя переинициализировать и сбросить своё состояние. Простой и элегантный способ решения проблемы.
И помните, что если вы столкнулись с ситуацией, когда вам нужно принудительно перерендерить компонент, то вероятно вы делаете что-то не лучшим образом. Если всё же вам нужно как-то перерендерить компонент, то выбирайте способ с привязкой ключа.
Дополнение от переводчика
Стоит упомянуть отдельно Vuex и связанную с ним реактивность. Дело в том, что если у вас в глобальном сторе хранится сложный объект или многомерный массив, то Vue не сможет отслеживать изменения в них, если делать это неправильно. В документации Vue, в описании реактивности, есть информация о том, как работать со сложными объектами и массивами, чтобы достичь реактивности.
Полная чушь бред
А почему?
Кирилл, очень полезно для меня было попасть на вашу статью.
Благодарю.
Спасибо за статью, помог метод с ключом, правда совсем в другой ситуации (swiper slider отказывался удалять классы с пагинации при destroy (), добавил изменение ключа и вуаля)