从源码到架构
约 2041 字大约 7 分钟
2025-11-22
响应式的最小可用模型
详情
// 响应式系统核心实现
// 当前正在执行的副作用函数
let activeEffect = null;
// 依赖映射表:target -> key -> Set<effect>
// 用于存储每个响应式属性的所有依赖函数
const targetMap = new WeakMap();
/**
* 依赖收集:将当前正在执行的 effect 添加到依赖集合中
* @param {Object} target - 目标对象
* @param {string} key - 属性名
*/
function track(target, key) {
// 如果没有正在执行的 effect,说明不在 effect 函数中,不需要收集依赖
if (!activeEffect) return;
// 获取或创建 target 的依赖映射
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 获取或创建 key 的依赖集合
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
// 将当前 effect 添加到依赖集合中
deps.add(activeEffect);
console.log(`📝 收集依赖: ${key} -> effect函数`);
}
/**
* 依赖触发:当数据变化时,执行所有依赖该数据的 effect
* @param {Object} target - 目标对象
* @param {string} key - 属性名
*/
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (!deps) return;
console.log(`🚀 触发更新: ${key} 变化,执行 ${deps.size} 个依赖函数`);
// 执行所有依赖该属性的 effect
deps.forEach(effect => {
effect();
});
}
/**
* 创建响应式对象
* @param {Object} target - 目标对象
* @returns {Proxy} 响应式代理对象
*/
function reactive(target) {
return new Proxy(target, {
// 读取属性时,收集依赖
get(target, key) {
track(target, key);
return target[key];
},
// 设置属性时,触发更新
set(target, key, value) {
const oldValue = target[key];
target[key] = value;
// 只有值真正改变时才触发更新
if (oldValue !== value) {
trigger(target, key);
}
return true;
}
});
}
/**
* 副作用函数:用于执行需要响应式更新的代码
* @param {Function} fn - 要执行的函数
*/
function effect(fn) {
// 将当前函数设置为活跃的 effect
activeEffect = fn;
// 执行函数,此时会触发 getter,从而收集依赖
fn();
// 执行完毕后,清除活跃的 effect
activeEffect = null;
}
// ==================== 使用示例 ====================
// 创建响应式数据
const state = reactive({
count: 0,
name: 'Vue',
age: 3
});
effect(()=>{
console.log(state.count);
})
setInterval(()=>{
state.count++;
},1000);像架构师一样思考
Vue 最大的特点就是它能实现 视图 (render) 与数据之间的关联
拦截数据
Object.defineProperty
在对象上定义访问器属性(accessor descriptor)用于拦截对某个属性的读取与写入。Vue 2 的响应式正是基于这个机制实现的。
Object.defineProperty(obj, propName, descriptor)descriptor 可以是:
- 数据描述符(data descriptor):
{ value, writable, enumerable, configurable } - 访问器描述符(accessor descriptor):
{ get: function, set: function, enumerable, configurable }
重要字段说明:
get:读取时调用,无参数,返回值为属性值set:写入时调用,参数为新值enumerable:是否可枚举(for...in/Object.keys)configurable:是否允许删除或再次修改该属性描述符writable:数据描述符中是否可被赋值(与 accessor 描述符互斥)
示例(最小实现)
const obj = {};
let _val = 1;
Object.defineProperty(obj, 'x', {
get() {
console.log('读取 x');
return _val;
},
set(v) {
console.log('设置 x ->', v);
_val = v;
},
enumerable: true,
configurable: true
});
console.log(obj.x); // 触发 get
obj.x = 5; // 触发 set优缺点
优点:兼容 IE9+(相对广),语义清晰。
缺点:
- 只能拦截已存在的属性(不能捕获新增属性或删除属性)。
- 无法直接拦截数组索引赋值(
arr[0] = ...),需额外覆盖数组方法。 - 为嵌套对象需要递归遍历并为每一项都 defineProperty(初始化成本高)。
- 不易实现对 Map/Set 的拦截。
Vue2 相关要点
Vue2 在初始化时会深度遍历对象并对每个属性做 defineReactive(即 Object.defineProperty),并通过覆盖数组变异方法(push/pop/shift/unshift/splice/sort/reverse)来处理数组变更。
Proxy
ES6 提供的 Proxy 可以拦截对象的所有操作(“元操作”):读取、写入、删除、枚举、属性描述符查询、函数调用等。Vue 3 基于这个实现响应式。
创建方式与 handler 参数(常用 trap)
const proxy = new Proxy(target, handler);handler 常用 trap 详细介绍(含签名与示例):
get(target, prop, receiver)- 作用:拦截属性读取操作(如
proxy.foo)。 - 示例:
const proxy = new Proxy({a: 1}, { get(target, prop, receiver) { console.log('读取属性', prop); return target[prop]; } }); proxy.a; // 控制台打印:读取属性 a
- 作用:拦截属性读取操作(如
set(target, prop, value, receiver)- 作用:拦截属性设置操作(如
proxy.foo = 123),返回布尔值,表示赋值是否成功。 - 示例:
const proxy = new Proxy({}, { set(target, prop, value, receiver) { console.log(`设置${prop}为${value}`); target[prop] = value; return true; // 返回 true 表示赋值成功 } }); proxy.x = 10; // 控制台打印:设置x为10
- 作用:拦截属性设置操作(如
has(target, prop)- 作用:拦截
in操作符(如'foo' in proxy)。 - 示例:
const proxy = new Proxy({a: 1}, { has(target, prop) { console.log(`判断${prop}是否存在`); return prop in target; } }); 'a' in proxy; // 控制台打印:判断a是否存在
- 作用:拦截
deleteProperty(target, prop)- 作用:拦截
delete操作(如delete proxy.foo)。 - 示例:
const proxy = new Proxy({a: 1}, { deleteProperty(target, prop) { console.log(`删除属性${prop}`); return delete target[prop]; } }); delete proxy.a; // 控制台打印:删除属性a
- 作用:拦截
ownKeys(target)- 作用:拦截对象自身属性的读取,比如
Object.keys()、for...in、Object.getOwnPropertyNames()、Reflect.ownKeys()。 - 示例:
const proxy = new Proxy({a: 1, b: 2}, { ownKeys(target) { console.log('获取所有属性键'); return Reflect.ownKeys(target); } }); Object.keys(proxy); // 控制台打印:获取所有属性键
- 作用:拦截对象自身属性的读取,比如
getOwnPropertyDescriptor(target, prop)- 作用:拦截属性描述符的读取(如
Object.getOwnPropertyDescriptor(proxy, 'foo'))。 - 示例:
const proxy = new Proxy({a: 1}, { getOwnPropertyDescriptor(target, prop) { console.log(`获取属性描述符:${prop}`); return Object.getOwnPropertyDescriptor(target, prop); } }); Object.getOwnPropertyDescriptor(proxy, 'a'); // 控制台打印:获取属性描述符:a
- 作用:拦截属性描述符的读取(如
defineProperty(target, prop, descriptor)- 作用:拦截属性定义(如
Object.defineProperty(proxy, 'foo', {...}))。 - 示例:
const proxy = new Proxy({}, { defineProperty(target, prop, descriptor) { console.log(`定义属性${prop}`); return Object.defineProperty(target, prop, descriptor); } }); Object.defineProperty(proxy, 'b', {value: 5}); // 控制台打印:定义属性b
- 作用:拦截属性定义(如
apply(target, thisArg, args)- 作用:拦截函数调用(仅 target 是函数时有效,如
proxy(...args))。 - 示例:
const fn = function(x) { return x*2; } const proxy = new Proxy(fn, { apply(target, thisArg, args) { console.log('函数被调用', args); return target.apply(thisArg, args); } }); proxy(3); // 控制台打印:函数被调用 [3]
- 作用:拦截函数调用(仅 target 是函数时有效,如
construct(target, args)- 作用:拦截
new proxy(...args)实例化操作(仅 target 是构造函数时有效)。 - 示例:
function Foo(x) { this.x = x; } const proxy = new Proxy(Foo, { construct(target, args) { console.log('构造函数被调用', args); return new target(...args); } }); new proxy(10); // 控制台打印:构造函数被调用 [10]
- 作用:拦截
通常与 Reflect 一起用,以保持默认行为和内部一致性:
set(target, prop, value, receiver) {
return Reflect.set(target, prop, value, receiver);
}示例(基础)
const obj = {a:1};
const p = new Proxy(obj, {
get(t,k,rec){
console.log('get', k);
return Reflect.get(t,k,rec);
},
set(t,k,v,rec){
console.log('set', k, '->', v);
return Reflect.set(t,k,v,rec);
}
});
console.log(p.a);
p.b = 2; // 新增属性会被拦截优缺点
优点:
- 能拦截新增/删除属性,数组索引,
in,for...in,Object.keys等多种操作。 - 默认情况下,Proxy 只会代理被包裹的表层对象,不会自动对其嵌套的子对象进行深层代理。但一个常见模式是:在
get拦截器里,只有当访问到某个子对象时,才为它创建并返回一个新的 Proxy,这种方式被称为懒代理(按需代理子对象),从而实现对对象的动态深层代理,而不用一开始就递归遍历所有嵌套属性。 - 能自然代理 Map/Set/数组/函数等复杂结构。
缺点:
- 无法在旧浏览器(IE)上运行(无法 polyfill)。
- Proxy 的 trap 需要遵守引擎内部的不变性(invariant):例如
getOwnPropertyDescriptor返回不可配置但原对象实际可配置会抛错。 - debug/堆栈可能更复杂。
注意点(关键参数语义)
set必须返回 boolean。若返回false且严格模式,会抛出 TypeError。get的receiver通常用于处理继承/getter 情况,直接Reflect.get可避免原型链问题。- 若要保留目标对象的一致行为,优先使用
Reflect系列调用。
get / set(语法糖)
在对象字面量或类中直接声明访问器,比 defineProperty 更简洁。背后同样是定义访问器描述符。
class Person {
constructor(name){
this._name = name;
}
get name() {
console.log('读取 name');
return this._name;
}
set name(v) {
console.log('设置 name');
this._name = v;
}
}
const p = new Person('A');
console.log(p.name);
p.name = 'B';优点:语法更清晰,适合类封装。
缺点:和 defineProperty 一样,只能作用于已声明的属性(不能拦截后期新增属性),且同样不能直接解决数组索引问题。