从零实现 Vue 响应式机制:带你吃透依赖收集与更新原理

一、为什么需要响应式?

通过电商网站购物车案例,演示传统 DOM 操作 vs Vue 自动更新的效率对比:

html 复制代码
<!-- 传统方式 -->
<div id="cart">数量:0</div>
<button onclick="updateCart()">+1</button>

<script>
let count = 0
function updateCart() {
    count++
    document.getElementById('cart').innerText = `数量:${count}`
}
</script>

<!-- Vue 方式 -->
<template>
    <div>数量:{{ count }}</div>
    <button @click="count++">+1</button>
</template>

二、响应式系统架构全景

Vue2 响应式系统架构

#### **核心流程说明**

  1. 数据劫持(初始化阶段)

    • 当组件声明数据(data() 返回的对象)时,Vue2 通过 Object.defineProperty 遍历对象的每个属性,为其定义 gettersetter
    • 目的:拦截属性的访问(读)和修改(写),分别用于依赖收集和派发更新。
  2. 依赖收集(读操作触发)

    • 当模板或计算属性中访问数据属性(如 {{ msg }})时,会触发属性的 getter
    • 此时,Vue2 会将当前正在渲染的组件的 Watcher(称为 activeWatcher)收集到该属性对应的 dep(依赖集合)中。
    • 关键点 :每个属性对应一个 dep,每个 dep 存储所有依赖该属性的 Watcher
  3. 派发更新(写操作触发)

    • 当属性值被修改时,触发 setter,此时 Vue2 会遍历该属性的 dep 列表,通知所有 Watcher 执行 update 方法。
    • Watcherupdate 方法会将组件标记为需要重新渲染,并通过虚拟 DOM 的 diff 算法更新真实 DOM。
  4. 关键角色

    • Watcher:与组件渲染函数绑定的监听器,负责在数据变化时触发组件更新。
    • dep :每个属性的依赖集合,本质是一个 Set,存储所有依赖该属性的 Watcher

Vue3 响应式系统架构

### **核心流程解析**

  1. 响应式对象创建

    • 通过 reactiveref等 API 创建响应式对象,本质是用 Proxy 代理目标对象。
    • Proxy 拦截范围 :包括属性访问修改删除等操作。
  2. 依赖收集(track 过程)

当访问响应式对象的属性(触发 get 拦截)时,调用 track 函数收集依赖:

  • activeEffect :全局变量,记录当前正在执行的副作用函数(如组件的渲染函数、watch 回调、计算属性的 getter 等)。
  • 其核心目标是:将"当前活跃的副作用函数"与"被访问的属性"建立依赖关系。
  • targetMap :结构如下:
    • target:被代理的响应式对象。
    • key:对象的属性名。
    • effects:依赖该属性的 effect 集合(避免重复收集)。
js 复制代码
targetMap = {
  target1: {
    count: Set([effect1, effect2])
  }
}
  1. 派发更新(trigger 过程)

