Vue3 响应式原理:从零实现 Reactive

前言

还记得第一次使用 Vue 时的那种惊艳吗?数据变了,视图自动更新,就像魔法一样!但作为一名有追求的前端开发者,我们不能只停留在"会用"的层面,更要深入理解背后的原理。

今天,我将带你从零实现一个 Vue3 的响应式系统,手写代码不到 200 行,却能覆盖核心原理。读完本文,你将彻底明白:

  • 🤔 为什么 Vue3 放弃 Object.defineProperty 选择 Proxy?
  • 🔥 依赖收集和触发更新的精妙设计
  • 🎯 数组方法的重写背后隐藏的智慧
  • 💡 Vue3 响应式相比 Vue2 的性能优势

什么是响应式?

简单来说,响应式是当数据变化时,自动执行依赖数据的代码

js 复制代码
const state = reactive({ count: 0 });
effect(() => {
  console.log(`count值变化:${state.count}`);
});
state.count++; // count值变化:1
state.count++; // count值变化:2

vue2和vue3响应式区别

特性 vue2(Object.defineProperty) vue3(proxy)
对象新增属性 $set api实现响应式 直接支持
对象删除属性 $delete api 实现响应式 直接支持
数组拦截 改写数组原型方法 原生支持,重新包装
性能 递归遍历所有属性 懒代理,访问时才代理

综上所述Proxy的优势非常的明显,这就是Vue3选择重构响应式系统的根本原因。

手写实现:从零构建响应式

1. 项目结构

text 复制代码
├── reactive.js           // reactive 核心
├── effect.js             // 副作用管理
├── baseHandler.js        // Proxy 处理器
├── arrayInstrumentations.js // 数组方法重写
├── utils.js              // 工具函数
└── index.js              // 入口文件

响应式入口

我们先从reactive函数着手,使用过vue3应该对reactive并不陌生。此函数接收一个对象,然后返回一个代理对象。

js 复制代码
// reactive.js
export function reactive(target) {
  // 判断target是否一个对象
  if (!isObject(target)) {
    return target;
  }
  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      // 后续收集依赖
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      if (oldValue != value) {
        // 后续触发更新
      }
    }
  })
  return proxy;
}

目前已经搭建了reactive函数的框架,但是目前还有些问题:

  1. 同一个对象代理多次,会返回不同的代理对象,这样性能上带来不必要的开销。
js 复制代码
const originalObj = { name: 'Vue', version: 3 }; // 第一次调用 reactive 
const proxy1 = reactive(originalObj); // 第二次调用 reactive(传入同一个对象)
const proxy2 = reactive(originalObj); // 验证两个代理是同一个实例
console.log(proxy1 === proxy2); // false

可以通过缓存代理对象解决此类问题,采用WeakMap来缓存代理对象,keytarget,value为代理对象。

js 复制代码
// 缓存代理对象,避免重复代理
const reactiveMap = new WeakMap();
export function reactive(target) {
  // 判断target是否一个对象
  if (!isObject(target)) {
    return target;
  }
  // 将target是否已经代理过,如果代理则返回缓存的代理对象。
  const existsProxy = reactiveMap.get(target);
  if (existsProxy) {
    return existsProxy;
  }
  const proxy = new Proxy(target, {
    /* 此处暂时省略 */
  })
  // 缓存代理对象
  reactiveMap.set(target, proxy);
  return proxy;
}

💡 提示

在上述代码中之所以采用WeakMap主要考虑key是一个对象并且WeakMap可以当target不再引用时会自动清理。

  1. 当已经被reactive处理后,再次调用reactive时,又被代理。
js 复制代码
const originalObj = { count: 1 }; // 第一次创建响应式对象 
const proxy1 = reactive(originalObj);
const proxy2 = reactive(proxy1); // 将代理对象再次代理

Vue3的源码中通过__v_isReactive标记来判断:

