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 的优势:

  • 性能提升:减少不必要的重新渲染
  • 精确控制:可以指定具体的依赖项
  • 内存效率:相比其他优化方式更轻量
  • 开发体验:简单的声明式语法

适用场景:

  1. 大型列表渲染
  2. 频繁更新的复杂组件
  3. 昂贵的子组件树
  4. 需要精确控制渲染的场合

注意事项:

  1. 不要过度使用,只在必要时使用
  2. 注意内存占用,特别是大型列表
  3. 确保依赖项选择合理
  4. 在性能关键的场景进行测试
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