手写 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 算法,我们可以继续展开。

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