Lặp qua một khoảng số với v-for trong Vue 3
v-for thường được dùng để render một danh sách từ Array:
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>Nhưng Vue cũng cho phép v-for nhận trực tiếp một số nguyên.
Ví dụ:
<li v-for="n in 5" :key="n">
Item #{{ n }}
</li>Khi sử dụng một số nguyên với v-for, Vue sẽ lặp theo range từ 1 đến số đó.
Kết quả là:
Item #1
Item #2
Item #3
Item #4
Item #5Đây là một tính năng đơn giản nhưng hữu ích cho những trường hợp mà điều bạn cần biểu diễn là số lượng phần tử, thay vì một tập dữ liệu cụ thể.
1. Sử dụng v-for với một số
Cú pháp cơ bản:
<template>
<ul>
<li
v-for="n in 5"
:key="n"
>
Item #{{ n }}
</li>
</ul>
</template>Biến n lần lượt nhận các giá trị:
1
2
3
4
5Điểm quan trọng cần nhớ là range v-for bắt đầu từ 1, không phải 0.
Vì vậy:
v-for="n in 5"tương đương về mặt kết quả lặp với:
1, 2, 3, 4, 5chứ không phải:
0, 1, 2, 3, 42. Sử dụng một giá trị reactive
Số lượng phần tử không nhất thiết phải là một literal.
Bạn có thể sử dụng một giá trị reactive:
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(5)
</script>
<template>
<ul>
<li
v-for="n in count"
:key="n"
>
Item #{{ n }}
</li>
</ul>
</template>Nếu:
count.value = 3thì Vue sẽ render:
Item #1
Item #2
Item #3Điều này đặc biệt hữu ích khi số lượng phần tử phụ thuộc vào state của component.
3. Thay đổi điểm bắt đầu
Range syntax của v-for luôn bắt đầu từ 1.
Nếu muốn hiển thị một range bắt đầu từ một giá trị khác, bạn có thể biến đổi giá trị n.
Ví dụ:
<script setup lang="ts">
const offset = 4
</script>
<template>
<ul>
<li
v-for="n in 5"
:key="n"
>
Item #{{ n + offset }}
</li>
</ul>
</template>Kết quả:
Item #5
Item #6
Item #7
Item #8
Item #9Ở đây:
n = 1 → 1 + 4 = 5
n = 2 → 2 + 4 = 6
n = 3 → 3 + 4 = 7
n = 4 → 4 + 4 = 8
n = 5 → 5 + 4 = 9Đây là một cách đơn giản để tạo offset khi logic chỉ có một phép tính nhỏ.
4. Khi cần một range phức tạp hơn
Nếu range cần nhiều quy tắc, không nên đưa toàn bộ logic vào template.
Ví dụ, nếu muốn tạo một range bắt đầu từ 5:
<script setup lang="ts">
import { computed, ref } from 'vue'
const start = ref(5)
const count = ref(5)
const numbers = computed(() => {
return Array.from(
{ length: count.value },
(_, index) => start.value + index
)
})
</script>
<template>
<ul>
<li
v-for="number in numbers"
:key="number"
>
{{ number }}
</li>
</ul>
</template>Kết quả:
5
6
7
8
9Ở đây, numbers là một giá trị được suy ra từ start và count.
Việc tạo Array trước sẽ trở nên hợp lý hơn khi range có logic phức tạp hoặc cần được sử dụng ở nhiều nơi.
5. Tạo range với bước nhảy khác 1
Range syntax:
v-for="n in count"chỉ trực tiếp cung cấp các giá trị tăng từng 1.
Nếu cần một bước nhảy khác, chẳng hạn:
0
2
4
6
8có thể tạo Array bằng JavaScript:
<script setup lang="ts">
import { computed } from 'vue'
const numbers = computed(() => {
return Array.from(
{ length: 5 },
(_, index) => index * 2
)
})
</script>
<template>
<ul>
<li
v-for="number in numbers"
:key="number"
>
{{ number }}
</li>
</ul>
</template>Hoặc tạo range bắt đầu từ 10 với bước nhảy 5:
<script setup lang="ts">
import { computed } from 'vue'
const numbers = computed(() => {
const start = 10
const count = 5
const step = 5
return Array.from(
{ length: count },
(_, index) => start + index * step
)
})
</script>
<template>
<ul>
<li
v-for="number in numbers"
:key="number"
>
{{ number }}
</li>
</ul>
</template>Kết quả:
10
15
20
25
30Trong trường hợp này, Array thể hiện rõ hơn ý định của dữ liệu so với việc cố đưa phép tính vào v-for.
6. Range v-for và Array là hai trường hợp khác nhau
Một nguyên tắc đơn giản:
Range biểu diễn số lượng. Array biểu diễn dữ liệu.
Ví dụ, nếu bạn cần tạo năm placeholder:
<li
v-for="n in 5"
:key="n"
>
Loading...
</li>range là lựa chọn phù hợp.
Nhưng nếu mỗi phần tử có dữ liệu riêng:
<script setup lang="ts">
const products = [
{
id: 101,
name: 'Keyboard'
},
{
id: 102,
name: 'Mouse'
},
{
id: 103,
name: 'Monitor'
}
]
</script>
<template>
<ul>
<li
v-for="product in products"
:key="product.id"
>
{{ product.name }}
</li>
</ul>
</template>hãy lặp trực tiếp qua Array.
Không cần biến nó thành:
v-for="n in 3"rồi truy cập:
products[n - 1]Cách thứ hai làm mất đi ý nghĩa của dữ liệu và khiến template khó đọc hơn.
7. Render một phần của Array
Một cách có thể làm nhưng không phải lúc nào cũng rõ ràng là dùng range để lấy một số phần tử đầu tiên:
<script setup lang="ts">
const fruits = [
'Apples',
'Oranges',
'Bananas',
'Pineapple'
]
</script>
<template>
<ul>
<li
v-for="n in 3"
:key="n"
>
{{ fruits[n - 1] }}
</li>
</ul>
</template>Kết quả:
Apples
Oranges
BananasCần trừ 1 vì:
v-for range → bắt đầu từ 1
Array index → bắt đầu từ 0Do đó:
n = 1 → fruits[0]
n = 2 → fruits[1]
n = 3 → fruits[2]Cách này hợp lệ, nhưng nếu mục đích thực sự là tạo một danh sách con từ Array, computed() thường diễn đạt ý định rõ ràng hơn.
8. Sử dụng computed cho danh sách dẫn xuất
Ví dụ:
<script setup lang="ts">
import { computed, ref } from 'vue'
const fruits = ref([
'Apples',
'Oranges',
'Bananas',
'Pineapple'
])
const visibleFruits = computed(() => {
return fruits.value.slice(0, 3)
})
</script>
<template>
<ul>
<li
v-for="fruit in visibleFruits"
:key="fruit"
>
{{ fruit }}
</li>
</ul>
</template>Ở đây:
fruits
↓
slice(0, 3)
↓
visibleFruits
↓
v-forTemplate chỉ cần render visibleFruits.
Điều này đặc biệt hữu ích khi danh sách cần được:
- lọc;
- sắp xếp;
- giới hạn;
- biến đổi;
- kết hợp nhiều reactive dependencies.
9. computed không phải là yêu cầu của mọi v-for
Không nên hiểu rằng cứ có v-for thì phải tạo computed.
Ví dụ này hoàn toàn ổn:
<template>
<ul>
<li
v-for="n in 100"
:key="n"
>
{{ n }}
</li>
</ul>
</template>Không cần chuyển thành:
const numbers = computed(() => {
return Array.from(
{ length: 100 },
(_, index) => index + 1
)
})chỉ để tránh sử dụng range syntax.
v-for="n in 100" đã diễn đạt chính xác ý định:
Render 100 lần.
Ngược lại, nếu bạn có dữ liệu cần lọc:
const visibleProducts = computed(() => {
return products.value
.filter(product => product.inStock)
.slice(0, 10)
})thì computed() có ý nghĩa vì visibleProducts là derived state.
10. Hiểu đúng về caching của computed
Computed được cache dựa trên các reactive dependencies.
Ví dụ:
const visibleFruits = computed(() => {
return fruits.value.slice(0, 3)
})Khi fruits.value không thay đổi, Vue có thể sử dụng lại giá trị computed đã được tính.
Khi dependency thay đổi, computed sẽ được đánh giá lại khi cần.
Vì vậy, không nên diễn đạt đơn giản rằng:
“Computed chỉ chạy một lần.”
Cách chính xác hơn là:
Computed được cache dựa trên các reactive dependencies.
Computed nên được sử dụng để biểu diễn derived state, không phải như một thủ thuật tối ưu hiệu năng cho mọi vòng lặp.
11. Pagination là trường hợp phù hợp cho range
Pagination là một ví dụ điển hình.
<script setup lang="ts">
import { computed, ref } from 'vue'
const currentPage = ref(1)
const totalItems = ref(47)
const pageSize = 10
const totalPages = computed(() => {
return Math.ceil(totalItems.value / pageSize)
})
</script>
<template>
<nav aria-label="Pagination">
<button
v-for="page in totalPages"
:key="page"
type="button"
:aria-current="page === currentPage ? 'page' : undefined"
@click="currentPage = page"
>
{{ page }}
</button>
</nav>
</template>Ở đây:
v-for="page in totalPages"có nghĩa:
Render một phần tử cho mỗi trang.
Không cần tạo một Array chỉ để chứa:
1, 2, 3, 4, 5totalPages là số lượng trang, vì vậy range v-for phù hợp.
12. Skeleton loading
Range cũng rất phù hợp với skeleton UI.
<script setup lang="ts">
const skeletonCount = 6
</script>
<template>
<ul aria-label="Loading">
<li
v-for="n in skeletonCount"
:key="n"
class="skeleton"
/>
</ul>
</template>Ở đây n chỉ được sử dụng để tạo ra sáu phần tử.
Không có dữ liệu riêng biệt cho từng skeleton, vì vậy không cần tạo Array object.
13. Rating
Một hệ thống rating cũng có thể sử dụng range:
<script setup lang="ts">
const maxRating = 5
const rating = 4
</script>
<template>
<div aria-label="Rating">
<span
v-for="star in maxRating"
:key="star"
>
{{ star <= rating ? '★' : '☆' }}
</span>
</div>
</template>Kết quả:
★★★★☆Ở đây maxRating xác định số lượng ngôi sao, còn rating xác định trạng thái của từng ngôi sao.
14. Step indicator
Range cũng phù hợp với các bước có số lượng cố định:
<script setup lang="ts">
const totalSteps = 4
const currentStep = 2
</script>
<template>
<ol>
<li
v-for="step in totalSteps"
:key="step"
:aria-current="step === currentStep ? 'step' : undefined"
>
Step {{ step }}
</li>
</ol>
</template>Ở đây step lần lượt là:
1
2
3
415. Luôn cân nhắc key
Khi render danh sách bằng v-for, nên cung cấp key ổn định khi danh sách có component hoặc DOM state cần được duy trì.
Ví dụ với range:
<li
v-for="n in 5"
:key="n"
>
Item #{{ n }}
</li>Với dữ liệu:
<li
v-for="product in products"
:key="product.id"
>
{{ product.name }}
</li>key giúp Vue theo dõi identity của từng node khi danh sách được cập nhật.
Vue yêu cầu key nên là một primitive như string hoặc number, không phải object.
Với các nội dung DOM đơn giản, không có state cần duy trì, key không phải lúc nào cũng bắt buộc. Tuy nhiên, thêm key trong những trường hợp phù hợp thường giúp intent rõ ràng hơn.
16. Tránh lọc Array trực tiếp bằng v-if + v-for
Không nên viết:
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id"
>
{{ user.name }}
</li>Trong Vue 3, v-if trên cùng node với v-for có precedence cao hơn v-for.
Điều này có nghĩa biến user của v-for không có sẵn cho biểu thức v-if trên cùng node.
Thay vào đó, hãy tạo danh sách đã lọc:
<script setup lang="ts">
import { computed, ref } from 'vue'
interface User {
id: number
name: string
isActive: boolean
}
const users = ref<User[]>([
{
id: 1,
name: 'Alice',
isActive: true
},
{
id: 2,
name: 'Bob',
isActive: false
},
{
id: 3,
name: 'Carol',
isActive: true
}
])
const activeUsers = computed(() => {
return users.value.filter(user => user.isActive)
})
</script>
<template>
<ul>
<li
v-for="user in activeUsers"
:key="user.id"
>
{{ user.name }}
</li>
</ul>
</template>Cách này tách rõ hai nhiệm vụ:
activeUsers → xác định dữ liệu cần render
v-for → render dữ liệu đóĐây cũng là pattern được Vue documentation khuyến nghị cho trường hợp lọc danh sách.
17. Khi nào method phù hợp hơn computed?
Computed phù hợp khi bạn có một derived value từ reactive state.
Nhưng trong nested v-for, đôi khi method phù hợp hơn.
Ví dụ:
<script setup lang="ts">
function even(numbers: number[]) {
return numbers.filter(number => number % 2 === 0)
}
const sets = [
[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10]
]
</script>
<template>
<ul
v-for="numbers in sets"
:key="numbers[0]"
>
<li
v-for="number in even(numbers)"
:key="number"
>
{{ number }}
</li>
</ul>
</template>Ở đây mỗi iteration truyền một Array khác nhau vào even().
Một computed duy nhất không phải lúc nào cũng là abstraction tự nhiên cho trường hợp này.
18. Đừng đưa quá nhiều logic vào template
Một expression đơn giản như:
<li v-for="n in 5">
Item #{{ n + 4 }}
</li>không có vấn đề gì.
Nhưng nếu template bắt đầu chứa những phép tính phức tạp, nên chuyển chúng sang JavaScript.
Thay vì:
<li
v-for="item in getItems(items, filter, sort, offset)"
>
{{ item.name }}
</li>có thể tạo derived state:
<script setup lang="ts">
import { computed } from 'vue'
const visibleItems = computed(() => {
// Complex data transformation
return []
})
</script>
<template>
<ul>
<li
v-for="item in visibleItems"
:key="item.id"
>
{{ item.name }}
</li>
</ul>
</template>Template nên tập trung vào việc mô tả UI.
Logic tạo dữ liệu hiển thị nên được đặt ở nơi phù hợp trong phần JavaScript của component.
19. Range v-for không tạo ra một Array JavaScript
Một điểm dễ nhầm:
v-for="n in 5"không có nghĩa là Vue tạo ra một Array JavaScript [1, 2, 3, 4, 5] mà bạn có thể sử dụng ở mọi nơi.
Đây là syntax của directive v-for để lặp template theo một integer range.
Nếu thực sự cần một Array để xử lý trong JavaScript, hãy tạo Array:
const numbers = Array.from(
{ length: 5 },
(_, index) => index + 1
)Sau đó có thể render:
<li
v-for="number in numbers"
:key="number"
>
{{ number }}
</li>Hai cách có thể tạo ra UI giống nhau nhưng có ý nghĩa khác nhau:
v-for="n in 5"
→ Tôi cần render 5 lần.
v-for="number in numbers"
→ Tôi cần render dữ liệu trong Array.20. Khi danh sách lớn, tối ưu đúng vấn đề
Không nên mặc định xem:
v-for="n in 100"là một vấn đề hiệu năng.
Nếu component phải render hàng nghìn hoặc hàng chục nghìn phần tử, vấn đề thực tế thường liên quan đến số lượng DOM node và chi phí cập nhật UI.
Trong những trường hợp phù hợp, có thể cân nhắc:
- pagination;
- lazy rendering;
- list virtualization;
- giảm số lượng DOM node;
v-memocho các subtree lớn hoặcv-forlist phù hợp;- profiling trước khi tối ưu.
v-memo có thể giúp bỏ qua việc cập nhật các subtree lớn trong những trường hợp phù hợp, nhưng không phải là thứ cần thêm vào mọi v-for.
Điều quan trọng là xác định bottleneck trước khi tối ưu.
21. Một ví dụ hoàn chỉnh
Ví dụ dưới đây kết hợp range, reactive state và computed:
<script setup lang="ts">
import { computed, ref } from 'vue'
const currentPage = ref(1)
const totalItems = ref(47)
const pageSize = 10
const totalPages = computed(() => {
return Math.ceil(totalItems.value / pageSize)
})
</script>
<template>
<nav aria-label="Pagination">
<button
v-for="page in totalPages"
:key="page"
type="button"
:aria-current="page === currentPage ? 'page' : undefined"
@click="currentPage = page"
>
{{ page }}
</button>
</nav>
</template>Ở đây mỗi phần có một trách nhiệm rõ ràng:
totalItems
↓
pageSize
↓
totalPages
↓
v-for
↓
pagination buttonscomputed() tạo ra số trang cần thiết.
v-for render từng trang.
key xác định identity của từng button.
Không cần tạo một Array chỉ để chứa các số trang.
22. Checklist
Khi sử dụng range v-for, hãy tự hỏi:
- Tôi đang lặp qua số lượng hay dữ liệu?
- Nếu là số lượng,
v-for="n in count"có diễn đạt đúng ý định không? - Tôi có nhớ rằng range bắt đầu từ
1không? - Nếu cần offset, logic có đủ đơn giản để đặt trong template không?
- Nếu cần bước nhảy khác
1, có nên tạo Array bằngArray.from()không? - Nếu đang filter hoặc sort một Array, có nên dùng
computed()không? - Tôi có cần
:keyổn định cho các phần tử được render không? - Tôi có đang dùng
v-ifvàv-fortrên cùng một element không? - Template có đang chứa quá nhiều logic không?
- Nếu danh sách lớn, tôi đã xác định bottleneck trước khi tối ưu chưa?
Kết luận
Vue 3 vẫn hỗ trợ range v-for:
v-for="n in count"Khi biểu thức là một số nguyên, Vue sẽ lặp template từ 1 đến giá trị đó.
Đây là lựa chọn tự nhiên khi số lượng phần tử là thứ bạn muốn biểu diễn:
- pagination;
- rating;
- skeleton loading;
- step indicators;
- các phần tử được đánh số.
Nếu cần một offset đơn giản, có thể biến đổi n:
<li v-for="n in 5" :key="n">
Item #{{ n + 4 }}
</li>Nếu cần range phức tạp hơn, hãy tạo dữ liệu trong JavaScript.
Nếu đang render dữ liệu thực tế, hãy lặp trực tiếp qua Array:
<li
v-for="item in items"
:key="item.id"
>
{{ item.name }}
</li>Nếu cần render một phiên bản được lọc, sắp xếp hoặc biến đổi của reactive data, hãy cân nhắc computed():
const visibleItems = computed(() => {
return items.value
.filter(item => item.visible)
.slice(0, 10)
})Tư duy quan trọng nhất là:
Bạn đang lặp qua một số lượng, hay đang lặp qua một tập dữ liệu?
Nếu là số lượng, range v-for là công cụ phù hợp.
Nếu là dữ liệu, hãy lặp trực tiếp qua Array.
Nếu là dữ liệu được suy ra từ state khác, hãy cân nhắc computed().
Đó là cách sử dụng range v-for phù hợp với Vue 3 hiện đại.