js 复制代码
export const ReactiveFlags = {
  IS_REACTIVE: "__v_isReactive",
};
export function reactive(target) {
  // 判断target是否一个对象
  if (!isObject(target)) {
    return target;
  }
  // 避免重复代理
  if (target[ReactiveFlags.IS_REACTIVE]) {
    return target;
  }
  // 将target是否已经代理过,如果代理则返回缓存的代理对象。
  const existsProxy = reactiveMap.get(target);
  if (existsProxy) {
    return existsProxy;
  }
  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      if (key === ReactiveFlags.IS_REACTIVE) {
        return true;
      }
    },
    /* 此处暂时省略 */
  })
  // 缓存代理对象
  reactiveProxy.set(target, proxy);
  return proxy;
}
  • 当第一次调用reactive时,检查target中是否已经存在__v_isReactive标记,正常情况下是undefined,返回一个Proxy代理对象。
  • 如果将返回的Proxy代理对象,再次调用reactive函数,再次检查__v_isReactive是否存在,将会进入Proxy代理对象的get方法中,进入判断返回true。从而达到无论将相同代理对象调用多少次reactive都不会产生多层代理对象嵌套。

Vue3getset包裹的对象是抽离到一个单独的文件baseHandlers中的,我们也进行相同调整:

js 复制代码
// baseHandlers.js
import { ReactiveFlags } from "./reactive"; 
export const mutableHandlers = {
  get(target, key, receiver) {
    // 1. 响应式标识判断(Vue3 源码标准逻辑)
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true;
    }
    /* 后续实现依赖收集 */
  },

  set(target, key, value, receiver) {
    const oldValue = target[key];
    if (oldValue !== value) {
     // 后续触发更新
    }
  },
};
js 复制代码
// reactive.js
import { mutableHandlers } from "./baseHandler.js";
export const ReactiveFlags = {
  IS_REACTIVE: "__v_isReactive",
};
export function reactive(target) {
  // 判断target是否一个对象
  if (!isObject(target)) {
    return target;
  }
  // 避免重复代理
  if (target[ReactiveFlags.IS_REACTIVE]) {
    return target;
  }
  // 将target是否已经代理过,如果代理则返回缓存的代理对象。
  const existsProxy = reactiveMap.get(target);
  if (existsProxy) {
    return existsProxy;
  }
  const proxy = new Proxy(target, mutableHandlers)
  // 缓存代理对象
  reactiveProxy.set(target, proxy);
  return proxy;
}

副作用管理

Vue3中提供了一个effect函数,接收一个函数,提供给用户获取数据渲染视图,数据变化后再次调用该函数更新视图。effect具体实现如下:

js 复制代码
// 当前响应器
export let activeEffect;
// 清理依赖
export function cleanupEffect(effect) {
  effect.deps.forEach((dep) => {
    dep.delete(effect);
  });
  effect.deps.length = 0;
}
class ReactiveEffect {
  active = true; // 是否激活状态
  deps = []; // 依赖集合数组
  parent = undefined; // 父级effect 处理嵌套effect
  constructor(fn, scheduler) {
    this.fn = fn; // 用户提供的函数
    this.scheduler = scheduler // 调度器(用于computed、watch)
  }
  run() {
    if (!this.active) {
      return this.fn();
    }
    try {
      // 建立effect的父子关系 确保依赖收集的准确性
      this.parent = activeEffect;
      activeEffect = this;
      // 清除旧依赖 避免不必要的更新
      cleanupEffect(this);
      return this.fn();
    } finally {
      // 恢复父级effect
      activeEffect = this.parent;
      this.parent = undefined;
    }
  }
}
export function effect(fn, options = {}) {
  const e = new ReactiveEffect(fn, options.scheduler);
  e.run();
  // 给到用户自行控制响应
  const runner = e.run.bind(e); // 确保this的指向
  runner.effect = e;
  return runner;
}
// 收集依赖函数
export function track(target, key) {}
// 触发依赖
export function trigger(target, key) {}

实现收集依赖

js 复制代码
const state = reactive({ name: 'jim '});
effect(() => {
  document.getElementById('app').innerHTML = `${state.name}`;
})

当调用effect函数时,将会执行用户提供的函数逻辑,如上述代码执行state.name时将会进入代理对象的get方法,该方法中进行依赖收集。即调用track函数。

js 复制代码
// baseHandler.js
import { isObject } from "./utils";
import { ReactiveFlags, reactive } from "./reactive";
import { track } from "./effect";
export const mutableHandlers = {
  get(target, key, receiver) {
    // 响应式标识判断(Vue3 源码标准逻辑)
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true;
    }
    // 收集依赖(所有属性访问都需要追踪)
    track(target, key);
    // 执行原生 get 操作 
    const result = Reflect.get(target, key, receiver);
    // 深层响应式:嵌套对象/数组自动转为响应式(Vue3 懒代理特性)
    if (result && isObject(result)) {
      return reactive(result);
    }

    return result;
  },
  /* set方法在此省略 */
};

