说说Vue 是如何实现 MVVM 的?

291 阅读3分钟

什么是 MVVM 模型?🤔

🤔(好熟悉的表情包,又cp脑了)

MVVM(Model-View-ViewModel)是一种设计模式,用来组织前端项目。

  • Model:数据层(业务数据,如 data)
  • View:视图层(页面 UI,如 template)
  • ViewModel:连接 Model 和 View 的桥梁(Vue实例,负责数据响应和 DOM 渲染同步)

在 Vue 里,ViewModel 其实就是你创建的那个 Vue实例,负责:

  • 让 Model 改变时,View 自动刷新
  • 让 View(比如输入框)变化时,Model 自动更新

MVVM在Vue源码中是怎么实现的?(以Vue2为例)⚙️

Vue2 核心的 MVVM 流程:

步骤关键组件源码核心逻辑
1. 观察数据变化(数据劫持)Observer使用 Object.defineProperty 劫持 data 中的每个字段,给每个字段挂 getter/setter
2. 收集依赖Dep(依赖收集器)getter 里收集 “谁用到了我”
3. 响应式更新Watcher(订阅者)数据变了,通知依赖它的 Watcher,重新渲染或执行回调
4. 模板编译Compiler(编译器)解析模板里的指令(如 v-model、{{}}),把它们和 Watcher 绑定起来

核心源码片段(简化版)📄

Observer(数据劫持)

function defineReactive(obj, key, val) {
  const dep = new Dep();
  
  Object.defineProperty(obj, key, {
    get() {
      dep.depend(); // 依赖收集
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        val = newVal;
        dep.notify(); // 通知更新
      }
    }
  });
}

Dep(依赖收集器)

class Dep {
  constructor() {
    this.subs = []; // 存放 Watcher
  }
  
  depend() {
    if (Dep.target) {
      this.subs.push(Dep.target);
    }
  }
  
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}

Dep.target = null; // 当前正在依赖收集的 Watcher

Watcher(订阅者)

class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    
    Dep.target = this; // 设置当前 Watcher
    this.value = vm[key]; // 触发 getter,收集依赖
    Dep.target = null;
  }
  
  update() {
    const newVal = this.vm[this.key];
    this.cb(newVal);
  }
}

Vue用 Object.defineProperty + 发布订阅模式,完成了 MVVM 的双向绑定。****


Vue3变化:用Proxy替代了 Object.defineProperty 🚀

  • Object.defineProperty只能劫持对象已有的属性,新增/删除属性无法监控
  • Proxy可以直接拦截整个对象(包括动态增加、删除属性)
  • 效率更高,功能更强大

所以 Vue3 重构了响应式系统,核心是 Proxy + Reflect。

Vue3 响应式系统的三大支柱

组件作用源码核心
reactive代理数据对象使用 Proxy 劫持
effect包裹副作用函数(视图更新)自动收集依赖、触发更新
依赖收集(Dep表)追踪哪些函数依赖哪些数据用 WeakMap 管理 target -> key -> effectList

reactive:把对象变成响应式 🧩

reactive 用 Proxy 包了一层。

  • 监听 get:收集依赖
  • 监听 set:触发依赖

➡️ 源码核心(超简化版):

function reactive(target) {
  return new Proxy(target, {
    get(obj, key) {
      track(obj, key); // 收集依赖
      return Reflect.get(obj, key);
    },
    set(obj, key, value) {
      const result = Reflect.set(obj, key, value);
      trigger(obj, key); // 触发更新
      return result;
    }
  });
}

effect:注册副作用函数🧩

“副作用函数”就是:当依赖的数据变化时,要重新执行的函数。 比如更新 DOM、重新计算数据。

import { effect } from 'vue'

effect(() => {
  console.log(state.count);
})
  • effect会把传入的函数注册到”依赖表”里

  • 并且立即执行一次收集依赖

➡️ 源码核心(超简化版):

let activeEffect = null;

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

依赖收集:track & trigger 🧩

Vue3用了一个超级巧妙的依赖收集结构

const targetMap = new WeakMap(); 
// WeakMap: target -> Map(key -> Set(effects))
  • track() 收集谁依赖了这个属性

  • trigger() 通知谁需要更新

➡️ 源码超简化版:

function track(target, key) {
  if (!activeEffect) return;
  
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  
  let deps = depsMap.get(key);
  if (!deps) {
    deps = new Set();
    depsMap.set(key, deps);
  }
  
  deps.add(activeEffect);
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  
  const deps = depsMap.get(key);
  if (deps) {
    deps.forEach(effectFn => effectFn());
  }
}

数据对象 (Proxy) 
  ↙️ get(key) 时 -> track() -> 收集 effect
  ↘️ set(key) 时 -> trigger() -> 触发所有 effect

effect(fn) 
  ↘️ 内部读取了哪些 key → 这些 key 就和 effect 绑定

最终的完整结构长这样:

targetMap (WeakMap)
  └── target (数据对象)
        └── key (属性)
              └── deps (Set of effect 函数)

Vue3 响应式系统 = Proxy 劫持 + WeakMap 依赖追踪 + effect 函数机制。****

总结重点:Vue2 vs Vue3 响应式底层的巨大变化 🔚

特性Vue2Vue3
劫持方式Object.definePropertyProxy
深层次劫持需要递归所有属性懒劫持,按需触发
新增/删除属性需要手动处理 $set天生支持
性能较差(大量getter/setter对象)更好(只有一个Proxy对象)
内存管理不能回收WeakMap自动回收