Vue的响应式系统是其核心机制之一,它使得数据变化能够自动反映到视图上。这套系统的本质是通过数据劫持结合发布-订阅模式实现的。
在Vue2中,实现响应式的核心是Object.defineProperty()方法。这个方法允许我们定义对象属性的getter和setter。当访问数据时会触发getter,修改数据时会触发setter。Vue在getter中收集依赖(即哪些组件或计算属性依赖这个数据),在setter中通知这些依赖进行更新。
javascript复制// Vue2响应式原理简化实现
function defineReactive(obj, key, val) {
const dep = new Dep() // 依赖收集器
Object.defineProperty(obj, key, {
get() {
dep.depend() // 收集当前正在执行的依赖
return val
},
set(newVal) {
val = newVal
dep.notify() // 通知所有依赖进行更新
}
})
}
而在Vue3中,改用了ES6的Proxy来实现响应式。Proxy可以拦截整个对象的操作,而不需要像Object.defineProperty那样逐个属性定义。
javascript复制// Vue3响应式原理简化实现
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key) // 收集依赖
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发更新
return true
}
})
}
关键区别:Proxy可以检测到对象属性的添加和删除,而Object.defineProperty无法做到这一点。这也是Vue2需要使用Vue.set/Vue.delete特殊API的原因。
Vue2在初始化阶段就会递归地将所有数据属性转换为响应式,这在大型应用中可能导致性能问题。而Vue3采用了惰性代理,只有在实际访问属性时才会进行响应式处理。
javascript复制// Vue2的响应式初始化
function observe(data) {
if (typeof data !== 'object' || data === null) return
// 已经是响应式数据则直接返回
if (data.__ob__) return data.__ob__
// 递归地将所有属性转换为响应式
return new Observer(data)
}
Vue2对数组的处理比较特殊,它重写了数组的7个变异方法(push、pop、shift、unshift、splice、sort、reverse)来实现响应式:
javascript复制// Vue2数组响应式处理
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function(method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args)
const ob = this.__ob__
ob.dep.notify() // 手动触发更新
return result
})
})
Vue3则不需要这种特殊处理,因为Proxy可以捕获到数组的任何变化,包括通过索引修改元素、修改length属性等操作。
Vue3的响应式系统在性能上有显著提升:
计算属性(computed)和侦听器(watch)都是基于响应式系统构建的。Vue3中对它们的实现进行了优化:
javascript复制// Vue3计算属性实现原理简化版
function computed(getter) {
let value
let dirty = true
const runner = effect(getter, {
lazy: true,
scheduler: () => {
dirty = true
trigger(obj, 'value') // 触发计算属性依赖的更新
}
})
const obj = {
get value() {
if (dirty) {
value = runner()
dirty = false
}
track(obj, 'value') // 收集当前依赖
return value
}
}
return obj
}
Vue的组件更新是基于响应式数据的。当数据变化时,会触发组件的重新渲染。Vue3在这方面做了重要优化:
在Vue2中,直接给对象添加新属性或删除属性不会触发响应式更新:
javascript复制// Vue2中的问题
data: {
user: {
name: '张三'
}
}
// 不会触发响应式更新
this.user.age = 25
解决方案:
javascript复制// Vue2解决方案
this.$set(this.user, 'age', 25)
在Vue3中,由于使用Proxy,这个问题自然解决了:
javascript复制// Vue3中直接操作即可
this.user.age = 25 // 会自动触发响应式更新
对于大型数组,Vue2的响应式处理可能导致性能问题。Vue3通过以下方式优化:
在Vue2和Vue3中,解构响应式对象都会导致响应式丢失:
javascript复制const state = reactive({
count: 0
})
// 响应式丢失
const { count } = state
Vue3提供了toRefs工具函数解决这个问题:
javascript复制import { toRefs } from 'vue'
const state = reactive({
count: 0
})
// 保持响应式
const { count } = toRefs(state)
从Vue2迁移到Vue3时,在响应式系统方面需要注意:
javascript复制// Vue2选项式API
export default {
data() {
return {
count: 0
}
},
watch: {
count(newVal, oldVal) {
console.log('count changed', newVal)
}
}
}
// Vue3组合式API
import { ref, watch } from 'vue'
export default {
setup() {
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log('count changed', newVal)
})
return {
count
}
}
}
Vue3允许你创建自定义的响应式转换:
javascript复制import { customRef } from 'vue'
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
}
}
})
}
// 使用
const text = useDebouncedRef('hello')
对于不需要深度响应式的场景,Vue3提供了shallowReactive和shallowRef:
javascript复制import { shallowReactive } from 'vue'
const state = shallowReactive({
// 只有顶层的属性是响应式的
foo: 1,
nested: {
bar: 2 // 这个属性不是响应式的
}
})
Vue3提供了readonly和shallowReadonly来创建只读的响应式代理:
javascript复制import { reactive, readonly } from 'vue'
const original = reactive({ count: 0 })
const copy = readonly(original)
// 修改copy会触发警告
copy.count++ // 警告: Set operation on key "count" failed: target is readonly.
对于不需要响应式的数据,可以使用markRaw标记:
javascript复制import { reactive, markRaw } from 'vue'
const obj = reactive({
// 被标记为raw的数据不会被转换为响应式
foo: markRaw({ bar: 1 })
})
Vue3的响应式系统会自动批量处理同步的更新,但有时需要手动控制:
javascript复制import { nextTick } from 'vue'
async function updateData() {
state.value1 = 1
state.value2 = 2
// 等待所有更新完成
await nextTick()
// 此时DOM已经更新
}
对于大型列表,可以使用虚拟滚动或分片渲染:
javascript复制import { ref, onMounted } from 'vue'
export default {
setup() {
const list = ref([])
const visibleItems = ref([])
function updateVisibleItems() {
// 只渲染可见区域的项目
}
onMounted(() => {
window.addEventListener('scroll', updateVisibleItems)
})
return {
visibleItems
}
}
}
Vue3提供了onRenderTracked和onRenderTriggered钩子来调试组件的依赖:
javascript复制import { onRenderTracked, onRenderTriggered } from 'vue'
export default {
setup() {
onRenderTracked((event) => {
console.log('依赖被追踪:', event)
})
onRenderTriggered((event) => {
console.log('依赖触发更新:', event)
})
}
}
可以通过以下方式检查一个对象是否是响应式的:
javascript复制import { isReactive, isRef } from 'vue'
const obj = reactive({})
console.log(isReactive(obj)) // true
console.log(isRef(ref(0))) // true
可以通过自定义effect scheduler来调试响应式更新:
javascript复制import { effect } from 'vue'
const runner = effect(() => {
// 副作用代码
}, {
scheduler(effect) {
console.log('调度执行:', effect)
effect() // 手动执行副作用
}
})
Vue3的响应式系统能够正确处理循环引用:
javascript复制const obj = reactive({})
obj.self = obj // 循环引用
console.log(obj.self === obj) // true
Proxy能够捕获原型链上的属性访问,但需要注意:
javascript复制const parent = reactive({ foo: 1 })
const child = reactive(Object.create(parent))
child.bar = 2
console.log(child.foo) // 1, 来自原型链
代理内置对象(如Date、Map、Set)时需要特殊处理:
javascript复制const date = reactive(new Date())
// 直接修改不会触发响应式
date.setDate(date.getDate() + 1)
// 解决方案:创建一个新的Date实例
const newDate = new Date(date)
newDate.setDate(date.getDate() + 1)
date = newDate
javascript复制import { effect, stop } from 'vue'
export default {
setup() {
const myEffect = effect(() => {
// 副作用代码
})
onUnmounted(() => {
stop(myEffect) // 清理effect
})
}
}