手写 vue 2的双向绑定

Vue 的双向绑定是其核心特性,它允许数据与视图自动同步:数据变化时视图更新,视图变化时数据也更新。下面我将基于 Vue 2 的实现原理(使用 Object.defineProperty),手把手带你实现一个简易版双向绑定。整个过程基于​​数据劫持​ ​和​​发布-订阅模式​ ​,核心包括 Observer(监听数据)、Dep(依赖管理)、Watcher(订阅更新)和 Compile(模板解析)四个部分。

🔧 核心原理概述

  • ​数据劫持​ :通过 Object.defineProperty拦截数据的读取(getter)和设置(setter),在 setter 中检测变化并通知更新。
  • ​发布-订阅模式​ :每个响应式属性都有一个 Dep实例来管理依赖它的 Watcher。数据变化时,Dep通知所有 Watcher执行更新函数。
  • ​模板编译​ :Compile解析模板中的指令(如 v-model),初始化视图并绑定事件监听器,将数据与 DOM 元素关联。

⚙️ 分步实现代码

以下是一个最小化实现,包含关键类和方法。代码基于 Vue 2 风格,但进行了简化以便理解。

1. ​​实现 Observer(数据劫持)​​

Observer递归遍历数据对象,为每个属性添加 getter/setter:

javascript 复制代码
// 数据劫持函数
function defineReactive(obj, key, val) {
  const dep = new Dep(); // 每个属性对应一个 Dep 实例
  // 递归处理嵌套对象
  if (typeof val === 'object' && val !== null) {
    observe(val);
  }
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log(`访问属性 ${key}: ${val}`);
      // 依赖收集:如果当前有 Watcher,则添加到 Dep
      if (Dep.target) {
        dep.addSub(Dep.target);
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      console.log(`更新属性 ${key}: 从 ${val} 变为 ${newVal}`);
      val = newVal;
      // 通知所有订阅者更新
      dep.notify();
    }
  });
}

// 遍历对象属性
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) return;
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}

2. ​​实现 Dep(依赖管理)​​

Dep负责收集依赖(Watcher)并在数据变化时通知它们:

javascript 复制代码
class Dep {
  constructor() {
    this.subs = []; // 存储 Watcher 实例
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
Dep.target = null; // 全局变量,指向当前正在计算的 Watcher

3. ​​实现 Watcher(订阅者)​​

Watcher是 Observer 和 Compile 之间的桥梁,在数据变化时触发更新:

kotlin 复制代码
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb; // 更新回调函数
    Dep.target = this; // 设置当前 Watcher
    this.vm[this.key]; // 触发 getter,收集依赖
    Dep.target = null; // 收集完成后重置
  }
  update() {
    this.cb.call(this.vm, this.vm[this.key]);
  }
}

4. ​​实现 Compile(模板编译)​​

Compile解析 DOM 模板,处理指令并绑定事件:

ini 复制代码
function compile(el, vm) {
  const nodes = el.children;
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i];
    // 递归处理子节点
    if (node.children.length) {
      compile(node, vm);
    }
    // 处理 v-model 指令(双向绑定)
    if (node.hasAttribute('v-model')) {
      const key = node.getAttribute('v-model');
      node.value = vm[key]; // 初始化视图
      // 数据 -> 视图:创建 Watcher,数据变化时更新 input 值
      new Watcher(vm, key, (newVal) => {
        node.value = newVal;
      });
      // 视图 -> 数据:监听 input 事件
      node.addEventListener('input', (e) => {
        vm[key] = e.target.value;
      });
    }
    // 处理文本插值(如 {{ message }})
    if (node.nodeType === 3 && /{{(.*)}}/.test(node.textContent)) {
      const key = RegExp.$1.trim();
      node.textContent = vm[key]; // 初始化
      new Watcher(vm, key, (newVal) => {
        node.textContent = newVal;
      });
    }
  }
}

5. ​​整合为 MiniVue 类​​

将以上部分组合成一个简易 Vue 实例:

kotlin 复制代码
class MiniVue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    // 数据劫持
    observe(this.$data);
    // 代理数据:支持直接通过 vm.message 访问(而非 vm.$data.message)
    this._proxyData();
    // 编译模板
    compile(document.querySelector(options.el), this);
  }
  _proxyData() {
    Object.keys(this.$data).forEach(key => {
      Object.defineProperty(this, key, {
        get: () => this.$data[key],
        set: (newVal) => { this.$data[key] = newVal; }
      });
    });
  }
}

💻 完整使用示例

创建一个 HTML 文件测试上述代码:

xml 复制代码
<div id="app">
  <input type="text" v-model="message">
  <p>{{ message }}</p>
</div>

<script>
  // 此处插入以上所有 JavaScript 代码(Observer、Dep、Watcher、Compile、MiniVue)
  const vm = new MiniVue({
    el: '#app',
    data: { message: 'Hello, Vue!' }
  });
</script>
  • ​​效果​ ​:在输入框输入时,<p>标签内容会实时同步。

  • ​​关键流程​​:

    • ​数据 -> 视图​ :修改 vm.message时,触发 setter → Dep.notify() → Watcher.update() → 更新 DOM。
    • ​视图 -> 数据​ :输入框触发 input事件 → 修改 vm.message→ 触发 setter → 循环上述流程。

⚠️ 注意事项与局限性

  • ​Vue 2 的局限​ :Object.defineProperty无法检测新增属性(需用 Vue.set)或数组索引变化(需重写数组方法)。Vue 3 已改用 Proxy解决这些问题。
  • ​简化版缺陷​:本例未实现虚拟 DOM、组件系统等,实际 Vue 更复杂(如 Diff 算法优化性能)。
  • ​扩展建议​ :可添加事件指令(如 v-on)和计算属性,原理类似(通过 Watcher 依赖追踪)。

通过这个手写实现,你能更深入理解 Vue 的响应式本质。实际开发中推荐直接使用 Vue 框架,但掌握原理有助于调试和优化。

相关推荐
非优秀程序员6 小时前
第04章 邮箱大战,163 与 QQ 的暗战|《互联网产品恩仇录》
java·vue.js·架构
tryCbest13 小时前
Vue系列——第 15 篇:内置组件全家桶——Transition、Teleport、Suspense、KeepAlive
前端·vue.js
徐小夕18 小时前
开源Vector-Graph-RAG:向量+图谱双引擎,打破纯向量RAG的效果天花板
前端·vue.js·github
FungLeo21 小时前
成为全栈·Vue 管理后台篇·Vue3 + Vite + Naive UI:后台工程基座怎么搭
前端·vue.js·ui·typescript·vite·前端工程化·biome
FungLeo21 小时前
成为全栈·Vue管理后台篇·<scriptsetup>:把页面拆成可读、可测的 Vue 组件
前端·javascript·vue.js·typescript·前端组件化·composition api·vue sfc
软件技术NINI2 天前
基于SpringBoot和Vue的新能源汽车销售推荐系统
java·vue.js·spring boot·后端·汽车·毕业设计·毕设
招风的黑耳2 天前
SpringBoot+Vue+MySQL 民宿在线预定平台 完整可运行版(源码+文档)
vue.js·spring boot·mysql
10share3 天前
基于大模型的 i18n 方案:把中文原文当 Key,轻松搞定多语言维护
前端·vue.js
啾咪orJIUMI3 天前
Vue 3 + 高德地图封装实战:点位、区域、Loca 流光与 Vue 信息窗体
前端·vue.js
FRing033 天前
vue2干货总结8(路由模块封装、router-link、声明式导航、路由重定向、编程式导航)
开发语言·前端·vue.js·vue2