整体分析

  1. MVVM (Model-View-ViewModel)Vue2 的核心设计模式,核心目标是数据驱动视图:
    1. Model (数据) 变化时,ViewModel 自动更新 View (视图)
    2. View 交互时,ViewModel 同步更新 Model
  2. Vue2MVVM 核心依赖:
    1. Observer (数据劫持):通过 Object.defineProperty 劫持数据的 get/set,实现数据监听,收集依赖、触发更新;
    2. Dep (依赖收集):管理 Watcher,数据变化时通知所有相关 Watcher 执行更新;
    3. Watcher (观察者):连接 Dep 和视图,订阅数据变化,触发视图更新;
    4. Compiler (模板编译):解析模板中的指令和插值表达式,初始化视图,绑定数据与视图的关联;
    5. MVVM 入口:整合上述模块,作为对外暴露的核心类;
  3. 核心流程:

完整代码实现

目录结构

├── index.html  // 测试页面
└── mvvm/
    ├── MVVM.js  // 核心入口类
    ├── Observer.js  // 数据劫持
    ├── Dep.js  // 依赖收集器
    ├── Watcher.js  // 订阅者
    └── Compiler.js  // 模板解析

Dep.js(依赖收集器)

Dep 类:每个数据属性对应一个 Dep,负责收集依赖 (Watcher),数据变化时调用 notify 通知所有 Watcher 更新;

// 依赖收集器:收集 Watcher,数据变化时通知所有 Watcher
class Dep {
  constructor() {
    // 存储所有订阅者(Watcher 实例)
    this.subs = [];
  }

  // 添加订阅者
  addSub(sub) {
    // 只收集 Watcher 实例
    if (sub && sub.update) {
      this.subs.push(sub);
    }
  }

  // 通知所有订阅者更新
  notify() {
    this.subs.forEach(sub => sub.update());
  }

  // 依赖收集:在 get 中调用
  depend() {
    // Dep.target 是当前活跃的 Watcher
    if (Dep.target) {
      this.addSub(Dep.target);
    }
  }
}

// 静态属性:存储当前正在执行的 Watcher
Dep.target = null;

export default Dep;

Observer.js(数据劫持)

Observer 类:通过 Object.defineProperty 劫持数据的 get/set,get 时收集依赖,set 时通知依赖更新;

import Dep from './Dep.js';

// 数据劫持:遍历 data,为每个属性添加 get/set
class Observer {
  constructor(data) {
    this.data = data;
    // 遍历数据,实现劫持
    this.walk(data);
  }

  // 遍历对象所有属性,递归劫持
  walk(data) {
    if (!data || typeof data !== 'object') {
      return;
    }
    // 遍历对象属性
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key]);
    });
  }

  // 定义响应式属性(核心)
  defineReactive(obj, key, val) {
    const that = this;
    // 递归劫持子属性(如 data.obj.name)
    this.walk(val);
    // 为每个属性创建专属 Dep
    const dep = new Dep();

    // 劫持 get/set
    Object.defineProperty(obj, key, {
      enumerable: true, // 可枚举
      configurable: true, // 可配置
      // 读取属性时触发:收集依赖
      get() {
        // 收集依赖(Dep.target 是当前 Watcher)
        dep.depend();
        return val;
      },
      // 修改属性时触发:通知依赖更新
      set(newVal) {
        if (newVal === val) return;
        val = newVal;
        // 新值如果是对象,需要递归劫持
        that.walk(newVal);
        // 通知所有订阅者更新
        dep.notify();
      }
    });
  }
}

export default Observer;

Watcher.js(订阅者)

Watcher 类:初始化时触发数据的 get,将自身挂载到 Dep.target,被 Dep 收集;数据变化时执行 update 触发视图更新;

import Dep from './Dep.js';

