手写 50 行代码,彻底搞懂 Vue2 响应式原理

6 阅读3分钟

用过 Vue 的同学都知道,它最迷人的特性之一,就是数据变化 → 视图自动更新 的响应式魔法。不用手动操作 DOM,只需修改数据,页面就会同步刷新,这背后到底藏着怎样的逻辑?

其实答案很简单:Vue2 响应式的本质,就是 数据劫持 + 发布订阅模式 的结合。但这两个概念听起来抽象,具体是如何协同工作、实现“数据驱动视图”的?今天我们就用 50 行极简代码,拆解它的核心实现,让你一看就懂、一写就会。

一、先搞懂三个核心角色

Vue2 响应式就像一个通知系统,只有三个角色:

  1. Dep(调度中心) 负责记录谁用到了数据,数据一变就群发通知。
  2. Watcher(观察者) 用到数据的地方(视图 / 渲染函数),收到通知就更新。
  3. defineReactive(数据劫持) 给数据装上 “监控”,监听读取和修改行为。

一句话总结:
读数据 → 收集依赖
改数据 → 派发更新

二、代码实现迷你 Vue2 响应式

数据劫持 + 依赖收集 + 派发更新

// ====================
// 1. 调度中心 Dep:负责收集、通知 watcher
// 发布订阅模式的核心
// ====================
class Dep {
  constructor() {
    this.subs = []; // 存放所有 watcher(订阅者名单)
  }

  // 收集依赖:把当前 watcher 加入名单
  addSub(watcher) {
    this.subs.push(watcher);
  }

  // 通知更新:数据变了,告诉所有 watcher 更新
  notify() {
    this.subs.forEach(watcher => watcher.update());
  }
}

// 全局唯一标记:当前正在读取数据的 watcher
Dep.target = null;

// ====================
// 2. 观察者 Watcher:用到数据的地方(视图/渲染)
// ====================
class Watcher {
  constructor(updateFn) {
    this.updateFn = updateFn; // 保存视图更新函数
    this.get(); // 创建时立即执行一次 → 首次渲染 + 依赖收集
  }

  get() {
    Dep.target = this;    // 标记:现在是我在用数据
    this.updateFn();      // 执行渲染 → 触发数据的 get
    Dep.target = null;    // 收集完毕,清空标记
  }

  // 收到 Dep 通知,执行更新
  update() {
    console.log("🔄 视图更新啦!");
    this.updateFn(); // 重新渲染
  }
}

// ====================
// 3. 数据劫持:把普通对象变成响应式
// 使用 Object.defineProperty 监听 get/set
// ====================
function defineReactive(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]; // 闭包保存属性值
    const dep = new Dep(); // 每个属性,单独配一个调度中心

    // 劫持属性的 读 和 写
    Object.defineProperty(obj, key, {
      get() {
        // 依赖收集:谁在读我,就把谁加入订阅名单
        if (Dep.target) {
          dep.addSub(Dep.target);
        }
        return value;
      },
      set(newVal) {
        if (newVal === value) return; // 值没变化,不处理
        value = newVal; // 更新值

        // 派发更新:通知所有 watcher 重新渲染
        dep.notify();
      }
    });
  });
}

// ====================
// 4. 模拟 Vue 使用
// ====================
const data = {
  msg: "Hello Vue"
};

// 把 data 变成响应式数据
defineReactive(data);

// 创建 Watcher 模拟页面渲染 {{ msg }}
new Watcher(() => {
  console.log("渲染视图:", data.msg);
});

三、代码解析

第一步:数据变成响应式(数据劫持)

执行 defineReactive(data),给 data.msg 装上 getset,并创建一个 Dep 实例。

  • Object.defineProperty
  • 给对象的每个属性 劫持 get /set
  • 让数据变成 “可监听、可感知”

这一步就是 数据劫持

第二步:创建 Watcher → 首次渲染 + 依赖收集

  1. 执行 get()
  2. Dep.target = 当前watcher
  3. 执行渲染函数 → 读取 data.msg
  4. 触发 get把 watcher 存入 dep
  5. 输出:渲染视图:Hello Vue
  6. 清空标记

这一步叫:依赖收集

第三步:修改数据 → 自动更新视图

data.msg = "我学会响应式啦!"

触发流程:

  1. 触发 set
  2. 更新数据
  3. 执行 dep.notify()
  4. 通知所有 watcher 执行 update()
  5. 重新渲染视图

这一步叫:派发更新

最终效果

只修改数据,视图自动更新! 这就是 Vue 响应式的魔法!

四、总结

  1. 数据劫持:使用 Object.defineProperty 监听对象的 getset

  2. 依赖收集:渲染时读取数据 → 触发 get → 把 Watcher 存入 Dep。

  3. 派发更新:修改数据 → 触发 set → Dep 通知所有 Watcher 更新。

  4. 设计模式:发布 — 订阅模式。