Vue2 与 Vue3 双向绑定原理深度解析
Vue.js 作为现代前端框架的杰出代表,其双向数据绑定机制是其核心特性之一。本文将深入分析 Vue2 和 Vue3 在双向绑定实现上的差异和原理。
什么是双向数据绑定?
双向数据绑定是指数据模型(Model)和视图(View)之间的自动同步机制:
- 当数据模型发生变化时,视图自动更新
- 当用户在视图中进行操作(如输入)时,数据模型也会相应更新
Vue2 双向绑定原理
核心原理:Object.defineProperty
Vue2 的响应式系统基于 ES5 的 Object.defineProperty API,通过劫持对象属性的 getter 和 setter 来实现数据变化的监听。
javascript
// Vue2 响应式原理简化实现
function defineReactive(obj, key, val) {
const dep = new Dep() // 依赖收集器
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log(`访问了 ${key}: ${val}`)
// 收集依赖
if (Dep.target) {
dep.depend()
}
return val
},
set: function reactiveSetter(newVal) {
if (newVal === val) return
console.log(`设置 ${key}: ${newVal}`)
val = newVal
// 通知所有依赖更新
dep.notify()
}
})
}依赖收集与派发更新
Vue2 使用观察者模式实现依赖收集和更新派发:
javascript
// 依赖收集器
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
depend() {
if (Dep.target) {
this.addSub(Dep.target)
}
}
notify() {
this.subs.forEach(sub => sub.update())
}
}
// 观察者(Watcher)
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm
this.cb = cb
this.getter = expOrFn
this.value = this.get()
}
get() {
Dep.target = this
const value = this.getter.call(this.vm, this.vm)
Dep.target = null
return value
}
update() {
const newValue = this.get()
const oldValue = this.value
this.value = newValue
this.cb.call(this.vm, newValue, oldValue)
}
}Watcher 的使用方法
1. 内部 Watcher 使用
javascript
// 创建一个 Watcher 实例
const watcher = new Watcher(vm, expOrFn, callback, options)
// 参数说明:
// vm: Vue 实例
// expOrFn: 监听的表达式或函数
// callback: 数据变化时的回调函数
// options: 配置选项2. 实际使用示例
javascript
// 监听简单属性
const watcher1 = new Watcher(vm, 'message', (newVal, oldVal) => {
console.log(`message 从 ${oldVal} 变为 ${newVal}`)
})
// 监听计算属性或复杂表达式
const watcher2 = new Watcher(vm, function() {
return this.user.name + this.user.age
}, (newVal, oldVal) => {
console.log('用户信息变化:', newVal)
})
// 深度监听对象
const watcher3 = new Watcher(vm, 'user', (newVal, oldVal) => {
console.log('用户对象变化:', newVal)
}, { deep: true })
// 立即执行
const watcher4 = new Watcher(vm, 'count', (newVal, oldVal) => {
console.log('count:', newVal)
}, { immediate: true })3. Vue2 组件中的 watch 选项
javascript
export default {
data() {
return {
message: 'hello',
user: {
name: 'vue',
age: 18
}
}
},
watch: {
// 基础用法
message(newVal, oldVal) {
console.log('message changed:', newVal)
},
// 深度监听
user: {
handler(newVal, oldVal) {
console.log('user changed')
},
deep: true
},
// 立即执行
count: {
handler(val) {
console.log('count:', val)
},
immediate: true
}
}
}4. $watch API 使用
javascript
// 在组件实例中使用 $watch
this.$watch('message', function(newVal, oldVal) {
console.log('message changed:', newVal)
})
// 监听计算属性
this.$watch(function() {
return this.user.name + this.user.age
}, function(newVal, oldVal) {
console.log('computed value changed:', newVal)
})
// 返回取消监听函数
const unwatch = this.$watch('message', callback)
// 取消监听
unwatch()Vue2 双向绑定完整流程
- 初始化阶段:遍历 data 对象,使用
Object.defineProperty将所有属性转换为 getter/setter - 依赖收集:当组件渲染时,访问响应式数据会触发 getter,收集当前的 Watcher 依赖
- 派发更新:当数据改变时,触发 setter,通知所有依赖的 Watcher 执行更新
- 视图更新:Watcher 执行更新函数,重新渲染视图
javascript
// Vue2 响应式系统示例
const vm = new Vue({
data: {
message: 'Hello Vue2'
},
template: '<div>{{ message }}</div>'
})
// 当修改 vm.message 时,视图会自动更新
vm.message = 'Hello World' // 触发 setter -> notify -> watcher.update() -> 重新渲染Vue2 的局限性
- 无法检测数组索引和长度的变化
- 无法检测对象属性的添加和删除
- 需要深度遍历对象,性能开销较大
- 只能监听已存在的属性
javascript
// Vue2 中这些操作不会触发响应式更新
vm.items[0] = newValue // 数组索引赋值
vm.items.length = 0 // 修改数组长度
vm.newProperty = 'new value' // 添加新属性
// 需要使用特殊 API
Vue.set(vm.items, 0, newValue)
Vue.set(vm, 'newProperty', 'new value')Vue3 双向绑定原理
核心原理:Proxy
Vue3 采用 ES6 的 Proxy API 重写了响应式系统,解决了 Vue2 的诸多限制。
javascript
// Vue3 响应式原理简化实现
function reactive(target) {
const proxy = new Proxy(target, {
get(target, key, receiver) {
console.log(`访问了 ${key}`)
// 收集依赖
track(target, key)
const result = Reflect.get(target, key, receiver)
// 如果是对象,递归代理
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (value !== oldValue) {
console.log(`设置 ${key}: ${value}`)
// 触发更新
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
const hadKey = hasOwnProperty.call(target, key)
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
console.log(`删除了 ${key}`)
trigger(target, key)
}
return result
}
})
return proxy
}effect 和依赖收集
Vue3 使用 effect 函数替代 Vue2 的 Watcher:
javascript
let activeEffect = null
const targetMap = new WeakMap()
// 依赖收集
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
}
// 派发更新
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const deps = depsMap.get(key)
if (deps) {
deps.forEach(effect => effect())
}
}
// effect 函数
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn
fn()
activeEffect = null
}
effectFn()
return effectFn
}Vue3 响应式 API
Vue3 提供了更灵活的响应式 API:
javascript
import { reactive, ref, computed, watch } from 'vue'
// 响应式对象
const state = reactive({
count: 0,
user: {
name: 'Vue3'
}
})
// 响应式引用
const count = ref(0)
// 计算属性
const doubleCount = computed(() => count.value * 2)
// 监听器
watch(() => state.count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
// 修改数据
state.count++ // 支持
state.user.age = 25 // 支持,动态添加属性
state.arr[0] = 'new' // 支持,数组索引赋值
delete state.user.name // 支持,删除属性Vue2 vs Vue3 双向绑定对比
| 特性 | Vue2 | Vue3 |
|---|---|---|
| 核心 API | Object.defineProperty | Proxy |
| 浏览器兼容性 | IE9+ | IE 不支持 |
| 数组监听 | 需要重写数组方法 | 原生支持 |
| 对象属性添加/删除 | 不支持,需要 Vue.set | 原生支持 |
| 嵌套对象 | 需要递归遍历所有属性 | 懒代理,按需创建 |
| 性能 | 初始化时性能开销较大 | 更好的性能表现 |
| 监听粒度 | 属性级别 | 对象级别 |
双向绑定在组件中的应用
Vue2 v-model 实现
vue
<!-- 父组件 -->
<template>
<CustomInput v-model="inputValue" />
</template>
<script>
export default {
data() {
return {
inputValue: 'hello'
}
}
}
</script>
<!-- CustomInput 子组件 -->
<template>
<input
:value="value"
@input="$emit('input', $event.target.value)"
/>
</template>
<script>
export default {
props: ['value']
}
</script>Vue3 v-model 实现
vue
<!-- 父组件 -->
<template>
<CustomInput v-model="inputValue" />
</template>
<script setup>
import { ref } from 'vue'
const inputValue = ref('hello')
</script>
<!-- CustomInput 子组件 -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>实际应用场景
表单双向绑定
vue
<!-- Vue3 表单示例 -->
<template>
<form @submit.prevent="handleSubmit">
<div>
<label>姓名:</label>
<input v-model="form.name" type="text" />
</div>
<div>
<label>邮箱:</label>
<input v-model="form.email" type="email" />
</div>
<div>
<label>年龄:</label>
<input v-model.number="form.age" type="number" />
</div>
<div>
<label>爱好:</label>
<input
v-model="form.hobbies"
value="reading"
type="checkbox"
/> 阅读
<input
v-model="form.hobbies"
value="music"
type="checkbox"
/> 音乐
</div>
<button type="submit">提交</button>
</form>
<div>
<h3>实时预览:</h3>
<pre>{{ JSON.stringify(form, null, 2) }}</pre>
</div>
</template>
<script setup>
import { reactive } from 'vue'
const form = reactive({
name: '',
email: '',
age: 0,
hobbies: []
})
function handleSubmit() {
console.log('提交表单:', form)
}
</script>性能优化建议
Vue2 优化
- 减少深层嵌套:避免过深的对象嵌套
- 使用 Object.freeze():冻结不需要响应式的对象
- 合理使用 v-once:对于不变的内容使用 v-once
javascript
// Vue2 性能优化示例
export default {
data() {
return {
// 冻结静态数据
staticData: Object.freeze({
options: ['A', 'B', 'C']
}),
dynamicData: {
current: 'A'
}
}
}
}Vue3 优化
- 使用 shallowReactive:对于只需要浅层响应的对象
- 使用 markRaw:标记不需要响应式的对象
- 合理使用 ref vs reactive:简单类型用 ref,复杂对象用 reactive
javascript
// Vue3 性能优化示例
import { shallowReactive, markRaw, ref, reactive } from 'vue'
export default {
setup() {
// 浅层响应式
const shallowState = shallowReactive({
deep: {
nested: 'value' // 不会是响应式的
}
})
// 标记为非响应式
const nonReactive = markRaw({
huge: 'data'
})
// 简单值用 ref
const count = ref(0)
// 复杂对象用 reactive
const user = reactive({
name: 'Vue3',
profile: {
age: 25
}
})
return { shallowState, nonReactive, count, user }
}
}总结
Vue2 和 Vue3 在双向绑定的实现上有着本质的差异:
- Vue2 基于
Object.defineProperty,虽然兼容性好,但存在监听限制和性能问题 - Vue3 基于
Proxy,提供了更强大的拦截能力和更好的性能表现
Vue3 的响应式系统不仅解决了 Vue2 的局限性,还提供了更灵活的 API 设计,让开发者能够更精细地控制响应式行为。随着现代浏览器对 Proxy 的普及支持,Vue3 的响应式系统已成为现代前端开发的标准选择。
理解这些原理不仅有助于更好地使用 Vue.js,也为我们设计自己的响应式系统提供了宝贵的参考。