effect.jstrack函数中实现依赖收集

js 复制代码
// 当前响应器
export let activeEffect;
export const targetMap = new WeakMap(); //  收集依赖
export function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set())); // Vue3内部是一个Dep类
  }
  trackEffects(dep);
}
export function trackEffects(dep) {
  let shouldTrack = !dep.has(activeEffect);
  if (shouldTrack) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep); // 双向记录
  }
}

收集完毕后的依赖关系结构:

js 复制代码
WeakMap {
  target1: Map {
    key1: Set[effect1, effect2],
    key2: Set[effect3]
  },
  target2: Map { ... }
}

实现触发依赖

当用户对数据进行了修改时,需要根据收集的依赖自动对应执行effect的用户函数。

js 复制代码
state.name = 'tom'

baseHandle.js中调用trigger函数。该函数实现具体的触发依赖

js 复制代码
// baseHandle.js
import { isObject } from "./utils";
import { ReactiveFlags, reactive } from "./reactive";
import { track, trigger } from "./effect";
export const mutableHandlers = {
  /* get方法实现省略 */
  set(target, key, value, receiver) {
    const oldValue = target[key];
    const success = Reflect.set(target, key, value, receiver);
    // 7. 只有值变化且是自身属性时,才触发更新(避免原型链干扰)
    if (success && oldValue !== value) {
      trigger(target, key); // 触发依赖
    }
    return success;
  },
};

effect.js中实现trigger函数的实现

js 复制代码
// 触发依赖
export function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  let dep = depsMap.get(key);
  if (dep) triggerEffects(dep);
}
export function triggerEffects(dep) {
  const effects = [...dep]; // 避免在遍历 Set 过程中修改 Set 本身导致的迭代器异常问题
  effects.forEach((effect) => {
    // 避免无限递归:当前正在执行的effect不再次触发
    if (effect != activeEffect) {
      if (!effect.scheduler) {
        effect.run();
      } else {
        effect.scheduler();
      }
    }
  });
}

对数组响应式处理

Vue3源码中单独一个文件arrayInstrumentations对数组的方法重新包装了一下。我的处理与源码有点不同毕竟是简易版本,但是原理都是一样的

js 复制代码
// arrayInstrumentations.js
import { reactive } from "./reactive";
import { trigger } from "./effect";
import { isArray } from "./utils";

// 需要特殊处理的数组修改方法(Vue3 源码中也是用 Set 存储)
export const arrayInstrumentations = new Set([
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
]);

/**
 * 包装数组修改方法,添加响应式能力
 * @param {string} method - 数组方法名
 * @returns 包装后的函数
 */
function createArrayMethod(method) {
  // 获取原生数组方法
  const originalMethod = Array.prototype[method];

  return function (...args) {
    // 1. 执行原生数组方法(保证原有功能不变)
    const result = originalMethod.apply(this, args);

    // 2. 处理新增元素的响应式转换(push/unshift/splice 可能添加新元素)
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args; // 这两个方法的参数就是新增元素
        break;
      case "splice":
        inserted = args.slice(2); // splice 第三个参数及以后是新增元素
        break;
    }
    // 新增元素转为响应式(递归处理对象/数组)
    if (inserted) {
      inserted.forEach((item) => {
        if (typeof item === "object" && item !== null) {
          reactive(item);
        }
      });
    }

    // 3. 触发依赖更新(Vue3 源码中会触发 length 和对应索引的更新)
    trigger(this, "length");
    return result;
  };
}

// 生成所有包装后的数组方法(键:方法名,值:包装函数)
export const arrayMethods = Object.create(null);
arrayInstrumentations.forEach((method) => {
  arrayMethods[method] = createArrayMethod(method);
});

/**
 * 判断是否是需要拦截的数组方法
 * @param {unknown} target - 目标对象
 * @param {string} key - 属性名/方法名
 * @returns boolean
 */
export function isArrayInstrumentation(target, key) {
  return isArray(target) && arrayInstrumentations.has(key);
}

