【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 代码,更能提升你的架构思维。

相关推荐
LuckySusu3 小时前
【vue篇】Vue 插槽(Slot)完全指南:内容分发的艺术
前端·vue.js
LuckySusu3 小时前
【vue篇】前端架构三巨头:MVC、MVP、MVVM 全面对比
前端·vue.js
开心不就得了3 小时前
css、dom 性能优化方向
前端·性能优化
道可到3 小时前
一个属性,让无数前端工程师夜不能寐
前端
闲云S3 小时前
Lit开发:字体图标的使用
前端·web components·icon
我是天龙_绍3 小时前
uniapp 个人中心页面开发指南
前端
刘永胜是我3 小时前
解决React热更新中"process is not defined"错误:一个稳定可靠的方案
前端·javascript
星链引擎3 小时前
开发者深度版(面向技术人员 / 工程师)
前端
_大学牲3 小时前
Flutter 之魂 GetX🔥(一)从零了解状态管理
前端·程序员