深入理解 Vue 2 的双向绑定原理与实现

在 Vue 2 中,双向绑定是 Vue 的核心功能之一,它通过数据响应式系统使得数据的变化自动反映在视图上,同时用户在视图上做的更改也能够同步回数据模型。这种双向绑定是通过 数据劫持(Data Hijacking) 和 发布-订阅模式(Publish-Subscribe Pattern) 实现的。以下是双向绑定原理及实现方式的详细解析:

1. 数据劫持(Data Hijacking)

Vue 2 使用 Object.defineProperty() 来实现数据劫持。对于每一个数据属性,Vue 会将它转化为一个 gettersetter,从而拦截对数据的访问和修改操作。

实现原理:

  • 当访问一个数据属性时,getter 会被调用,Vue 可以通过这个 getter 来跟踪这个属性依赖的所有视图组件。

  • 当修改一个数据属性时,setter 会被调用,Vue 可以通知所有依赖这个属性的视图组件进行更新。

// 定义一个函数,用于将对象的某个属性转换为响应式属性
function defineReactive(obj, key, val) {
  
  // 使用 Object.defineProperty 对 obj 对象的 key 属性进行拦截
  Object.defineProperty(obj, key, {
    
    // 这个属性是否可以被枚举,例如在 for...in 循环中是否能遍历到
    enumerable: true,
    
    // 这个属性是否可以被删除或再次修改属性描述符
    configurable: true,
    
    // getter:当外部访问 obj.key 时,会调用此方法
    get() {
      console.log(`访问属性:${key}`);
      return val;  // 返回属性的当前值
    },
    
    // setter:当外部修改 obj.key 时,会调用此方法
    set(newVal) {
      if (newVal !== val) {  // 检查新值是否与旧值不同
        console.log(`属性${key}从${val}变为${newVal}`);
        val = newVal;  // 更新属性的值
        // 通知机制可以放在这里,告知相关视图进行更新
      }
    }
  });
}

解释:

  1. defineReactive 函数:用于将对象的某个属性(如 key)转化为响应式属性。它接收三个参数:对象 obj,属性名 key,以及属性的初始值 val

  2. Object.defineProperty():这是 JavaScript 中用于定义或修改对象属性的 API。在这里,Vue 用它来拦截对象属性的访问和赋值操作。

  3. enumerableconfigurable:这两个选项分别控制属性是否可枚举和是否可配置。将 enumerable 设置为 true,意味着该属性可以被循环枚举;configurable: true 允许属性描述符被修改或属性被删除。

  4. getter 方法:当外部代码访问 obj.key 时,getter 会被调用。getter 会返回属性的当前值 val,并在调试时打印访问日志。

  5. setter 方法:当外部代码为 obj.key 赋新值时,setter 会被调用。setter 首先检查新值是否与旧值不同,如果不同则更新属性的值,并可以在此处触发视图更新逻辑。

2. 发布-订阅模式(Publish-Subscribe Pattern)

在 Vue 2 中,视图和数据之间的关联是通过一个叫做 Watcher 的类来实现的。Watcher 作为发布者和订阅者之间的桥梁,每一个 Watcher 订阅了某个数据属性,当该属性发生变化时,Watcher 就会被通知并触发相应的视图更新。

实现步骤:

  1. Dep(依赖收集器):每个数据属性都有一个对应的 Dep 对象,用来收集所有依赖于该属性的 Watcher

  2. Watcher:当视图模板被解析时,Vue 会为每一个依赖数据的地方创建一个 Watcher,并将其添加到对应数据属性的 Dep 中。

  3. 通知更新:当数据属性发生变化时,对应的 Dep 会通知所有相关的 Watcher,进而触发视图更新。

// Dep 类用于收集依赖,每个属性都对应一个 Dep 实例
class Dep {
  constructor() {
    this.subs = [];  // 存储所有依赖于这个属性的 Watcher 实例
  }
  
  // 添加依赖的方法,传入一个 Watcher 实例
  addSub(sub) {
    this.subs.push(sub);
  }
  
  // 通知所有依赖,触发它们的更新
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}

// Watcher 类,用于当数据变化时执行特定的回调函数
class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm;  // 保存组件实例
    this.getter = expOrFn;  // 将表达式或函数保存起来
    this.cb = cb;  // 当数据变化时执行的回调函数
    this.value = this.get();  // 初始化时获取并保存属性值,并触发依赖收集
  }
  
  // 获取属性值并进行依赖收集
  get() {
    Dep.target = this;  // 将当前的 Watcher 实例指向 Dep.target
    let value = this.getter.call(this.vm, this.vm);  // 调用表达式或函数获取值
    Dep.target = null;  // 释放 Dep.target,避免重复收集
    return value;  // 返回获取到的属性值
  }
  
  // 当数据变化时,Watcher 会调用这个方法来更新视图
  update() {
    const oldValue = this.value;  // 保存旧值
    this.value = this.get();  // 获取新值并重新依赖收集
    this.cb.call(this.vm, this.value, oldValue);  // 执行回调函数,传入新旧值
  }
}

解释:

  1. Dep 类:每个属性对应一个 Dep 实例,用于收集所有依赖于该属性的 Watchersubs 数组用于存储这些 Watcher 实例。

  2. addSub 方法:将一个 Watcher 实例添加到 subs 数组中,这是依赖收集的过程。

  3. notify 方法:当属性值发生变化时,调用 notify 方法遍历 subs 数组,并触发每个 Watcherupdate 方法,从而通知视图更新。

  4. Watcher 类:用于创建一个观察者实例。当依赖的数据发生变化时,Watcher 会被通知并执行相应的更新操作。Watcher 保存了一个回调函数 cb,用于在数据变化时执行。

  5. get 方法:当创建 Watcher 实例时,会调用 get 方法获取数据的初始值,并触发依赖收集。Dep.target 用于临时存储当前的 Watcher 实例,以便在 getter 中添加依赖。

  6. update 方法:当数据变化时,update 方法会被调用,它会重新获取数据的当前值,并调用回调函数来更新视图。

3. 双向绑定的完整流程

结合以上内容,Vue 2 的双向绑定机制大致可以描述为以下步骤:

  1. 数据劫持:通过 Object.defineProperty() 拦截对象属性的访问和修改,将属性转化为响应式属性。

  2. 依赖收集:在初始化组件时,Vue 解析模板并为每个依赖于响应式属性的地方创建一个 Watcher。这些 Watcher 会被添加到相应属性的 Dep 中。

  3. 数据变更通知:当响应式属性的值发生变化时,setter 被触发,对应的 Dep 通知所有依赖此属性的 Watcher,调用它们的 update 方法更新视图。

总结

Vue 2 的双向绑定通过数据劫持和发布-订阅模式实现了数据和视图的自动同步,Object.defineProperty 提供了数据响应性,而 WatcherDep 通过依赖收集和通知机制保证了视图与数据的联动性。这种机制的实现虽然强大,但由于 Object.defineProperty 的限制,对对象属性的劫持只能在初始化时完成,这也是为什么 Vue 3 转向了使用 Proxy 来实现更灵活的响应式系统。

你可能感兴趣的:(vue.js,前端,javascript)