Note 5
Vue3 通用优化
以下技巧能同时改善页面加载和更新性能。
大型虚拟列表
所有的前端应用中最常见的性能问题就是渲染大型列表。无论一个框架性能有多好,渲染成千上万个列表项都会变得很慢,因为浏览器需要处理大量的 DOM 节点。
但是,我们并不需要立刻渲染出全部的列表。在大多数场景中,用户的屏幕尺寸只会展示这个巨大列表中的一小部分。我们可以通过列表虚拟化来提升性能,这项技术使我们只需要渲染用户视口中能看到的部分。
要实现列表虚拟化并不简单,幸运的是,你可以直接使用现有的社区库:
虚拟滚动的本质是通过 按需渲染 和 DOM 复用 优化性能,核心在于动态计算可视区域范围并精准更新内容。实现时需权衡固定行高与动态行高的复杂度,并结合实际场景选择合适的优化策略。
表格虚拟滚动(Virtual Scrolling)是一种优化大量数据渲染性能的技术,其核心思想是 仅渲染可视区域内的内容,而非完整渲染所有数据。
以下是其实现原理和关键步骤:
- 可视区域渲染:只渲染用户当前可见的行(或列),动态替换不可见区域的内容。
DOM复用:通过占位元素或动态更新DOM节点内容,保持有限的DOM数量。- 滚动模拟:通过占位元素或计算偏移量,模拟完整数据集的滚动条行为。
减少大型不可变数据的响应性开销
Vue 的响应性系统默认是深度的。虽然这让状态管理变得更直观,但在数据量巨大时,深度响应性也会导致不小的性能负担,因为每个属性访问都将触发代理的依赖追踪。好在这种性能负担通常只有在处理超大型数组或层级很深的对象时,例如一次渲染需要访问 100,000+ 个属性时,才会变得比较明显。因此,它只会影响少数特定的场景。
Vue 确实也为此提供了一种解决方案,通过使用 shallowRef() 和 shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理。这使得对深层级属性的访问变得更快,但代价是,我们现在必须将所有深层级对象视为不可变的,并且只能通过替换整个根状态来触发更新:
const shallowArray = shallowRef([
/* 巨大的列表,里面包含深层的对象 */
])
// 这不会触发更新...
shallowArray.value.push(newObject)
// 这才会触发更新
shallowArray.value = [...shallowArray.value, newObject]
// 这不会触发更新...
shallowArray.value[0].foo = 1
// 这才会触发更新
shallowArray.value = [
{
...shallowArray.value[0],
foo: 1
},
...shallowArray.value.slice(1)
]reactive() 的局限性
reactive() API 有一些局限性:
有限的值类型:它只能用于对象类型 (对象、数组和如
Map、Set这样的集合类型)。它不能持有如string、number或boolean这样的原始类型。不能替换整个对象:由于
Vue的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:jslet state = reactive({ count: 0 }) // 上面的 ({ count: 0 }) 引用将不再被追踪 // (响应性连接已丢失!) state = reactive({ count: 1 })对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
jsconst state = reactive({ count: 0 }) // 当解构时,count 已经与 state.count 断开连接 let { count } = state // 不会影响原始的 state count++ // 该函数接收到的是一个普通的数字 // 并且无法追踪 state.count 的变化 // 我们必须传入整个对象以保持响应性 callSomeFunction(state.count)
由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API。
ref & reactive
核心关系
ref 和 reactive 都是 Vue3 的响应式 API,用于创建响应式数据,但它们在实现方式和适用场景上有所不同。
主要区别
| 特性 | ref | reactive |
|---|---|---|
| 数据类型 | 任意类型(基本类型、对象、数组) | 仅限对象类型 |
| 访问方式 | 需要通过 .value 访问 | 直接访问属性 |
| 模板中使用 | 自动解包(无需 .value) | 直接使用 |
| 重新赋值 | 可以重新赋值整个对象 | 不能重新赋值整个响应式对象 |
代码示例
<template>
<div>
<!-- ref 在模板中自动解包 -->
<p>Count: {{ count }}</p>
<p>Name: {{ user.name }}</p>
<p>Age: {{ user.age }}</p>
<button @click="increment">Increment</button>
<button @click="updateUser">Update User</button>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
// ref 示例 - 可以处理基本类型
const count = ref(0)
// reactive 示例 - 只能处理对象
const user = reactive({
name: 'Alice',
age: 25
})
// ref 也可以处理对象(内部使用 reactive)
const userRef = ref({
name: 'Bob',
age: 30
})
const increment = () => {
// ref 需要 .value
count.value++
// reactive 直接访问
user.age++
// ref 对象也需要 .value
userRef.value.age++
}
const updateUser = () => {
// reactive 可以修改属性,但不能重新赋值整个对象
user.name = 'Carol'
// ref 可以重新赋值整个对象
userRef.value = { name: 'David', age: 35 }
}
</script>底层关系
实际上,ref 在内部使用了 reactive:
// 简化的 ref 实现原理
function ref(value) {
return {
get value() {
// 跟踪依赖
track()
// 如果是对象,用 reactive 包装
return isObject(value) ? reactive(value) : value
},
set value(newVal) {
value = newVal
// 触发更新
trigger()
}
}
}转换关系
两者可以相互转换:
import { ref, reactive, toRef, toRefs } from 'vue'
// reactive 转 ref
const state = reactive({ x: 1, y: 2 })
const xRef = toRef(state, 'x') // 创建单个 ref
const refs = toRefs(state) // 创建所有属性的 ref
// ref 转 reactive(直接使用 .value)
const countRef = ref(10)
const reactiveObj = reactive({ count: countRef.value })使用场景建议
使用 ref:
- 基本类型数据
- 需要重新赋值的对象
- 模板中需要直接使用的数据
使用 reactive:
- 复杂的对象结构
- 不需要重新赋值的对象
- 组合式函数中返回状态对象
总结
ref 和 reactive 都是 Vue3 响应式系统的核心,ref 更通用且可以处理所有数据类型,而 reactive 专门用于对象类型。
Vuex 和 Pinia 的区别
Vuex 和 Pinia 都是 Vue.js 的状态管理库,但它们有一些重要区别:
1. 设计理念
- Vuex: 更严格、更结构化的状态管理模式
- Pinia: 更轻量、更灵活的
Composition API风格
2. API 设计
Vuex 示例
// store.js
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
},
getters: {
doubleCount: state => state.count * 2
}
})Pinia 示例
// stores/counter.js
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
async incrementAsync() {
setTimeout(() => {
this.increment()
}, 1000)
}
}
})3. 主要区别
| 特性 | Vuex | Pinia |
|---|---|---|
| Vue 版本 | Vue 2 & 3 | 专为 Vue 3 设计 |
| Mutation | 必须使用 mutation 修改 state | 可以直接修改或使用 action |
| TypeScript 支持 | 需要额外配置 | 原生支持,类型推断更好 |
| 模块系统 | 命名空间模块 | 多个独立的(扁平化) store |
| 代码组织 | 相对复杂 | 更简洁直观 |
| 体积 | 较大 | 更轻量 (约 1KB) |
| Composition API | 需要额外适配 | 原生支持 |
| DevTools | 支持 | 支持 |
4. 使用方式对比
Vuex 在组件中使用
// Options API
computed: {
...mapState(['count']),
...mapGetters(['doubleCount'])
},
methods: {
...mapActions(['incrementAsync'])
}
// Composition API
import { useStore } from 'vuex'
setup() {
const store = useStore()
return {
count: computed(() => store.state.count),
increment: () => store.dispatch('incrementAsync')
}
}Pinia 在组件中使用
import { useCounterStore } from '@/stores/counter'
setup() {
const counter = useCounterStore()
return {
count: computed(() => counter.count),
doubleCount: computed(() => counter.doubleCount),
increment: () => counter.incrementAsync()
}
}5. 推荐使用
- 新项目: 推荐使用
Pinia - Vue 3 项目: 强烈推荐
Pinia - 现有 Vuex 项目: 可以继续使用,或逐步迁移到
Pinia
Pinia 现在是 Vue 官方推荐的状态管理库,它提供了更好的开发体验和更简洁的 API。