Skip to content

Note 5

Vue3 通用优化

以下技巧能同时改善页面加载和更新性能。

大型虚拟列表


所有的前端应用中最常见的性能问题就是渲染大型列表。无论一个框架性能有多好,渲染成千上万个列表项都会变得很慢,因为浏览器需要处理大量的 DOM 节点。

但是,我们并不需要立刻渲染出全部的列表。在大多数场景中,用户的屏幕尺寸只会展示这个巨大列表中的一小部分。我们可以通过列表虚拟化来提升性能,这项技术使我们只需要渲染用户视口中能看到的部分。

要实现列表虚拟化并不简单,幸运的是,你可以直接使用现有的社区库:

虚拟滚动的本质是通过 按需渲染DOM 复用 优化性能,核心在于动态计算可视区域范围并精准更新内容。实现时需权衡固定行高与动态行高的复杂度,并结合实际场景选择合适的优化策略。

表格虚拟滚动(Virtual Scrolling)是一种优化大量数据渲染性能的技术,其核心思想是 仅渲染可视区域内的内容,而非完整渲染所有数据

以下是其实现原理和关键步骤:

  • 可视区域渲染:只渲染用户当前可见的行(或列),动态替换不可见区域的内容。
  • DOM 复用:通过占位元素或动态更新 DOM 节点内容,保持有限的 DOM 数量。
  • 滚动模拟:通过占位元素或计算偏移量,模拟完整数据集的滚动条行为。

减少大型不可变数据的响应性开销


Vue 的响应性系统默认是深度的。虽然这让状态管理变得更直观,但在数据量巨大时,深度响应性也会导致不小的性能负担,因为每个属性访问都将触发代理的依赖追踪。好在这种性能负担通常只有在处理超大型数组或层级很深的对象时,例如一次渲染需要访问 100,000+ 个属性时,才会变得比较明显。因此,它只会影响少数特定的场景。

Vue 确实也为此提供了一种解决方案,通过使用 shallowRef()shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理。这使得对深层级属性的访问变得更快,但代价是,我们现在必须将所有深层级对象视为不可变的,并且只能通过替换整个根状态来触发更新:

js
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 有一些局限性:

  1. 有限的值类型:它只能用于对象类型 (对象、数组和如 MapSet 这样的集合类型)。它不能持有如 stringnumberboolean 这样的原始类型

  2. 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:

    js
    let state = reactive({ count: 0 })
    
    // 上面的 ({ count: 0 }) 引用将不再被追踪
    // (响应性连接已丢失!)
    state = reactive({ count: 1 })
  3. 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:

    js
    const state = reactive({ count: 0 })
    
    // 当解构时,count 已经与 state.count 断开连接
    let { count } = state
    // 不会影响原始的 state
    count++
    
    // 该函数接收到的是一个普通的数字
    // 并且无法追踪 state.count 的变化
    // 我们必须传入整个对象以保持响应性
    callSomeFunction(state.count)

由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API。

ref & reactive

核心关系


refreactive 都是 Vue3 的响应式 API,用于创建响应式数据,但它们在实现方式和适用场景上有所不同。

主要区别

特性refreactive
数据类型任意类型(基本类型、对象、数组)仅限对象类型
访问方式需要通过 .value 访问直接访问属性
模板中使用自动解包(无需 .value直接使用
重新赋值可以重新赋值整个对象不能重新赋值整个响应式对象

代码示例

vue
<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

javascript
// 简化的 ref 实现原理
function ref(value) {
  return {
    get value() {
      // 跟踪依赖
      track()
      // 如果是对象,用 reactive 包装
      return isObject(value) ? reactive(value) : value
    },
    set value(newVal) {
      value = newVal
      // 触发更新
      trigger()
    }
  }
}

转换关系


两者可以相互转换:

js
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

    • 复杂的对象结构
    • 不需要重新赋值的对象
    • 组合式函数中返回状态对象

总结


refreactive 都是 Vue3 响应式系统的核心,ref 更通用且可以处理所有数据类型,而 reactive 专门用于对象类型。

Vuex 和 Pinia 的区别

VuexPinia 都是 Vue.js 的状态管理库,但它们有一些重要区别:

1. 设计理念

  • Vuex: 更严格、更结构化的状态管理模式
  • Pinia: 更轻量、更灵活的 Composition API 风格

2. API 设计

Vuex 示例

js
// 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 示例

js
// 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. 主要区别

特性VuexPinia
Vue 版本Vue 2 & 3专为 Vue 3 设计
Mutation必须使用 mutation 修改 state可以直接修改或使用 action
TypeScript 支持需要额外配置原生支持,类型推断更好
模块系统命名空间模块多个独立的(扁平化) store
代码组织相对复杂更简洁直观
体积较大更轻量 (约 1KB)
Composition API需要额外适配原生支持
DevTools支持支持

4. 使用方式对比

Vuex 在组件中使用

js
// 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 在组件中使用

js
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。

Released under the MIT License.