Vue3 响应式系统 10 个常见误区(90% 的文章其实讲错了)

317 阅读3分钟

一句话总结:Vue3 的 Proxy 响应式系统已经解决了 Vue2 大部分“坑”,真正需要理解的是 依赖收集、Proxy identity、ref/reactive 边界和性能控制


目录

  1. Vue3 响应式到底解决了什么
  2. Vue2 的坑在 Vue3 已经不存在
  3. Vue3 响应式核心原理(源码级理解)
  4. Vue3 真正需要注意的 10 个误区
  5. 大规模数据的性能优化策略
  6. 最佳实践总结

一、Vue3 响应式到底解决了什么

在 Vue2 中,响应式是基于

Object.defineProperty

实现的。

这带来几个经典问题:

Vue2限制原因
数组索引修改不更新defineProperty 无法拦截
对象新增属性无法监听
深层对象性能差递归 defineProperty
需要 Vue.set手动触发

Vue3 使用:

ES6 Proxy

拦截:

get
set
deleteProperty
has
ownKeys

因此 这些限制已经全部消失


二、Vue2 的坑在 Vue3 已经不存在

1 数组索引修改

Vue2:

list[0] = 10 // 不更新

Vue3:

list[0] = 10 // 完全响应式

因为 Proxy 能拦截:

set(target, key)

2 对象新增属性

Vue2:

Vue.set(obj, "age", 20)

Vue3:

obj.age = 20

即可。


3 深层对象更新

Vue2:

obj.a.b.c = 1

可能不会更新。

Vue3:

完全响应式。


三、Vue3 响应式核心原理

Vue3 响应式核心是三件事:

track
trigger
effect

结构如下:

targetMap (WeakMap)

target -> depsMap(Map)

key -> effects(Set)

结构示意:

WeakMap
  |
  |--- state
        |
        |--- "user" ----> Set(effect)
        |
        |--- "age" -----> Set(effect)

reactive 简化实现

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key)

      const res = Reflect.get(target, key, receiver)

      if (isObject(res)) {
        return reactive(res)
      }

      return res
    },

    set(target, key, value, receiver) {
      const old = target[key]

      const result = Reflect.set(target, key, value, receiver)

      if (old !== value) {
        trigger(target, key)
      }

      return result
    }
  })
}

关键点:

嵌套对象在 get 时才 reactive。

这叫:

lazy reactive


四、Vue3 真正需要注意的 10 个误区

下面才是 Vue3 真正存在的问题。


1 reactive 解构会丢失响应性

const state = reactive({
  user: { name: "Tom" }
})

const { user } = state

此时:

user 不是 Proxy

修改:

user.name = "Jack"

不会触发更新。

解决:

import { toRefs }

const { user } = toRefs(state)

2 reactive identity 问题

Proxy 会改变对象身份:

const obj = {}

const proxy = reactive(obj)

proxy === obj // false

因此:

map.set(obj, 1)

map.get(proxy) // undefined

解决:

使用:

toRaw()

3 ref 在模板会自动 unwrap

const count = ref(0)

模板:

{{ count }}

自动变成:

count.value

但在 JS 中不会。

count + 1 // 错
count.value + 1 // 对

4 reactive 不能直接替换对象

const state = reactive({ count: 0 })

state = reactive({ count: 10 }) // 错

因为:

state 是 const

正确方式:

Object.assign

5 watch 深度监听性能问题

watch(
  state,
  () => {},
  { deep: true }
)

如果数据巨大:

递归遍历整个对象

性能极差。

解决:

只监听需要的部分:

watch(() => state.user)

6 shallowRef 与 ref 的区别

ref:

深层 reactive

shallowRef:

只监听 value

示例:

const data = shallowRef({
  deep: { value: 1 }
})

data.value.deep.value = 2

不会更新。

需要:

triggerRef(data)

7 markRaw 可以跳过响应式

某些对象不适合 Proxy:

例如:

  • 大型库实例
  • 地图对象
  • DOM

可以使用:

markRaw(obj)

例如:

const chart = markRaw(new Chart())

8 不要对超大数据 reactive

例如:

10万条数据
reactive(bigList)

会创建大量 Proxy。

优化:

shallowRef
virtual list

9 computed 是 lazy 的

computed:

只有被访问才执行

例如:

const total = computed(() => price * count)

如果模板不使用:

不会计算

10 watch 默认异步执行

watch 默认 flush:

pre

顺序:

state change
watch
render

可以改成:

watch(source, cb, { flush: "post" })

五、大规模数据优化

真正影响性能的不是响应式正确性,而是 数据规模

推荐策略:

1 虚拟滚动

只渲染:

可见 20 行

2 shallowRef

const list = shallowRef([])

3 markRaw

const hugeData = markRaw(data)

4 分页

不要 reactive 全量数据。


六、Vue3 响应式最佳实践

推荐原则:

1 基本类型用 ref

ref

2 对象用 reactive

reactive

3 避免 deep watch

watch specific path

4 大对象用 shallowRef


5 避免 reactive identity 问题

使用:

toRaw

总结

Vue3 的 Proxy 响应式已经解决了 Vue2 的绝大多数问题:

Vue2问题Vue3
数组索引更新已解决
对象新增属性已解决
深层对象监听已解决
Vue.set不再需要

真正需要理解的是:

ref vs reactive
Proxy identity
shallow reactive
依赖收集
性能控制

理解这些,你就真正掌握了 Vue3 响应式系统。