双向绑定原理

982 阅读3分钟

Vue 双向绑定原理

一、双向绑定的本质与核心流程

定义:双向绑定指视图与数据的自动同步,即修改数据时视图更新,用户操作视图时数据也随之改变(如 v-model 指令)。

核心流程(三要素)

  1. 数据监听:通过 Object.defineProperty 劫持数据的 getter/setter;
  2. 视图更新:依赖收集与派发更新(Watcher 与 Dep 模式);
  3. 用户输入处理:通过事件监听(如 input 事件)更新数据。

二、数据监听的实现(Object.defineProperty)

1. 核心代码示例
// Vue 2 中数据响应式的核心实现(简化版)
function defineReactive(obj, key, value) {
  // 依赖收集器(每个属性对应一个 Dep 实例)
  const dep = new Dep();
  
  // 劫持 getter/setter
  Object.defineProperty(obj, key, {
    get() {
      // 依赖收集:将当前 Watcher 存入 Dep
      if (Dep.target) {
        dep.depend();
      }
      return value;
    },
    set(newVal) {
      if (newVal === value) return;
      value = newVal;
      // 派发更新:通知所有依赖该属性的 Watcher 刷新视图
      dep.notify();
    }
  });
}

// 递归遍历对象所有属性
function observe(obj) {
  if (!obj || typeof obj !== 'object') return;
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}
2. 关键概念解析
  • Dep 类:每个响应式属性对应一个 Dep 实例,用于收集依赖它的 Watcher(订阅者);
  • Watcher 类:当数据变化时,Watcher 会收到通知并触发视图更新(如重新渲染组件);
  • 依赖收集时机:当视图读取数据(触发 getter)时,将当前 Watcher 存入 Dep;
  • 派发更新时机:当数据被修改(触发 setter)时,Dep 通知所有 Watcher 执行更新。

三、双向绑定的完整流程(以 v-model 为例)

1. 模板编译阶段
<!-- 模板 -->
<input v-model="message" />

<!-- 编译后等价于 -->
<input 
  :value="message" 
  @input="message = $event.target.value"
/>
2. 运行时双向绑定流程
  1. 初始化阶段

    • 视图渲染时读取 message,触发 getter,将当前组件的 Watcher 存入 message 对应的 Dep;
    • 输入框的 value 属性绑定到 message,视图显示 message 的值。
  2. 用户输入阶段

    • 用户修改输入框内容,触发 input 事件;
    • 事件回调将 $event.target.value 赋值给 message,触发 setter;
    • setter 通知 Dep 派发更新,Dep 遍历所有 Watcher(组件 Watcher),触发视图重新渲染。
  3. 数据修改阶段

    • 代码中修改 message(如 this.message = 'new value'),触发 setter;
    • setter 通知 Dep 派发更新,组件 Watcher 重新渲染视图,输入框 value 同步更新。

四、双向绑定的缺陷与 Vue 3 的优化

1. Vue 2 双向绑定的限制
  • 数组变异方法的特殊处理
    由于 Object.defineProperty 无法监听数组索引和长度的变化,Vue 2 对数组的 pushpop 等方法进行了重写(通过 Array.prototype 拦截),而直接修改索引(如 arr[0] = value)不会触发更新,需使用 Vue.set(arr, index, value)arr.splice(index, 1, value)

  • 对象新增属性的响应式问题
    新增属性不会被 defineReactive 劫持,需使用 Vue.set(obj, 'newProp', value)this.$set

  • 性能问题
    深层嵌套对象会递归监听所有属性,导致初始化性能开销较大。

2. Vue 3 的优化(Proxy 替代 Object.defineProperty)
// Vue 3 中使用 Proxy 实现响应式(简化版)
function createReactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      // 依赖收集(比 Vue 2 更高效,可捕获整个对象的访问)
      track(target, key);
      return Reflect.get(target, key);
    },
    set(target, key, value) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value);
      // 派发更新(精准通知变化的属性)
      if (oldValue !== value) {
        trigger(target, key);
      }
      return result;
    }
  });
}

优势

  • 原生支持数组索引和长度变化:Proxy 可直接监听数组的所有操作;
  • 动态新增属性响应式:Proxy 可捕获任意属性的访问与修改;
  • 性能优化:按需监听(仅在属性被访问时收集依赖),避免递归全量监听。

五、问题

1. 问:Vue 双向绑定的核心原理是什么?请用代码简单说明。

  • Vue 通过 Object.defineProperty 劫持数据的 getter/setter,结合 Watcher 和 Dep 实现依赖收集与更新派发。当视图读取数据时,触发 getter 收集依赖;当数据修改时,触发 setter 通知 Watcher 刷新视图。以 v-model 为例,它本质是 :value@input 的语法糖,实现视图与数据的双向同步。
2. 问:Vue 2 中数组直接通过索引修改元素为什么不会触发更新?如何解决?

  • 因为 Object.defineProperty 无法监听数组索引的变化,直接修改 arr[0] = value 不会触发 setter。解决方案:
    • 使用 Vue.set(arr, index, value)this.$set
    • 使用数组的变异方法(如 splice):arr.splice(index, 1, value)
    • 替换整个数组:this.arr = [...this.arr]
3. 问:Vue 3 为什么用 Proxy 替代 Object.defineProperty?
    • 功能更完整:Proxy 原生支持监听数组索引、长度变化及动态新增属性;
    • 性能更优:Object.defineProperty 需递归遍历所有属性,而 Proxy 可直接代理整个对象,且依赖收集是按需进行的(仅在属性被访问时收集);
    • 语法更简洁:Proxy 以声明式方式定义拦截行为,代码结构更清晰。

六、总结

双向绑定三要素:数据监听、依赖收集、更新派发;
Vue 2 用 Object.defineProperty 劫持 getter/setter,配合 Dep 和 Watcher;
v-model 是语法糖,等价于 :value + @input
数组索引修改不触发更新,需用 Vue.setsplice
Vue 3 用 Proxy 优化,支持动态属性、数组操作,性能更优。