vue的双向绑定原理

192 阅读3分钟

Vue 的响应式原理和双向绑定实现,核心在于利用数据劫持(通过 Object.definePropertyProxy)来实现数据和视图的同步更新,同时通过依赖追踪和发布-订阅模式来实现变化通知。

一、双向绑定原理

双向绑定可以理解为两部分:

  1. 数据驱动视图(数据到视图)
    • Vue通过响应式系统来监听数据的变化,当数据发生改变时,自动更新相关的视图。这是通过 getter/setter 提前设置数据变化的回调函数来完成的。
  2. 视图驱动数据(视图到数据)
    • 通过用户与视图的交互(如表单输入等),触发事件监听器,自动将视图的变化反馈到数据上。
    • Vue 的 v-model 是最典型的双向绑定实现,表单元素的变化通过事件监听自动同步到数据。

二、响应式原理

Vue 的响应式系统是通过以下几个步骤实现的:

  1. 数据劫持(Observer 模式)

    • Vue 使用 Observer 对象来对数据进行劫持。它会递归遍历整个数据对象的所有属性,并使用 Object.defineProperty 将每个属性转换为 getter/setter。在 Vue3 中则是通过 Proxy 进行劫持。
    • getter 用来收集依赖,setter 用来触发更新。
  2. 依赖追踪(Dep 模式)

    • Vue 会在数据的 getter 中收集依赖,这个依赖关系是通过 Dep 来管理的。每个数据属性都会创建一个对应的 Dep 对象,用来存储所有依赖这个属性的 Watcher 对象。
  3. 依赖收集(Watcher 模式)

    • 当视图中访问响应式数据时,getter 方法会触发,将当前视图的 Watcher 对象添加到这个属性的 Dep 对象中。这样,当数据变化时,Vue 就可以找到依赖此数据的所有 Watcher,并通知它们更新。
  4. 响应更新

    • 当响应式数据被修改时,Vue 会触发 setter 方法,通知对应的 Dep 对象,Dep 会通知所有依赖这个数据的 WatcherWatcher 接收到通知后,会更新视图。

三、核心代码示例(Vue2 实现)

// 1. 数据劫持:Observer 类
class Observer {
  constructor(data) {
    this.observe(data);
  }

  observe(data) {
    if (!data || typeof data !== 'object') return;
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key]);
    });
  }

  defineReactive(obj, key, value) {
    this.observe(value); // 递归遍历对象
    const dep = new Dep(); // 为每个属性实例化依赖管理器
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 收集依赖
        Dep.target && dep.addDep(Dep.target);
        return value;
      },
      set(newValue) {
        if (newValue !== value) {
          value = newValue;
          dep.notify(); // 通知依赖更新
        }
      }
    });
  }
}

// 2. 依赖管理:Dep 类
class Dep {
  constructor() {
    this.subs = []; // 存放依赖
  }

  addDep(watcher) {
    this.subs.push(watcher);
  }

  notify() {
    this.subs.forEach(watcher => watcher.update());
  }
}

// 3. Watcher 类,负责更新视图
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    Dep.target = this; // 将当前的 watcher 实例挂载到 Dep.target 上
    this.vm[key]; // 触发 getter 收集依赖
    Dep.target = null; // 收集完依赖后清除
  }

  update() {
    // 数据更新时,调用回调更新视图
    this.cb.call(this.vm, this.vm[this.key]);
  }
}

// 4. Vue 模拟实现
class Vue {
  constructor(options) {
    this.$data = options.data;
    this.observe(this.$data); // 实现数据劫持
    new Watcher(this, 'message', function(newVal) {
      console.log('视图更新了:', newVal);
    });
  }

  observe(data) {
    new Observer(data); // 将数据变成响应式的
  }
}

// 测试
const vm = new Vue({
  data: {
    message: 'Hello, Vue!'
  }
});

vm.$data.message = 'Hello, World!'; // 修改数据后,视图会自动更新

四、Vue3 响应式系统的变化

在 Vue3 中,Vue 使用 Proxy 替代了 Object.defineProperty 来实现响应式系统。相比于 Vue2 的数据劫持,Proxy 可以直接监听对象新增或删除的属性,并且支持数组等复杂数据类型的直接操作。

Vue3 响应式系统简单实现:

const reactive = (target) => {
  if (typeof target !== 'object' || target === null) return target;

  const handler = {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver);
      console.log(`获取属性:${key} = ${result}`);
      return result;
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      console.log(`设置属性:${key} = ${value}`);
      return result;
    }
  };

  return new Proxy(target, handler);
};

// 测试
const state = reactive({ message: 'Hello, Vue3!' });
state.message = 'Hello, Proxy!';