Vue 3 v-memo 完全指南:解锁高效渲染的黑科技
·
Vue 3 v-memo 完全指南:解锁高效渲染的黑科技
文章目录
1. 概述
v-memo 是 Vue 3.2 引入的指令,用于优化组件和 DOM 的渲染性能。它通过记忆模板子树来避免不必要的重新渲染。
2. 基本语法
<template>
<div v-memo="[dependency1, dependency2, ...]">
<!-- 子树内容 -->
</div>
</template>
3. 基本用法
3.1 简单示例
<template>
<div>
<button @click="count++">计数: {{ count }}</button>
<button @click="text = '新文本'">更新文本</button>
<!-- 只有当 count 发生变化时才重新渲染 -->
<div v-memo="[count]">
<h3>记忆化区域</h3>
<p>计数: {{ count }}</p>
<p>文本: {{ text }}</p>
<p>渲染时间: {{ new Date().toLocaleTimeString() }}</p>
</div>
<!-- 普通区域,任何数据变化都会重新渲染 -->
<div>
<h3>普通区域</h3>
<p>计数: {{ count }}</p>
<p>文本: {{ text }}</p>
<p>渲染时间: {{ new Date().toLocaleTimeString() }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const text = ref('初始文本')
</script>
3.2 多个依赖项
<template>
<div>
<button @click="user.name = '新名字'">更新名字</button>
<button @click="user.age++">更新年龄</button>
<button @click="user.email = 'new@email.com'">更新邮箱</button>
<!-- 只有当 name 或 age 变化时才重新渲染 -->
<div v-memo="[user.name, user.age]" class="memo-section">
<h3>用户信息 (v-memo)</h3>
<p>姓名: {{ user.name }}</p>
<p>年龄: {{ user.age }}</p>
<p>邮箱: {{ user.email }}</p>
<p>最后渲染: {{ new Date().toLocaleTimeString() }}</p>
</div>
<div class="normal-section">
<h3>用户信息 (普通)</h3>
<p>姓名: {{ user.name }}</p>
<p>年龄: {{ user.age }}</p>
<p>邮箱: {{ user.email }}</p>
<p>最后渲染: {{ new Date().toLocaleTimeString() }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
interface User {
name: string
age: number
email: string
}
const user = reactive<User>({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
})
</script>
<style scoped>
.memo-section {
border: 2px solid green;
padding: 10px;
margin: 10px 0;
}
.normal-section {
border: 2px solid blue;
padding: 10px;
margin: 10px 0;
}
</style>
4. 与 v-for 结合使用
4.1 大型列表优化
<template>
<div>
<div>
<button @click="addItem">添加项目</button>
<button @click="updateFirstItem">更新第一个项目</button>
<button @click="filterList">过滤列表</button>
<input v-model="searchText" placeholder="搜索..." />
</div>
<!-- 未优化的列表 -->
<div class="list-section">
<h3>未优化的列表 ({{ unoptimizedItems.length }} 项)</h3>
<div
v-for="item in unoptimizedItems"
:key="item.id"
class="list-item"
>
<span>{{ item.name }}</span>
<span>价格: ¥{{ item.price }}</span>
<span>库存: {{ item.stock }}</span>
<span>渲染: {{ new Date().toLocaleTimeString() }}</span>
</div>
</div>
<!-- 使用 v-memo 优化的列表 -->
<div class="list-section">
<h3>使用 v-memo 优化的列表 ({{ optimizedItems.length }} 项)</h3>
<div
v-for="item in optimizedItems"
v-memo="[item.id, item.name, item.price, item.stock]"
:key="item.id"
class="list-item optimized"
>
<span>{{ item.name }}</span>
<span>价格: ¥{{ item.price }}</span>
<span>库存: {{ item.stock }}</span>
<span>渲染: {{ new Date().toLocaleTimeString() }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Product {
id: number
name: string
price: number
stock: number
category: string
}
const items = ref<Product[]>([
{ id: 1, name: '商品A', price: 100, stock: 10, category: 'electronics' },
{ id: 2, name: '商品B', price: 200, stock: 5, category: 'clothing' },
{ id: 3, name: '商品C', price: 300, stock: 8, category: 'books' },
{ id: 4, name: '商品D', price: 400, stock: 3, category: 'electronics' },
{ id: 5, name: '商品E', price: 500, stock: 15, category: 'clothing' }
])
const searchText = ref('')
const unoptimizedItems = computed(() => {
return items.value.filter(item =>
item.name.toLowerCase().includes(searchText.value.toLowerCase())
)
})
const optimizedItems = computed(() => {
return items.value.filter(item =>
item.name.toLowerCase().includes(searchText.value.toLowerCase())
)
})
let nextId = 6
const addItem = () => {
items.value.push({
id: nextId++,
name: `商品${String.fromCharCode(64 + nextId)}`,
price: Math.floor(Math.random() * 500) + 100,
stock: Math.floor(Math.random() * 20) + 1,
category: ['electronics', 'clothing', 'books'][Math.floor(Math.random() * 3)]
})
}
const updateFirstItem = () => {
if (items.value.length > 0) {
items.value[0].price += 10
}
}
const filterList = () => {
items.value = items.value.filter(item => item.stock > 5)
}
</script>
<style scoped>
.list-section {
margin: 20px 0;
padding: 10px;
border: 1px solid #ccc;
}
.list-item {
padding: 8px;
margin: 4px 0;
border: 1px solid #ddd;
background: #f9f9f9;
}
.list-item.optimized {
background: #e8f5e8;
border-color: #4caf50;
}
</style>
4.2 动态依赖项
<template>
<div>
<button @click="sortBy = sortBy === 'name' ? 'price' : 'name'">
切换排序: {{ sortBy }}
</button>
<button @click="reverseOrder = !reverseOrder">
反转顺序: {{ reverseOrder ? '是' : '否' }}
</button>
<div
v-for="item in sortedItems"
v-memo="[item.id, item.name, item.price, sortBy, reverseOrder]"
:key="item.id"
class="item-card"
>
<h4>{{ item.name }}</h4>
<p>价格: ¥{{ item.price }}</p>
<p>排序字段: {{ sortBy }}</p>
<p>反转: {{ reverseOrder ? '是' : '否' }}</p>
<small>渲染时间: {{ new Date().toLocaleTimeString() }}</small>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Item {
id: number
name: string
price: number
}
const items = ref<Item[]>([
{ id: 1, name: '苹果', price: 5 },
{ id: 2, name: '香蕉', price: 3 },
{ id: 3, name: '橙子', price: 4 },
{ id: 4, name: '葡萄', price: 8 }
])
const sortBy = ref<'name' | 'price'>('name')
const reverseOrder = ref(false)
const sortedItems = computed(() => {
const sorted = [...items.value].sort((a, b) => {
if (a[sortBy.value] < b[sortBy.value]) return -1
if (a[sortBy.value] > b[sortBy.value]) return 1
return 0
})
return reverseOrder.value ? sorted.reverse() : sorted
})
</script>
<style scoped>
.item-card {
border: 1px solid #ddd;
padding: 10px;
margin: 5px 0;
background: #f0f8ff;
}
</style>
5. 与组件结合使用
5.1 优化子组件渲染
<!-- ChildComponent.vue -->
<template>
<div class="child-component" :class="{ highlighted }">
<h4>子组件 {{ id }}</h4>
<p>姓名: {{ user.name }}</p>
<p>年龄: {{ user.age }}</p>
<p>分数: {{ score }}</p>
<p>最后渲染: {{ new Date().toLocaleTimeString() }}</p>
<button @click="highlighted = !highlighted">切换高亮</button>
</div>
</template>
<script setup lang="ts">
interface Props {
id: number
user: {
name: string
age: number
}
score: number
}
defineProps<Props>()
const highlighted = ref(false)
</script>
<style scoped>
.child-component {
padding: 10px;
margin: 5px;
border: 1px solid #ccc;
transition: all 0.3s;
}
.highlighted {
background-color: #fffacd;
border-color: #ffeb3b;
}
</style>
<!-- ParentComponent.vue -->
<template>
<div>
<div>
<button @click="updateAllUsers">更新所有用户</button>
<button @click="updateFirstUser">更新第一个用户</button>
<button @click="updateScores">更新分数</button>
</div>
<h3>未优化的子组件列表</h3>
<ChildComponent
v-for="item in items"
:key="item.id"
:id="item.id"
:user="item.user"
:score="item.score"
/>
<h3>使用 v-memo 优化的子组件列表</h3>
<div
v-for="item in items"
v-memo="[item.user.name, item.user.age, item.score]"
:key="item.id"
>
<ChildComponent
:id="item.id"
:user="item.user"
:score="item.score"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
interface ListItem {
id: number
user: {
name: string
age: number
}
score: number
}
const items = ref<ListItem[]>([
{ id: 1, user: { name: '张三', age: 25 }, score: 85 },
{ id: 2, user: { name: '李四', age: 30 }, score: 92 },
{ id: 3, user: { name: '王五', age: 28 }, score: 78 },
{ id: 4, user: { name: '赵六', age: 35 }, score: 88 }
])
const updateAllUsers = () => {
items.value.forEach(item => {
item.user.age += 1
})
}
const updateFirstUser = () => {
if (items.value.length > 0) {
items.value[0].user.name = '更新后的名字'
}
}
const updateScores = () => {
items.value.forEach(item => {
item.score += Math.floor(Math.random() * 10) - 5
})
}
</script>
6. 高级用法和模式
6.1 条件记忆
<template>
<div>
<label>
<input type="checkbox" v-model="enableMemo" />
启用 v-memo 优化
</label>
<button @click="updateData">更新数据</button>
<button @click="addItem">添加项目</button>
<!-- 条件性使用 v-memo -->
<div
v-for="item in items"
:key="item.id"
v-memo="enableMemo ? [item.id, item.value] : []"
class="item"
>
<span>ID: {{ item.id }}</span>
<span>值: {{ item.value }}</span>
<span>时间: {{ new Date().toLocaleTimeString() }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
interface DataItem {
id: number
value: string
}
const items = ref<DataItem[]>([
{ id: 1, value: '值1' },
{ id: 2, value: '值2' },
{ id: 3, value: '值3' }
])
const enableMemo = ref(true)
const updateData = () => {
items.value.forEach(item => {
item.value = `更新值${item.id}-${Math.random().toString(36).substr(2, 5)}`
})
}
const addItem = () => {
const newId = Math.max(...items.value.map(i => i.id)) + 1
items.value.push({
id: newId,
value: `新值${newId}`
})
}
</script>
<style scoped>
.item {
padding: 8px;
margin: 4px;
border: 1px solid #ddd;
background: #f5f5f5;
}
</style>
6.2 复杂对象记忆
<template>
<div>
<button @click="updateUserProfile">更新用户资料</button>
<button @click="updateUserSettings">更新用户设置</button>
<button @click="updateBoth">同时更新</button>
<!-- 记忆整个用户对象 -->
<div v-memo="[user]" class="user-card">
<h3>用户信息</h3>
<p>姓名: {{ user.profile.name }}</p>
<p>年龄: {{ user.profile.age }}</p>
<p>主题: {{ user.settings.theme }}</p>
<p>语言: {{ user.settings.language }}</p>
<p>最后渲染: {{ new Date().toLocaleTimeString() }}</p>
</div>
<!-- 记忆特定嵌套属性 -->
<div v-memo="[user.profile, user.settings.theme]" class="user-card">
<h3>部分记忆</h3>
<p>姓名: {{ user.profile.name }}</p>
<p>年龄: {{ user.profile.age }}</p>
<p>主题: {{ user.settings.theme }}</p>
<p>语言: {{ user.settings.language }}</p>
<p>最后渲染: {{ new Date().toLocaleTimeString() }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
interface User {
profile: {
name: string
age: number
email: string
}
settings: {
theme: string
language: string
notifications: boolean
}
}
const user = reactive<User>({
profile: {
name: '张三',
age: 25,
email: 'zhangsan@example.com'
},
settings: {
theme: 'light',
language: 'zh-CN',
notifications: true
}
})
const updateUserProfile = () => {
user.profile.age += 1
}
const updateUserSettings = () => {
user.settings.theme = user.settings.theme === 'light' ? 'dark' : 'light'
}
const updateBoth = () => {
user.profile.name = `用户${Math.random().toString(36).substr(2, 5)}`
user.settings.language = user.settings.language === 'zh-CN' ? 'en-US' : 'zh-CN'
}
</script>
<style scoped>
.user-card {
border: 1px solid #ccc;
padding: 15px;
margin: 10px 0;
background: #f9f9f9;
}
</style>
7. 性能优化策略
7.1 基准测试示例
<template>
<div>
<div class="controls">
<button @click="startBenchmark">开始性能测试</button>
<button @click="resetTest">重置测试</button>
<span>测试结果: {{ benchmarkResult }}</span>
</div>
<div class="test-section">
<h3>大型列表渲染测试 ({{ items.length }} 项)</h3>
<div class="benchmark-row">
<div class="benchmark-col">
<h4>普通渲染</h4>
<div
v-for="item in items"
:key="item.id"
class="list-item"
>
{{ item.name }} - {{ item.value }}
</div>
</div>
<div class="benchmark-col">
<h4>v-memo 优化</h4>
<div
v-for="item in items"
v-memo="[item.id, item.name, item.value]"
:key="item.id"
class="list-item memoized"
>
{{ item.name }} - {{ item.value }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
interface TestItem {
id: number
name: string
value: number
}
const items = ref<TestItem[]>([])
const benchmarkResult = ref('')
const generateTestData = (count: number): TestItem[] => {
return Array.from({ length: count }, (_, i) => ({
id: i + 1,
name: `项目${i + 1}`,
value: Math.floor(Math.random() * 1000)
}))
}
const startBenchmark = () => {
// 模拟更新操作
const startTime = performance.now()
// 触发重新渲染
items.value = [...items.value]
const endTime = performance.now()
benchmarkResult.value = `渲染耗时: ${(endTime - startTime).toFixed(2)}ms`
}
const resetTest = () => {
items.value = generateTestData(1000)
benchmarkResult.value = ''
}
onMounted(() => {
resetTest()
})
</script>
<style scoped>
.controls {
margin-bottom: 20px;
padding: 10px;
background: #f0f0f0;
}
.test-section {
border: 1px solid #ccc;
padding: 15px;
}
.benchmark-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.benchmark-col {
border: 1px solid #ddd;
padding: 10px;
max-height: 400px;
overflow-y: auto;
}
.list-item {
padding: 4px;
margin: 2px 0;
border-bottom: 1px solid #eee;
}
.list-item.memoized {
background: #e8f5e8;
}
</style>
8. 注意事项和最佳实践
8.1 使用时机
<template>
<div>
<h3>v-memo 使用指南</h3>
<!-- 适合使用 v-memo 的情况 -->
<!-- 1. 大型列表 -->
<div v-for="item in largeList" v-memo="[item.id, item.name]" :key="item.id">
<!-- 复杂子组件 -->
</div>
<!-- 2. 频繁更新的组件 -->
<div v-memo="[user.id, user.importantProp]">
<!-- 用户信息显示 -->
</div>
<!-- 3. 昂贵的计算渲染 -->
<div v-memo="[dataSource]">
<!-- 复杂图表或可视化 -->
</div>
<!-- 不适合使用 v-memo 的情况 -->
<!-- 1. 简单静态内容 -->
<div>
<!-- 静态内容不需要 v-memo -->
<p>这是一个静态段落</p>
</div>
<!-- 2. 依赖项频繁变化 -->
<div v-memo="[frequentlyChangingValue]">
<!-- 如果依赖项频繁变化,v-memo 反而会增加开销 -->
</div>
</div>
</template>
8.2 内存考虑
<template>
<div>
<button @click="showLargeList = !showLargeList">
{{ showLargeList ? '隐藏' : '显示' }}大型列表
</button>
<!-- 动态管理记忆 -->
<div v-if="showLargeList">
<div
v-for="item in largeList"
v-memo="[item.id, item.name, item.value]"
:key="item.id"
class="large-item"
>
{{ item.name }} - {{ item.value }}
</div>
</div>
<!-- 空依赖数组表示不记忆 -->
<div v-memo="[]">
这个div永远不会被记忆,总是重新渲染
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
interface LargeItem {
id: number
name: string
value: number
}
const showLargeList = ref(false)
const largeList = computed(() => {
if (!showLargeList.value) return []
return Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `项目${i}`,
value: Math.random() * 1000
}))
})
</script>
9. 总结
v-memo 的优势:
- 性能提升:减少不必要的重新渲染
- 精确控制:可以指定具体的依赖项
- 内存效率:相比其他优化方式更轻量
- 开发体验:简单的声明式语法
适用场景:
- 大型列表渲染
- 频繁更新的复杂组件
- 昂贵的子组件树
- 需要精确控制渲染的场合
注意事项:
- 不要过度使用,只在必要时使用
- 注意内存占用,特别是大型列表
- 确保依赖项选择合理
- 在性能关键的场景进行测试
更多推荐
所有评论(0)