手写 vue3 的双向绑定

Vue 3 的双向绑定是其响应式系统的核心,它通过 ​​Proxy API​ ​ 实现数据劫持,结合​​依赖收集​ ​与​​触发更新​ ​机制,自动同步数据与视图。下面我将手把手带你实现一个简化版 Vue 3 双向绑定,涵盖响应式数据、依赖追踪和模板指令解析(如 v-model)。整个过程基于以下关键架构:

组件 职责 核心实现
​响应式系统​ 数据劫持与依赖管理 使用 Proxy拦截对象操作,通过 tracktrigger管理依赖
​副作用处理​ 关联数据变化与视图更新 effect函数注册副作用,在数据变化时重新执行
​模板编译​ 解析指令并绑定事件 递归遍历 DOM,处理 v-model等指令,建立数据与 DOM 的联系

🔧 核心代码实现

我们将实现一个迷你版 Vue 类(如 MyVue),包含响应式处理、依赖收集和模板编译。完整代码如下:

xml 复制代码
<!-- index.html -->
<div id="app">
  <input v-model="message" />
  <p>{{ message }}</p>
</div>

<script type="module">
  import { MyVue } from './my-vue.js';
  
  const app = new MyVue({
    el: '#app',
    data: {
      message: 'Hello Vue 3!'
    }
  });
</script>
ini 复制代码
// my-vue.js
// 1. 依赖管理:存储全局依赖关系
const targetMap = new WeakMap(); // 弱引用,避免内存泄漏
let activeEffect = null;         // 当前活跃的副作用函数

// 2. 依赖收集(track)与触发更新(trigger)
function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }
  dep.add(activeEffect); // 将当前副作用函数添加到依赖集合
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => effect()); // 执行所有关联的副作用函数
  }
}

// 3. 响应式数据创建(核心:Proxy)
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key); // 读取属性时收集依赖
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (oldValue !== value) {
        trigger(target, key); // 属性值变化时触发更新
      }
      return result;
    }
  });
}

// 4. 副作用函数(effect)
function effect(fn) {
  activeEffect = fn;
  fn(); // 执行函数,触发 getter,从而收集依赖
  activeEffect = null;
}

// 5. 迷你 Vue 类(整合响应式与模板编译)
export class MyVue {
  constructor(options) {
    this.$el = document.querySelector(options.el);
    this.$data = reactive(options.data); // 数据响应化
    this.compile(this.$el);              // 编译模板
  }

  // 模板编译方法
  compile(node) {
    if (node.nodeType === Node.TEXT_NODE) {
      // 处理文本插值 {{ }}
      const text = node.textContent;
      const regex = /{{\s*(\w+)\s*}}/;
      if (regex.test(text)) {
        const key = RegExp.$1.trim();
        effect(() => {
          node.textContent = this.$data[key]; // 数据变化时更新文本
        });
      }
    } else if (node.nodeType === Node.ELEMENT_NODE) {
      // 处理元素节点和指令
      const attributes = node.attributes;
      for (let attr of attributes) {
        if (attr.name === 'v-model') {
          const key = attr.value;
          effect(() => {
            node.value = this.$data[key]; // 数据 -> 视图
          });
          node.addEventListener('input', (e) => {
            this.$data[key] = e.target.value; // 视图 -> 数据
          });
        }
      }
      // 递归处理子节点
      if (node.childNodes.length) {
        node.childNodes.forEach(child => this.compile(child));
      }
    }
  }
}

⚙️ 关键原理解析

  1. ​响应式数据(reactive函数)​​:

    • 使用 Proxy代理目标对象,拦截 getset操作。
    • get拦截器​ :当读取属性时,调用 track函数,将当前活跃的副作用函数(activeEffect)收集到该属性的依赖集合中(通过 targetMap结构管理)。
    • set拦截器​ :当修改属性时,调用 trigger函数,通知所有依赖该属性的副作用函数重新执行,从而更新视图。
  2. ​依赖管理(tracktrigger)​​:

    • targetMap是一个 WeakMap,键是响应式对象,值是一个 Map(记录对象属性与依赖集合的映射)。
    • 依赖集合使用 Set存储副作用函数,确保唯一性。
    • 这种结构允许精确知道哪个对象的哪个属性被哪些副作用函数依赖。
  3. ​副作用函数(effect)​​:

    • effect接收一个函数(如渲染函数或更新函数),执行时设置 activeEffect为该函数。
    • 当函数内部访问响应式数据时,触发 get拦截器,完成依赖收集。
    • 数据变化时,通过 trigger重新执行所有依赖函数,实现自动更新。
  4. ​模板编译(compile方法)​​:

    • 递归遍历 DOM 树,处理文本节点({{ }}插值)和元素节点(如 v-model指令)。

    • 对于 v-model

      • input元素绑定 input事件,在用户输入时更新数据(视图 → 数据)。
      • 使用 effect函数建立响应式关联,数据变化时自动更新 input的值(数据 → 视图)。

💡 进阶优化与注意事项

  • ​数组与嵌套对象​ :上述简化版未处理数组和深层对象。Vue 3 的 reactive会递归代理嵌套对象,并通过重写数组方法(如 push)确保响应式。
  • ​性能优化​ :真实 Vue 3 使用异步更新队列(如 nextTick)批量处理多次数据变化,避免重复渲染。
  • ​Ref 与 Reactive 区别​reactive适用于对象,而基本类型需使用 ref(通过 .value访问)。
  • ​解构响应式对象​ :直接解构会失去响应性,需使用 toRefs转换。

💎 总结

通过以上代码,我们实现了一个 Vue 3 双向绑定的核心骨架:​​Proxy 数据劫持​ ​ + ​​依赖收集/触发​ ​ + ​​模板指令解析​ ​。虽然简化版未覆盖全部边界情况(如组件、计算属性),但它清晰揭示了响应式系统的本质:​​在数据读取时收集依赖,在数据修改时通知依赖更新​ ​。这种设计使 Vue 3 在性能与功能上显著优于 Vue 2(基于 Object.defineProperty)。 希望这个手写实现帮助你深入理解 Vue 3 的响应式魔法!如需进一步探讨虚拟 DOM 或 Diff 算法,我们可以继续展开。

相关推荐
我有满天星辰13 小时前
Vue 3 响应式双雄:ref 与 reactive 完全指南
前端·javascript·vue.js
leoZ2311 天前
Claude 驱动的全栈开发:Spring Boot + Vue 的 BS 架构实践
vue.js·spring boot·架构
荒诞英雄1 天前
从 17MB Vendor 大包到按需加载:Vite 多入口 Vue 组件库首屏优化实践
vue.js·vite
jsonbro1 天前
手把手带你实现发布订阅模式
前端·vue.js·设计模式
Cobyte1 天前
23.Vue Vapor 组件 attrs 的实现
前端·javascript·vue.js
斯特凡今天也很帅1 天前
xml页面可以打开,不报错,但是显示数据的地方也不显示,我是怎么解决的
xml·vue.js
柯克七七1 天前
给老项目加了 TypeScript,本来只想自己爽,结果全公司代码审查标准被我抬高了
前端·vue.js·typescript
自然 醒1 天前
前端如何实现在线预览office常见文件功能?
前端·vue.js
肉肉不吃 肉1 天前
前端调试跨域如何解决
前端·vue.js
卓怡学长1 天前
w268基于springboot + vue 物流系统
java·数据库·vue.js·spring boot·spring·intellij-idea