【vue篇】Vue 双向数据绑定原理解析:从 MVVM 到响应式视图

Vue 的 双向数据绑定 是其最核心的特性之一。

你只需在 <input v-model="message"> 中绑定一个数据,输入框的值变化时,message 自动更新;反之,message 变化时,输入框也自动刷新。

这背后,是 数据劫持 + 发布-订阅模式 的精妙结合。

本文将带你从零构建一个简易的 Vue,深入理解 v-model 背后的秘密。


一、双向数据绑定的核心思想

"数据变化 → 视图自动更新;视图交互 → 数据自动同步。"

这正是 MVVM 模式 的精髓:

  • M (Model):数据层(data
  • V (View):视图层(DOM)
  • VM (ViewModel):连接层(Vue 实例)

二、四大核心模块

Vue 的双向绑定由四个关键模块协作完成:

模块 职责
Observer 劫持数据,实现响应式
Compile 编译模板,解析指令
Watcher 订阅者,连接数据与视图
MVVM 入口,整合三大模块

三、模块详解与代码实现

✅ 1. Observer:数据劫持,实现响应式

目标:遍历 data,为每个属性添加 getter/setter

js 复制代码
class Observer {
  constructor(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) {
    // 递归处理嵌套对象
    this.walk(val);

    const dep = new Dep(); // 依赖收集器

    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 依赖收集
        if (Dep.target) {
          dep.depend();
        }
        return val;
      },
      set(newVal) {
        if (newVal === val) return;
        val = newVal;
        // 新值也可能是对象
        this.walk(newVal);
        // 派发更新
        dep.notify();
      }
    });
  }
}

✅ 2. Dep:依赖收集器(发布者)

每个响应式属性都有一个 Dep,管理所有依赖它的 Watcher

js 复制代码
class Dep {
  constructor() {
    this.subs = []; // 存储 Watcher
  }

  // 添加订阅者
  depend() {
    Dep.target && this.subs.push(Dep.target);
  }

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

// 全局唯一 Watcher 标识
Dep.target = null;

✅ 3. Watcher:订阅者,连接数据与视图

WatcherObserver 和 Compile 之间的通信桥梁

js 复制代码
class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr; // 如 'user.name'
    this.cb = cb;
    this.value = this.get(); // 首次获取,触发 getter
  }

  get() {
    Dep.target = this; // 标记当前 Watcher
    // 读取属性,触发 getter,完成依赖收集
    const value = this.getVMVal(this.vm, this.expr);
    Dep.target = null; // 清除
    return value;
  }

  update() {
    const oldVal = this.value;
    const newVal = this.getVMVal(this.vm, this.expr);
    if (newVal !== oldVal) {
      this.cb(newVal); // 触发回调(如更新视图)
    }
  }

  // 辅助方法:从 vm 中读取嵌套属性
  getVMVal(vm, expr) {
    return expr.split('.').reduce((obj, key) => obj[key], vm);
  }
}

✅ 4. Compile:模板编译,解析指令

负责解析模板中的指令(如 {{}}v-model),并绑定更新函数。

js 复制代码
class Compile {
  constructor(el, vm) {
    this.el = document.querySelector(el);
    this.vm = vm;
    this.fragment = this.nodeToFragment(this.el);
    this.compile(this.fragment);
    this.el.appendChild(this.fragment);
  }

  nodeToFragment(el) {
    const fragment = document.createDocumentFragment();
    let child;
    while (child = el.firstChild) {
      fragment.appendChild(child);
    }
    return fragment;
  }

  compile(node) {
    const reg = /\{\{(.*)\}\}/;
    if (node.nodeType === 1) { // 元素节点
      const attrs = node.attributes;
      Array.from(attrs).forEach(attr => {
        const attrName = attr.name;
        const exp = attr.value;
        if (attrName === 'v-model') {
          // 绑定 input 事件
          this.modelUpdater(node, exp);
          node.addEventListener('input', e => {
            const value = e.target.value;
            this.setVMVal(this.vm, exp, value); // 视图 → 数据
          });
          // 创建 Watcher,数据 → 视图
          new Watcher(this.vm, exp, value => {
            node.value = value;
          });
        }
      });
    } else if (node.nodeType === 3 && reg.test(node.nodeValue)) { // 文本节点
      const exp = RegExp.$1.trim();
      const value = this.getVMVal(this.vm, exp);
      node.nodeValue = value;
      // 创建 Watcher
      new Watcher(this.vm, exp, value => {
        node.nodeValue = value;
      });
    }

    // 递归子节点
    if (node.childNodes && node.childNodes.length) {
      node.childNodes.forEach(child => this.compile(child));
    }
  }

  // 辅助方法
  getVMVal(vm, exp) {
    return exp.split('.').reduce((obj, key) => obj[key], vm);
  }

  setVMVal(vm, exp, value) {
    exp.split('.').reduce((obj, key, index, arr) => {
      if (index === arr.length - 1) {
        obj[key] = value;
      }
      return obj[key];
    }, vm);
  }

  modelUpdater(node, exp) {
    const value = this.getVMVal(this.vm, exp);
    node.value = value;
  }
}

✅ 5. MVVM:整合者

js 复制代码
class MVVM {
  constructor(options) {
    this.$el = options.el;
    this.$data = options.data;

    // 代理 this.$data 到 this
    Object.keys(this.$data).forEach(key => {
      this.proxyData(key);
    });

    // 响应式系统
    new Observer(this.$data);
    // 编译模板
    new Compile(this.$el, this);
  }

  proxyData(key) {
    Object.defineProperty(this, key, {
      get() {
        return this.$data[key];
      },
      set(newVal) {
        this.$data[key] = newVal;
      }
    });
  }
}

四、使用示例

html 复制代码
<div id="app">
  <input v-model="message" />
  <p>{{ message }}</p>
</div>
js 复制代码
new MVVM({
  el: '#app',
  data: {
    message: 'Hello MVVM'
  }
});

✅ 运行流程

  1. 初始化

    • Observer 劫持 message
    • Compile 解析模板,发现 v-model{{}}
    • message 创建 Watcher,绑定更新函数。
  2. 数据变化

    js 复制代码
    vm.message = 'New Value';
    • 触发 setterdep.notify()Watcher.update() → 视图更新。
  3. 视图交互

    • 用户在输入框输入;
    • 触发 input 事件 → setVMVal()vm.message 被修改 → 触发 setter → 视图更新。

五、Vue 3 的改进

Vue 3 使用 Proxy 替代 Object.defineProperty,解决了:

  • 无法监听属性新增/删除;
  • 数组索引修改;
  • 初始化性能问题。

但核心思想 "数据劫持 + 发布-订阅" 依然不变。


💡 结语

"双向绑定不是魔法,而是设计模式的优雅组合。"

通过 ObserverCompileWatcherDep 的协作,Vue 实现了:

  • 数据驱动视图
  • 视图反馈数据
  • 开发者零手动 DOM 操作

理解其原理,不仅能写出更高质量的 Vue 代码,更能提升你的架构思维。

相关推荐
恋猫de小郭5 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅11 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606112 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了12 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅12 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅13 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅13 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment13 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅13 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊13 小时前
jwt介绍
前端