然后在baseHandler中添加数组情况下的逻辑

js 复制代码
// baseHandler.js
import { isObject } from "./utils";
import { ReactiveFlags, reactive } from "./reactive";
import { track, trigger } from "./effect";
// 引入抽离的数组工具
import { isArrayInstrumentation, arrayMethods } from "./arrayInstrumentations";

export const mutableHandlers = {
  get(target, key, receiver) {
    // 1. 响应式标识判断(Vue3 源码标准逻辑)
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true;
    }
    // 2. 收集依赖(所有属性访问都需要追踪)
    track(target, key);
    // 3. 执行原生 get 操作
    const result = Reflect.get(target, key, receiver);
    // 4. 数组方法拦截:如果是需要处理的数组方法,返回包装后的函数
    if (isArrayInstrumentation(target, key)) {
      // 绑定 this 为目标数组,确保原生方法执行时上下文正确
      return arrayMethods[key].bind(target);
    }
    // 5. 深层响应式:嵌套对象/数组自动转为响应式(Vue3 懒代理特性)
    if (result && isObject(result)) {
      return reactive(result);
    }

    return result;
  },

  set(target, key, value, receiver) {
    const oldValue = target[key];
    const isArrayTarget = Array.isArray(target);
    // 6. 执行原生 set 操作
    const success = Reflect.set(target, key, value, receiver);
    // 7. 只有值变化且是自身属性时,才触发更新(避免原型链干扰)
    if (success && oldValue !== value) {
      // 数组索引设置:触发对应索引和 length 更新(Vue3 源码逻辑)
      if (isArrayTarget && key !== "length") {
        const index = Number(key);
        if (index >= 0 && index < target.length) {
          trigger(target, key); // 触发索引更新
          trigger(target, "length"); // 触发长度更新
          return success;
        }
      }
      // 普通对象/数组 length 设置:触发对应 key 更新
      trigger(target, key);
    }
    return success;
  },
};

完整代码使用示例

js 复制代码
import { reactive, effect } from "./packages/index";
const state = reactive({
  name: "vue",
  version: "3.4.5",
  author: "vue team",
  friends: ["jake", "james"],
});
effect(() => {
  app.innerHTML = `
    <div> Welcome ${state.name} !</div>
    <div> ${state.friends} </div>
  `;
});
setTimeout(() => {
  state.name = "vue3"; 
  state.friends.push("jimmy");
}, 1000);
// 一开始显示:
//    Welcome vue 
//    'jake,james'
// 1秒钟后:
//    Welcome vue3
//    'jake,james,jimmy'

总结

通过这 200 行代码,我们实现了一个完整的 Vue3 响应式系统核心:

  • 响应式代理: 基于 Proxy 的懒代理机制
  • 依赖收集: 精准的 effect 追踪
  • 批量更新: 避免重复执行的调度机制
  • 数组处理: 重写数组方法保持响应性
  • 嵌套支持: 自动的深层响应式转换

完整代码和资源 本文所有代码已开源,包含详细注释和测试用例:

GitHub 仓库:github.com/gardenia83/...

这为我们理解 Vue3 的响应式原理提供了坚实的基础,也为学习更高级的特性如 computed、watch 等打下了基础。

相关推荐
月弦笙音2 小时前
【AI】👉提示词入门基础篇指南
前端·后端·aigc
jason_yang2 小时前
俄罗斯Yandex地图实战
vue.js·api
konh2 小时前
React Native 自定义字体导致 Text / TextInput 文本垂直不居中的终极解决方案
前端·react native
奔赴_向往2 小时前
跨域问题深度剖析:为何CORS设置了还是报错?
前端
daols882 小时前
vxe-table 配置 ajax 加载列表数据,配置分页和查询搜索表单
vue.js·ajax·table·vxe-table
纯爱掌门人2 小时前
别再死磕框架了!你的技术路线图该更新了
前端·架构·前端框架
丁点阳光2 小时前
Ract Router v7:最全基础与高级用法指南(可直接上手)
前端·react.js
~无忧花开~2 小时前
Vue.config.js配置全攻略
开发语言·前端·javascript·vue.js
w***Q3503 小时前
前端跨平台开发工具,Tauri与Electron
前端·javascript·electron