Skip to content

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 双向绑定完整流程

  1. 初始化阶段:遍历 data 对象,使用 Object.defineProperty 将所有属性转换为 getter/setter
  2. 依赖收集:当组件渲染时,访问响应式数据会触发 getter,收集当前的 Watcher 依赖
  3. 派发更新:当数据改变时,触发 setter,通知所有依赖的 Watcher 执行更新
  4. 视图更新: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 的局限性

  1. 无法检测数组索引和长度的变化
  2. 无法检测对象属性的添加和删除
  3. 需要深度遍历对象,性能开销较大
  4. 只能监听已存在的属性
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 双向绑定对比

特性Vue2Vue3
核心 APIObject.definePropertyProxy
浏览器兼容性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 优化

  1. 减少深层嵌套:避免过深的对象嵌套
  2. 使用 Object.freeze():冻结不需要响应式的对象
  3. 合理使用 v-once:对于不变的内容使用 v-once
javascript
// Vue2 性能优化示例
export default {
  data() {
    return {
      // 冻结静态数据
      staticData: Object.freeze({
        options: ['A', 'B', 'C']
      }),
      dynamicData: {
        current: 'A'
      }
    }
  }
}

Vue3 优化

  1. 使用 shallowReactive:对于只需要浅层响应的对象
  2. 使用 markRaw:标记不需要响应式的对象
  3. 合理使用 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,也为我们设计自己的响应式系统提供了宝贵的参考。

基于 VitePress 构建