手写 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 框架,但掌握原理有助于调试和优化。

相关推荐
武天4 小时前
vue 的双向绑定原理
vue.js
武天4 小时前
手写 vue3 的双向绑定
vue.js
XXX-X-XXJ5 小时前
Vue Router完全指南 —— 从基础配置到权限控制
前端·javascript·vue.js
云和数据.ChenGuang5 小时前
vue钩子函数调用问题
前端·javascript·vue.js
咖啡の猫5 小时前
Vue内置指令与自定义指令
前端·javascript·vue.js
哆啦A梦158813 小时前
搜索页面布局
前端·vue.js·node.js
_院长大人_14 小时前
el-table-column show-overflow-tooltip 只能显示纯文本,无法渲染 <p> 标签
前端·javascript·vue.js
SevgiliD14 小时前
el-table中控制单列内容多行超出省略及tooltip
javascript·vue.js·elementui