// 订阅者:连接数据和视图,接收 Dep 通知并更新视图
class Watcher {
  /**
   * @param {Object} vm MVVM 实例
   * @param {String} expr 数据表达式(如 'name'/'obj.age')
   * @param {Function} cb 回调函数(更新视图)
   */
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    // 存储旧值(用于对比更新)
    this.oldVal = this.getOldVal();
  }

  // 获取旧值(触发 get,收集依赖)
  getOldVal() {
    // 将当前 Watcher 挂载到 Dep.target
    Dep.target = this;
    // 解析表达式并获取值(如 'obj.age' → vm.obj.age),获取数据会被 Object.defineProperty 的 get 劫持
    const oldVal = this.getVal(this.vm, this.expr);
    // 清空 Dep.target,避免重复收集
    Dep.target = null;
    return oldVal;
  }

  // 解析表达式,获取对应数据(支持多层属性)
  getVal(vm, expr) {
    return expr.split('.').reduce((data, key) => data[key], vm.$data);
  }

  // 更新方法(Dep 通知时调用)
  update() {
    // 获取新值
    const newVal = this.getVal(this.vm, this.expr);
    // 新旧值不同时执行回调(更新视图)
    if (newVal !== this.oldVal) {
      this.oldVal = newVal;
      this.cb(newVal);
    }
  }
}

export default Watcher;

Compiler.js(模板解析)

Compiler 类:解析模板中的指令和插值表达式,初始化视图,创建 Watcher 绑定数据与视图;处理 v-model 实现双向绑定;

import Watcher from './Watcher.js';

// 模板解析:解析指令/插值表达式,初始化视图,创建 Watcher
class Compiler {
  constructor(vm) {
    this.vm = vm;
    this.el = vm.$el;
    // 编译模板
    this.compile(this.el);
  }

  // 编译模板(核心)
  compile(el) {
    const childNodes = el.childNodes;
    // 遍历所有子节点
    Array.from(childNodes).forEach(node => {
      // 元素节点(如 <div>、<input>)
      if (this.isElementNode(node)) {
        this.compileElement(node);
      }
      // 文本节点(包含 {{}})
      else if (this.isTextNode(node)) {
        this.compileText(node);
      }

      // 递归编译子节点
      if (node.childNodes && node.childNodes.length) {
        this.compile(node);
      }
    });
  }

  // 编译元素节点(解析指令,如 v-model)
  compileElement(node) {
    // 获取所有属性
    const attrs = node.attributes;
    Array.from(attrs).forEach(attr => {
      const attrName = attr.name;
      // 判断是否是指令(以 v- 开头)
      if (this.isDirective(attrName)) {
        // 提取指令名(如 v-model → model)
        const dirName = attrName.slice(2);
        const expr = attr.value; // 指令值(如 'name')
        // 执行对应指令的编译方法
        this[dirName](node, expr);
      }
    });
  }

  // 编译 v-model 指令
  model(node, expr) {
    // 初始化视图:设置 input 值
    this.update(node, expr, 'model');

    // 监听 input 事件,实现双向绑定
    node.addEventListener('input', e => {
      const newVal = e.target.value;
      // 更新 data 数据(触发 set,通知 Watcher)
      this.setVal(this.vm, expr, newVal);
    });
  }

  // 编译文本节点(解析 {{}})
  compileText(node) {
    // 提取 {{}} 中的表达式(如 {{name}} → name)
    const reg = /\{\{(.+?)\}\}/;
    const textContent = node.textContent;
    if (reg.test(textContent)) {
      const expr = RegExp.$1.trim();
      // 替换 {{}} 为真实数据,初始化视图
      node.textContent = textContent.replace(reg, this.getVal(this.vm, expr));

      // 创建 Watcher,数据变化时更新文本
      new Watcher(this.vm, expr, newVal => {
        node.textContent = textContent.replace(reg, newVal);
      });
    }
  }

  // 通用更新方法(初始化视图 + 创建 Watcher)
  update(node, expr, dir) {
    // 获取更新方法(如 modelUpdater)
    const updateFn = this[`${dir}Updater`];
    // 初始化视图
    updateFn && updateFn(node, this.getVal(this.vm, expr));
    // 创建 Watcher,数据变化时更新视图
    new Watcher(this.vm, expr, newVal => {
      updateFn && updateFn(node, newVal);
    });
  }

