Vue双向绑定原理

251 阅读2分钟

Vue双向绑定原理

Vue 的双向绑定(Two-Way Data Binding)主要依赖于 数据劫持(Reactive System)+ 依赖收集(Dep & Watcher) ,核心在于 数据变化时自动更新视图,以及 视图变化时同步更新数据

Vue 2 双向绑定原理

Vue 2 使用 Object.defineProperty() 进行数据劫持,并结合 WatcherDep 进行依赖收集。

1. 数据劫持(Observer)

Vue 2 在 data 初始化时,会遍历对象的每个属性,并使用 Object.defineProperty() 进行劫持。

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`读取 ${key}`);
      return val;
    },
    set(newVal) {
      console.log(`修改 ${key} = ${newVal}`);
      val = newVal;
    }
  });
}

const data = {};
defineReactive(data, "message", "Hello Vue");
console.log(data.message);  // 读取 message
data.message = "Hello World";  // 修改 message = Hello World

关键点:

  • 使用 Object.defineProperty() 劫持对象的 get/set 方法。
  • get 被调用时,触发依赖收集。
  • set 被调用时,触发视图更新。

2. 依赖收集(Dep & Watcher)

Vue 2 通过 DepWatcher 进行依赖收集,使得数据变更后可以通知相关的视图更新。

(1)Dep:依赖管理

Dep 维护一个 subs 依赖列表,每个属性对应一个 Dep,用于收集依赖。

class Dep {
  constructor() {
    this.subs = [];  // 依赖列表
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
(2)Watcher:订阅者

Watcher 订阅 Dep,当数据变化时,它会收到通知并更新视图。

class Watcher {
  constructor(updateFn) {
    this.updateFn = updateFn;
  }
  update() {
    this.updateFn();
  }
}
(3)绑定 Dep 和 Watcher

属性被读取(get)时,收集 Watcher;当 属性被修改(set)时,通知 Watcher 更新视图

function defineReactive(obj, key, val) {
  const dep = new Dep();

  Object.defineProperty(obj, key, {
    get() {
      dep.addSub(Dep.target);  // 依赖收集
      return val;
    },
    set(newVal) {
      val = newVal;
      dep.notify();  // 触发更新
    }
  });
}

const data = {};
defineReactive(data, "message", "Hello Vue");

// 创建 Watcher 监听数据变化
const watcher = new Watcher(() => {
  console.log("视图更新:" + data.message);
});
Dep.target = watcher;  // 模拟触发依赖收集
console.log(data.message);
Dep.target = null;

// 修改数据,触发更新
data.message = "Hello World";  

总结 Vue 2 原理:

  • Object.defineProperty() 劫持数据 get/set
  • Dep 依赖收集
  • Watcher 监听数据变化,通知视图更新

Vue 3 双向绑定原理

Vue 3 使用 Proxy 取代 Object.defineProperty(),使得 可以监听整个对象,不仅限于某个属性。

1. reactive() 代理数据

Vue 3 的 reactive() 通过 Proxy 劫持整个对象,返回一个可响应的代理。

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      console.log(`读取 ${key}`);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      console.log(`修改 ${key} = ${value}`);
      return Reflect.set(target, key, value, receiver);
    }
  });
}

const state = reactive({ count: 0 });
console.log(state.count); // 读取 count
state.count = 1;  // 修改 count = 1

2. 依赖收集 & 触发更新

Vue 3 依然使用 DepWatcher,但收集依赖的方式不同。

const bucket = new WeakMap();

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      if (!bucket.has(target)) {
        bucket.set(target, new Map());
      }
      const deps = bucket.get(target);
      if (!deps.has(key)) {
        deps.set(key, new Set());
      }
      deps.get(key).add(activeEffect); // 依赖收集
      return target[key];
    },
    set(target, key, value) {
      target[key] = value;
      if (bucket.has(target) && bucket.get(target).has(key)) {
        bucket.get(target).get(key).forEach(effect => effect());
      }
      return true;
    }
  });
}

let activeEffect = null;
function effect(fn) {
  activeEffect = fn;
  fn();  // 立即执行一次,触发依赖收集
  activeEffect = null;
}

// 使用示例
const state = reactive({ count: 0 });

effect(() => {
  console.log("视图更新:" + state.count);
});

state.count = 1;  // 触发 effect 更新

Vue 3 主要优化点

  1. Proxy 能监听整个对象,支持数组、新增属性等,Vue 2 需要 Vue.set() 手动监听新属性。
  2. 依赖收集存储在 WeakMap,避免内存泄漏。
  3. 响应式系统更灵活,支持 shallowReactive()readonly() 等不同模式。

Vue 2 和 Vue 3 对比

Vue 2Vue 3
核心 APIObject.defineProperty()Proxy
依赖收集方式Dep + WatcherWeakMap 存储依赖
响应式支持只能监听已存在的属性能监听新增/删除属性
性能遍历所有属性进行 definePropertyProxy 直接代理整个对象
代码简洁性代码较复杂代码更简洁,性能更优

总结

Vue 双向绑定核心在于 数据劫持 + 依赖收集 + 视图更新

  • Vue 2 使用 Object.defineProperty(),每个属性单独劫持。
  • Vue 3 使用 Proxy,直接代理整个对象,更灵活高效。
  • DepWatcher 负责依赖收集,实现数据变更触发视图更新。

Vue 3 的响应式系统更强大,避免了 Vue 2 的性能瓶颈,使得响应式更自然、更高效!🚀