前言
还记得第一次使用 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函数的框架,但是目前还有些问题:
- 同一个对象代理多次,会返回不同的代理对象,这样性能上带来不必要的开销。
js
const originalObj = { name: 'Vue', version: 3 }; // 第一次调用 reactive
const proxy1 = reactive(originalObj); // 第二次调用 reactive(传入同一个对象)
const proxy2 = reactive(originalObj); // 验证两个代理是同一个实例
console.log(proxy1 === proxy2); // false
可以通过缓存代理对象解决此类问题,采用WeakMap来缓存代理对象,key为target,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不再引用时会自动清理。
- 当已经被
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都不会产生多层代理对象嵌套。
在Vue3中get和set包裹的对象是抽离到一个单独的文件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.js中track函数中实现依赖收集
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 等打下了基础。