  // model 指令更新器(更新 input 值)
  modelUpdater(node, value) {
    node.value = value;
  }

  // 解析表达式,获取数据(同 Watcher)
  getVal(vm, expr) {
    return expr.split('.').reduce((data, key) => data[key], vm.$data);
  }

  // 设置 data 数据(支持多层属性)
  setVal(vm, expr, value) {
    expr.split('.').reduce((data, key, index, arr) => {
      if (index === arr.length - 1) {
        data[key] = value;
      }
      return data[key];
    }, vm.$data);
  }

  // 判断是否是指令(v- 开头)
  isDirective(attrName) {
    return attrName.startsWith('v-');
  }

  // 判断是否是元素节点
  isElementNode(node) {
    return node.nodeType === 1;
  }

  // 判断是否是文本节点
  isTextNode(node) {
    return node.nodeType === 3;
  }
}

export default Compiler;

MVVM.js(核心入口)

MVVM 类:入口类,做数据代理 (简化访问)、初始化 ObserverCompiler

import Observer from './Observer.js';
import Compiler from './Compiler.js';

// MVVM 核心类:整合所有模块
class MVVM {
  constructor(options) {
    // 挂载配置项
    this.$options = options || {};
    this.$data = options.data || {};
    this.$el = typeof options.el === 'string' 
      ? document.querySelector(options.el) 
      : options.el;

    // 1. 数据代理:将 data 挂载到 vm 上(可通过 vm.name 访问 vm.$data.name)
    this._proxyData(this.$data);

    // 2. 数据劫持:监听 data 变化
    new Observer(this.$data);

    // 3. 模板解析:解析指令/插值表达式
    new Compiler(this);
  }

  // 数据代理:简化 data 访问
  _proxyData(data) {
    Object.keys(data).forEach(key => {
      // 此处写成 this 是为了实现「vm.xxx」直接访问「vm._data.xxx」
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return data[key];
        },
        set(newVal) {
          if (newVal === data[key]) return;
          data[key] = newVal;
        }
      });
    });
  }
}

export default MVVM;

测试页面 index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Vue2 MVVM 手写实现</title>
</head>
<body>
  <div id="app">
    <h1>{{name}}</h1>
    <h2>{{age}}</h2>
    <h3>{{obj.gender}}</h3>
    <input type="text" v-model="name">
    <input type="text" v-model="obj.gender">
  </div>

  <script type="module">
    import MVVM from './mvvm/MVVM.js';
    
    // 初始化 MVVM
    const vm = new MVVM({
      el: '#app',
      data: {
        name: 'Vue2 MVVM',
        age: 8,
        obj: {
          gender: '男'
        }
      }
    });

    // 测试数据更新(触发视图更新)
    setTimeout(() => {
      vm.name = '手写 MVVM 成功!';
      vm.obj.gender = '女';
    }, 2000);
  </script>
</body>
</html>

总结

  1. Vue2 MVVM 的核心是数据劫持 (Object.defineProperty) + 依赖收集 + 观察者模式,实现数据驱动视图;
  2. 核心模块分工:Observer 监听数据、Dep 管理依赖、Watcher 触发更新、Compiler 解析模板、MVVM 整合所有模块;
  3. 双向绑定的本质是:v-model 既通过 Watcher 绑定数据 → 视图,又通过输入事件绑定视图 → 数据;
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

你好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 整体分析
  2. 2. 完整代码实现
    1. 2.1. 目录结构
    2. 2.2. Dep.js(依赖收集器)
    3. 2.3. Observer.js(数据劫持)
    4. 2.4. Watcher.js(订阅者)
    5. 2.5. Compiler.js(模板解析)
    6. 2.6. MVVM.js(核心入口)
    7. 2.7. 测试页面 index.html
  3. 3. 总结