Бързина: кога се рисува наново
Накратко: Във Vue се рисува наново компонентът, който чете сменената стойност, и детето, ако props му са нови; първо се мери, после computed пази същия резултат.
Проблем
Всяка буква рисува целия списък
Горе има поле за търсене и бутон „Търси“. Списъкът се филтрира едва след „Търси“. ProductList пише в конзолата всеки път, когато се рисува. Напиши „мляко“ в полето и гледай конзолата.
Конзола
Пет букви, пет нови реда „рисувам ProductList“. Списъкът е същият: още не си натиснал „Търси“. Тук продуктите са осем и не се усеща. В истинския каталог са 2000, всеки с картинка и цена, и полето започва да изостава след пръстите.
Идеята
Кога компонентът се рисува наново
Компонентът се рисува наново, когато се сменят данните, които показва. Но кое значи „данните му“, двете библиотеки решават различно. И двете дават верен резултат; разликата е колко работа вършат.
Vue следи кои реактивни стойности чете всеки компонент, докато се рисува. Когато се смени draft, Vue рисува наново само компонентите, които четат draft: тук App. Детето се рисува наново, ако props му са различни от предишните (сравнява ги един по един).
Броячът в ProductList.vue е в onBeforeMount и onBeforeUpdate: Vue ги вика преди първото и преди всяко следващо рисуване на компонента.
Тук шаблонът подава :products="visible()": функция, която филтрира. Тя се пуска при всяко рисуване на App и връща нов масив. За Vue това е нов prop, затова рисува и ProductList.
Първо мери, после поправяй. Брояч в конзолата, както тук, е най-простото мерене. Vue DevTools имат времева линия (Timeline) с рисуванията на компонентите и времето им. Поправяш само рисуването, което е и излишно, и бавно.
Виж го
Виж кой светва
<script setup>const draft = ref('');const query = ref('');const visible = computed(() => filterByName(PRODUCTS, query.value));</script>
<template> <input v-model="draft" /> <button @click="query = draft">Търси</button> <ProductList :products="visible" @add="add" /></template>
<!-- ProductList.vue --><script setup>defineProps(['products']);onBeforeUpdate(() => console.log('рисувам ProductList'));</script>Стъпка по стъпка, с думи
- App пази текста в полето (draft) и търсенето (query). ProductList рисува картите. Конзолата брои колко пъти се рисува списъкът.
- Преди поправката шаблонът викаше функция, visible(). Всяко рисуване на App дава нов масив, затова Vue вижда нов prop и рисува ProductList наново.
- computed пази същия масив, докато query е същото. Vue следи какво чете всеки компонент и сравнява props на детето.
- Сега клавишът рисува само App и полето. Props на ProductList са същите, затова той е пропуснат, а броячът стои.
- „Търси“ сменя query. Масивът е нов, затова ProductList се рисува. Това рисуване е нужно: списъкът вече показва два продукта.
- Първо мери: брояч в конзолата или Profiler в DevTools. Поправяй само рисуванията, които са и излишни, и бавни.
computed пази масива, а функция в шаблона дава нов при всяко рисуване.Пробвай
Само „Търси“ рисува списъка
Направи така, че писането в полето и „Добави“ да не рисуват ProductList наново. Само „Търси“ трябва да го рисува, защото само тогава списъкът се сменя. Проверката пише „мляко“, добавя два продукта, натиска „Търси“ и брои редовете „рисувам ProductList“ в конзолата.
Конзола
Решение
App.vue<script setup>
import { ref, computed } from 'vue';
import ProductList from './ProductList.vue';
import { PRODUCTS } from './products.js';
const draft = ref('');
const query = ref('');
const count = ref(0);
const visible = computed(() => PRODUCTS.filter((p) => p.name.toLowerCase().includes(query.value.toLowerCase())));
function add() {
count.value++;
}
function search() {
query.value = draft.value;
}
</script>
<template>
<header>
<form role="search" @submit.prevent="search">
<input type="search" aria-label="Търсене" v-model="draft" />
<button>Търси</button>
</form>
<p>В кошницата: {{ count }}</p>
</header>
<main>
<ProductList :products="visible" @add="add" />
</main>
</template>
ProductList.vue<script setup>
import { onBeforeMount, onBeforeUpdate } from 'vue';
import { money } from './products.js';
defineProps(['products']);
const emit = defineEmits(['add']);
let renders = 0;
const log = () => console.log(`рисувам ProductList: ${++renders}`);
onBeforeMount(log);
onBeforeUpdate(log);
</script>
<template>
<ul>
<li v-for="p in products" :key="p.id">
<span>{{ p.name }}, {{ money(p.price) }}</span>
<button type="button" :aria-label="`Добави ${p.name}`" @click="emit('add', p)">
Добави
</button>
</li>
</ul>
</template>
products.jsexport const PRODUCTS = [
{ id: 1, name: 'Бял хляб', price: 1.2 },
{ id: 2, name: 'Пълнозърнест хляб', price: 1.9 },
{ id: 5, name: 'Прясно мляко', price: 1.6 },
{ id: 6, name: 'Кисело мляко', price: 0.9 },
{ id: 7, name: 'Сирене', price: 7.9 },
{ id: 8, name: 'Кашкавал', price: 9.5 },
{ id: 9, name: 'Масло', price: 3.4 },
{ id: 10, name: 'Яйца (10 бр.)', price: 2.8 },
];
export const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; align-items: center; gap: 8px; background: #f3efe6; padding: 8px 16px; }
main { padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span { flex: 1; }
button, input { font: inherit; }
Функцията visible() става computed: const visible = computed(() => PRODUCTS.filter(…));, а шаблонът подава :products="visible", без скоби. computed смята наново само когато query се смени и дотогава връща същия масив. add е обикновена функция: тя е същата при всяко рисуване, и Vue пази и стрелките в @add="…".
Проверката хваща и :products="[...visible]": копието е нов масив при всяко рисуване.
Пробвай
Намери грешката
visible е computed, а списъкът пак се рисува при всяка буква. Кой ред е виновен?Избери реда с грешката.
[...visible] прави нов масив при всяко рисуване на App, макар че visible е същият. Подай го направо: :products="visible".Пробвай
Попълни
Как скъпият списък остава същият между рисуванията?
const visible = (
() => PRODUCTS.filter((p) => p.name.includes(query.value)),
);computed: пуска функцията, когато се смени нещо, което тя чете (query.value). Иначе връща същия масив.Идеята
Дълъг списък: рисувай по-малко
Най-бързият елемент е този, който не е нарисуван. Човек вижда 10–20 продукта на екрана, а не 2000. Затова дългият списък рисува част:
- „Покажи още“: първите 20, после още 20 при клик. Помни се само колко са показани.
- Страници: „1, 2, 3 …“, често с номера в адреса (
/?page=2), за да се споделя. - Виртуален списък (windowing): рисува само редовете, които се виждат, и ги сменя при превъртане. Пише се трудно, затова се ползва библиотека: TanStack Virtual или vue-virtual-scroller.
Ключовете от урока за списъците помагат и тук: с key={p.id} (:key="p.id") добавените 20 продукта са нови елементи, а старите остават, без да се правят наново.
Идеята
Мързеливо зареждане
Не всяка част от страницата трябва веднага. Отзивите за продукта, картата с магазините, редакторът на профила: може да ги зареди едва когато човекът ги отвори. Така първото отваряне тегли по-малко код.
import { defineAsyncComponent } from 'vue';
const Reviews = defineAsyncComponent(() => import('./Reviews.vue'));import('./Reviews.vue') е функция, която зарежда файла и връща Promise. defineAsyncComponent прави от нея компонент, който се ползва като всеки друг: <Reviews v-if="open" />.
Конзола
Отвори конзолата: редът от reviews.js излиза едва след клика. Честно казано, в пясъчника всички файлове вече са в страницата, затова тук import() само пуска файла по-късно. В истинския проект Vite слага всеки такъв файл отделно и браузърът го тегли от сървъра при първия клик. Същото важи за страниците: във Vue Router component: () => import('./CartPage.vue') зарежда страницата при първия преход към нея.
По-точно, за любопитните
v-memo. За много дълъг v-for Vue има v-memo="[p.id === selected]": редът се рисува наново само ако стойностите в масива се сменят. Документацията го нарича микрооптимизация за редки случаи. Сгрешен масив значи пропуснато обновяване.
Състоянието надолу. Полето със своя draft може да е отделен компонент SearchBox, който изпраща search нагоре само при „Търси“. Тогава буквите сменят само SearchBox.
computed със стабилен резултат. От Vue 3.4 нататък computed, който връща същото примитивно значение (например true), не задейства зависимите от него. За обект може да върнеш предишния (computed((old) => …)), ако съдържанието е същото.
Режимът на разработка е по-бавен. Предупрежденията и проверките струват време. Мери бързината на събраната версия (vite build и vite preview), а не само при разработка.
Упражнение
Покажи още
Каталогът има 10 продукта и рисува всичките. Покажи първите 4, а под тях „Показани 4 от 10“ и бутон „Покажи още“, който добавя още 4. Когато всички са показани, бутонът изчезва. Проверката брои продуктите след всеки клик и гледа текста и бутона.
Конзола
Решение
App.vue<script setup>
import { ref, computed } from 'vue';
import { PRODUCTS, money } from './products.js';
const PAGE = 4;
const limit = ref(PAGE);
const shown = computed(() => PRODUCTS.slice(0, limit.value));
</script>
<template>
<main>
<ul>
<li v-for="p in shown" :key="p.id">
<span>{{ p.name }}, {{ money(p.price) }}</span>
</li>
</ul>
<p>{{ `Показани ${shown.length} от ${PRODUCTS.length}` }}</p>
<button v-if="limit < PRODUCTS.length" type="button" @click="limit += PAGE">Покажи още</button>
</main>
</template>
products.jsexport const PRODUCTS = [
{ id: 1, name: 'Бял хляб', price: 1.2 },
{ id: 2, name: 'Пълнозърнест хляб', price: 1.9 },
{ id: 5, name: 'Прясно мляко', price: 1.6 },
{ id: 6, name: 'Кисело мляко', price: 0.9 },
{ id: 7, name: 'Сирене', price: 7.9 },
{ id: 8, name: 'Кашкавал', price: 9.5 },
{ id: 9, name: 'Масло', price: 3.4 },
{ id: 10, name: 'Яйца (10 бр.)', price: 2.8 },
{ id: 11, name: 'Ябълки (1 кг)', price: 2.2 },
{ id: 12, name: 'Банани (1 кг)', price: 2.6 },
];
export const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; align-items: center; gap: 8px; background: #f3efe6; padding: 8px 16px; }
main { padding: 0 16px; }
ul { list-style: none; padding: 0; }
li { display: flex; gap: 8px; align-items: center; margin: 6px 0; }
li span { flex: 1; }
button, input { font: inherit; }
Пазиш само колко са показани: const limit = ref(4);. Показаните са изчислени: computed(() => PRODUCTS.slice(0, limit.value)). Бутонът е с v-if="limit < PRODUCTS.length", а кликът е limit += 4. Обикновена променлива let limit = 4 не е реактивна и шаблонът не се рисува наново: проверката го хваща.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Изчислено състояние · Вдигане на състоянието · Ефекти · Рутиране
- Задачи по темата: Цяло приложение