当修改响应式对象的属性(触发 set 拦截)时,调用 trigger 函数触发更新:

  • targetMap 中获取该属性对应的所有 effect
  • 通过 调度器(scheduler 批量处理 effect 的执行,避免频繁更新导致性能损耗。
  • 调度机制 :默认将 effect 加入微任务队列(基于 Promise.thenqueueMicrotask),在同一事件循环末尾合并执行。

三 minVue

基于vue2写了一个小小的demo,只支持单模版({{test}})和v-model的处理

1. 依赖管理器 Dep

js 复制代码
class Dep {
  constructor() {
    this.subs = []; // 当前属性对应的所有 Watcher 实例
  }

  addSub(sub) {
    this.subs.push(sub); // 收集依赖
  }

  notify() {
    this.subs.forEach(sub => sub.update()); // 通知所有依赖更新
  }
}

Dep.target = null; // 当前被收集的 Watcher

2.观察者 Watcher

js 复制代码
class Watcher {
  constructor(data, key, cb) {
    this.data = data;
    this.key = key;
    this.cb = cb;

    Dep.target = this; // 当前 watcher 设为全局 target
    this.value = data[key]; // 触发 getter,从而添加到 dep.subs
    Dep.target = null;
  }

  update() {
    const newVal = this.data[this.key];
    if (newVal !== this.value) {
      this.value = newVal;
      this.cb(newVal); // 执行视图更新逻辑
    }
  }
}

3. 响应式系统 observe

js 复制代码
observe(data) {
  if (!data || typeof data !== 'object') return;

  Object.keys(data).forEach(key => {
    let value = data[key];
    const dep = new Dep();

    Object.defineProperty(data, key, {
      get() {
        if (Dep.target) {
          dep.addSub(Dep.target);
        }
        return value;
      },
      set(newVal) {
        if (newVal !== value) {
          value = newVal;
          dep.notify();
        }
      }
    });

    this.observe(value); // 递归子对象
  });
}

4. 模版编译器

js 复制代码
compile() {
  const el = document.querySelector(this.$options.el);
  this.compileNode(el);
}

compileNode(node) {
  if (node.nodeType === 1) {
    Array.from(node.attributes).forEach(attr => {
      if (attr.name === 'v-model') {
        this.bindModel(node, attr.value);
      }
    });
    node.childNodes.forEach(child => this.compileNode(child));
  } else if (node.nodeType === 3) {
    this.bindText(node);
  }
}

5. 处理 bindTextbindModel

bindText:绑定插值表达式

js 复制代码
bindText(node) {
  const regex = /\{\{(.*?)\}\}/g;
  const matches = node.textContent.match(regex);
  if (matches) {
    const key = matches[0].slice(2, -2).trim();
    node.textContent = this._data[key];
    new Watcher(this._data, key, (newVal) => {
      node.textContent = newVal;
    });
  }
}

bindModel:绑定输入框 v-model

js 复制代码
bindModel(node, key) {
  node.value = this._data[key];
  node.addEventListener('input', (e) => {
    this._data[key] = e.target.value;
  });
  new Watcher(this._data, key, (newVal) => {
    node.value = newVal;
  });
}

完整代码

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <title>MiniVue Demo</title>
</head>
<body>
  <!-- 示例模板 -->
  <div id="app">
    <input v-model="message">
    <p>{{ message }}</p>
    <p>{{ status }}</p>
    <div>Counter: {{ counter }}</div>
  </div>

  <script>
// ================================================================================
// 依赖管理器 (Dep)
// 功能:管理某个数据属性的所有 Watcher,当数据变化时通知所有 Watcher 更新
// ================================================================================
class Dep {
  constructor() {
    this.subs = []; // 存储所有依赖(即 Watcher 实例)
  }
  // 添加依赖(Watcher)
  addSub(sub) {
    this.subs.push(sub);
  }
  // 通知所有依赖更新
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}

// 全局变量,用于暂存当前正在处理的 Watcher
Dep.target = null;

// ================================================================================
// 观察者 (Watcher)
// 功能:连接数据和视图,当数据变化时触发回调函数更新视图
// ================================================================================
class Watcher {
  constructor(data, key, cb) {
    this.data = data;
    this.key = key;
    this.cb = cb;

    // 触发getter,将当前 Watcher 实例添加到 Dep 中
    Dep.target = this;
    this.value = data[key];
    Dep.target = null;
  }
  // 更新函数
  update() {
    const newVal = this.data[this.key];
    if (newVal !== this.value) {
      this.value = newVal;
      this.cb(newVal); // 调用回调函数更新视图
    }
  }
}

// ================================================================================
// 核心 MiniVue 类
// ================================================================================
class MiniVue {
  constructor(options) {
    this.$options = options;       // 用户传入的配置项
    this._data = options.data();   // 初始化数据(注意:data 是函数)
    this.observe(this._data);      // 将数据变为响应式
    this.compile();                // 编译模板
  }

  // ------------------------------------------------------------------------------
  // 响应式系统:通过 Object.defineProperty 实现数据劫持
  // ------------------------------------------------------------------------------
  observe(data) {
    if (!data || typeof data !== 'object') return;

    Object.keys(data).forEach(key => {
      let value = data[key];
      const dep = new Dep(); // 每个属性对应一个 Dep 实例

      // 劫持属性的 getter/setter
      Object.defineProperty(data, key, {
        get() {
          // 收集依赖:如果有 Watcher 正在读取此属性,将其添加到 Dep 中
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set(newVal) {
          if (newVal === value) return;
          value = newVal;
          dep.notify(); // 数据变化时通知所有 Watcher 更新
        }
      });

      // 递归处理嵌套对象
      this.observe(value);
    });
  }

  // ------------------------------------------------------------------------------
  // 模板编译:解析 DOM 中的指令和插值表达式
  // ------------------------------------------------------------------------------
  compile() {
    const el = document.querySelector(this.$options.el);
    this.compileNode(el);
  }

  compileNode(node) {
    // 处理元素节点(如 div、input)
    if (node.nodeType === 1) {
      // 解析指令(只处理 v-model)
      console.log(Array.from(node.attributes), 'node.attributes');
      Array.from(node.attributes).forEach(attr => {
        if (attr.name === 'v-model') {
          this.bindModel(node, attr.value);
        }
      });
      // 递归处理子节点
      node.childNodes.forEach(child => this.compileNode(child));

    // 处理文本节点(如 {{ message }})
    } else if (node.nodeType === 3) {
      this.bindText(node);
    }
  }

  // 绑定文本插值({{ ... }})
  bindText(node) {
    const regex = /\{\{(.*?)\}\}/g;
    const matches = node.textContent.match(regex);
    // 不处理复杂表达式,只处理简单的 {{ message }}
    if (matches) {
      const key = matches[0].slice(2, -2).trim(); // 提取属性名(如 "message")
      // 初始化文本内容
      node.textContent = this._data[key];
      
      // 创建 Watcher,当数据变化时更新文本
      new Watcher(this._data, key, (newVal) => {
        node.textContent = newVal;
      });
    }
  }

  // 绑定 v-model 指令(双向绑定)
  bindModel(node, key) {
    // 初始化输入框的值
    node.value = this._data[key];
    
    // 监听 input 事件,更新数据
    node.addEventListener('input', (e) => {
      this._data[key] = e.target.value;
    });
    
    // 创建 Watcher,当数据变化时更新输入框的值
    new Watcher(this._data, key, (newVal) => {
      node.value = newVal;
    });
  }
}

// ================================================================================
// 使用示例
// ================================================================================
const app = new MiniVue({
  el: '#app', // 挂载目标
  data: () => ({
    message: 'Hello miniVue!',
    status: 'I am sad',
    counter: 0
  })
});

// 测试:修改数据,观察视图是否更新
setTimeout(() => {
  app._data.status = 'I am happy';
}, 2000);
setInterval(() => {
  app._data.counter++;
}, 1000);
  </script>
</body>
</html>

小小测试了一下,v-model和定时器触发对应依赖导致页面更新都是没有问题的

相关推荐
步行cgn15 分钟前
Vue 中的数据代理机制
前端·javascript·vue.js
风吹头皮凉4 小时前
vue实现气泡词云图
前端·javascript·vue.js
萌萌哒草头将军4 小时前
🚀🚀🚀尤雨溪推荐的这个库你一定要知道!轻量⚡️,优雅!
前端·vue.js·react.js
BillKu5 小时前
Vue3 + Vite 中使用 Lodash-es 的防抖 debounce 详解
前端·javascript·vue.js
chengchong_cc5 小时前
海康对接摄像头
java·vue.js
逝缘~6 小时前
小白学Pinia状态管理
前端·javascript·vue.js·vscode·es6·pinia
光影少年6 小时前
vite原理
前端·javascript·vue.js
源猿人7 小时前
文化与代码的交汇:OpenCC 驱动的中文语系兼容性解决方案
前端·vue.js
難釋懷7 小时前
Vue非单文件组件
前端·vue.js
克里斯前端7 小时前
vue在打包的时候能不能固定assets里的js和css文件名称
javascript·css·vue.js