Vue3 的响应式系统是其核心特性之一,它基于 ES6 的 Proxy API 重新设计,相比 Vue2 的 Object.defineProperty 有了显著的性能提升和功能增强。本文将深入解析 Vue3 响应式系统的工作原理。
Vue2 vs Vue3 响应式系统
Vue2 的局限性
Vue2 使用 Object.defineProperty 来实现响应式:
// Vue2 的实现方式
Object.defineProperty(obj, 'key', {
get() {
// 依赖收集
return value
},
set(newVal) {
// 触发更新
value = newVal
}
})
存在的问题:
- 无法监听数组索引和长度的变化
- 无法监听对象属性的添加和删除
- 需要递归遍历对象的所有属性
- 性能开销较大
Vue3 的优势
Vue3 使用 Proxy 来实现响应式:
// Vue3 的实现方式
const proxy = new Proxy(obj, {
get(target, key) {
// 依赖收集
return target[key]
},
set(target, key, value) {
// 触发更新
target[key] = value
return true
}
})
优势:
- 可以监听数组索引和长度变化
- 可以监听对象属性的添加和删除
- 性能更好,按需响应
- 支持 Map、Set、WeakMap、WeakSet
核心概念
1. Reactive 函数
reactive 函数将普通对象转换为响应式对象:
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: {
name: 'John',
age: 30
}
})
// 所有嵌套属性都是响应式的
state.count++ // 触发更新
state.user.name = 'Jane' // 触发更新
内部实现原理:
function reactive(target) {
// 如果已经是响应式对象,直接返回
if (isReactive(target)) {
return target
}
// 创建 Proxy
return new Proxy(target, {
get(target, key, receiver) {
// 依赖收集
track(target, key)
const result = Reflect.get(target, key, receiver)
// 如果值是对象,递归转换为响应式
if (isObject(result)) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 触发更新
if (oldValue !== value) {
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
const hadKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey) {
trigger(target, key)
}
return result
}
})
}
2. Ref 函数
ref 用于创建基本类型的响应式数据:
import { ref } from 'vue'
const count = ref(0)
// ref 内部实现
function ref(value) {
return {
_value: value,
get value() {
track(this, 'value')
return this._value
},
set value(newValue) {
if (this._value !== newValue) {
this._value = newValue
trigger(this, 'value')
}
}
}
}
3. 依赖收集(Track)
当访问响应式数据时,Vue 会收集当前的副作用函数:
// 简化的依赖收集实现
const targetMap = new WeakMap()
function track(target, key) {
// 获取当前正在执行的副作用函数
const effect = activeEffect
if (!effect) return
// 获取 target 的依赖映射
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取 key 的依赖集合
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 添加依赖
dep.add(effect)
}
4. 触发更新(Trigger)
当修改响应式数据时,Vue 会触发相关的副作用函数:
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (!dep) return
// 执行所有相关的副作用函数
dep.forEach(effect => {
if (effect.scheduler) {
effect.scheduler()
} else {
effect()
}
})
}
5. 副作用函数(Effect)
副作用函数是响应式系统的核心,它会在依赖变化时重新执行:
let activeEffect = null
function effect(fn, options = {}) {
const effectFn = () => {
// 清理旧的依赖
cleanup(effectFn)
// 设置为当前活跃的副作用
activeEffect = effectFn
effectStack.push(effectFn)
// 执行函数
const result = fn()
// 恢复之前的副作用
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return result
}
effectFn.deps = []
effectFn.options = options
if (options.lazy) {
return effectFn
}
effectFn()
return effectFn
}
实际应用
示例 1: 计算属性的实现
function computed(getter) {
let value
let dirty = true
const effectFn = effect(getter, {
lazy: true,
scheduler() {
if (!dirty) {
dirty = true
trigger(obj, 'value')
}
}
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
track(obj, 'value')
}
return value
}
}
return obj
}
示例 2: Watch 的实现
function watch(source, cb, options = {}) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let oldValue, newValue
const job = () => {
newValue = effectFn()
cb(newValue, oldValue)
oldValue = newValue
}
const effectFn = effect(() => getter(), {
lazy: true,
scheduler: job
})
if (options.immediate) {
job()
} else {
oldValue = effectFn()
}
}
性能优化
1. 浅层响应式
对于不需要深度响应式的大对象,可以使用 shallowReactive:
import { shallowReactive } from 'vue'
const state = shallowReactive({
count: 0,
user: {
name: 'John' // 这个属性不是响应式的
}
})
2. 只读响应式
使用 readonly 创建只读的响应式对象:
import { readonly } from 'vue'
const state = readonly({
count: 0
})
state.count++ // 警告:无法修改只读属性
3. 标记原始对象
使用 markRaw 标记对象,使其永远不会被转换为响应式:
import { markRaw, reactive } from 'vue'
const foo = markRaw({})
const state = reactive({
nested: foo // foo 不会被转换为响应式
})
常见问题
1. 为什么 ref 需要 .value?
因为基本类型无法使用 Proxy,所以 ref 将值包装在一个对象中:
const count = ref(0)
// 实际上相当于
const count = { value: 0 }
2. reactive 和 ref 的区别?
reactive只能用于对象和数组ref可以用于任何类型,包括基本类型ref在模板中会自动解包,但在 JavaScript 中需要.value
3. 如何避免响应式丢失?
不要解构响应式对象:
// ❌ 错误:解构会丢失响应式
const { count } = reactive({ count: 0 })
// ✅ 正确:使用 toRefs
const state = reactive({ count: 0 })
const { count } = toRefs(state)
总结
Vue3 的响应式系统基于 Proxy 实现,相比 Vue2 有了显著的改进:
- 更好的性能:按需响应,不需要递归遍历
- 更强大的功能:支持数组索引、对象属性添加/删除
- 更好的类型支持:与 TypeScript 配合更好
理解响应式系统的工作原理,有助于我们更好地使用 Vue3,编写出更高效的代码。
深入理解响应式系统是掌握 Vue3 的关键。希望这篇文章能帮助你更好地理解 Vue3 的响应式机制!