-
Notifications
You must be signed in to change notification settings - Fork 4
Description
Vue.js的响应系统
类似Vue.js的响应式框架都会通过某种手段实现响应系统,从而实现当数据变更时自动更新试图: data => ui。
Vue.js 2.x的响应系统通过Object.defineProperty
实现,所以后加的属性没有响应能力。Vue.js 3.x的响应系统使用Proxy
实现,并发布可独立使用的包@vue/reactivity
。
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
TypeScript 7 154 115 1225
Qt Linguist 1 0 0 52
-------------------------------------------------------------------------------
SUM: 8 154 115 1277
-------------------------------------------------------------------------------
Vue.js 3.x的响应系统模块在仓库的packages/reactivity
,包含7个主要文件,大概1200行代码。
这篇内容主要实现一个类似的简易响应系统,会对理解基于Proxy
的响应系统和阅读@vue/reactivty
有些帮助。
响应系统的主要功能
响应系统的主要功能很简单:当响应对象target
的属性A变更时,依赖A的函数fn被自动调用。
使用示例
// source
import { reactive, effect } from '@vue/reactivity';
const user = {
name: '张三',
age: 10,
};
const userProxy = reactive(user);
// fn依赖name,city三个属性
effect(() => console.log(`${userProxy.name}: 居住在 ${userProxy.city}`));
// 修改属性name
userProxy.name = '李四';
## output
## 因为fn也需要响应初始数据, 所以通过effect()注册时会主动调用一次fn。
张三: 居住在 undefined
李四: 居住在 undefined
// 修改属性age
userProxy.age = 11;
# 无输出,响应函数fn未被调用,因为fn不依赖age属性。
简述下示例代码:
- 使用
reactive()
创建user的代理对象userProxy
。 - 使用
effect()
注册响应函数fn
,fn依赖属性name
,city
。 - 修改
userProxy
的name
属性,fn
被自动调用。
实现
触发函数
响应系统核心功能是:当响应对象target
的属性A变更时,依赖A的函数fn被自动调用。那么很自然可以想到以如下结构存储响应对象、属性和被调函数fn:
// WeakMap<object, Map<string, Set<Function>>>
const targetFnMap = new WeakMap();
targetFnMap
是WeakMap,key是响应对象target
本身,value是另一个map:depsMap
,depsMap
存储属性名和被调函数。depsMap
存储属性名和被调函数,key是属性名,value是依赖此属性的函数集合。
那么触发回调的操作就是:
- 根据
target
和变更的属性名key
,从targetFnMap
中获取回调函数集合fns
。 - 依次执行函数集合
fns
中的回调函数。
// reactive.js
function trigger(target, key) {
const depsMap = targetFnMap.get(target);
if (!depsMap) {
return;
}
const fns = depsMap.get(key);
if (!fns) {
return;
}
fns.forEach(fn => fn());
}
当target
的属性变更时使用trigger()
执行回调。
监控变更
接下来实现监测target
的属性变更。这里使用Proxy
实现。为防止重复生成proxy,使用WeakMap存储target
和proxy
的对应关系:
// WeakMap<target, proxy>
const reactiveMap = new WeakMap();
属性变更操作有两种:1、设置属性; 2、使用delete
删除属性。所以需要实现set()
和deleteProperty()
捕获器。
// reactive.js
export function reactive(target) {
const existProxy = reactiveMap.get(target);
if (existProxy) {
return existProxy;
}
const proxy = new Proxy(target, handlers);
reactiveMap.set(target, proxy);
return proxy;
}
const handlers = {
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
// target不是receiver本身时不触发回调。
if (reactiveMap.get(target) === receiver) {
trigger(target, key);
}
return res;
},
deleteProperty(target, key) {
const hasOwn = Object.prototype.hasOwnProperty.call(target, key);
const res = Reflect.deleteProperty(target, key);
// 删除自身属性时触发回调
if (hasOwn && res) {
trigger(target, key);
}
return res;
}
};
set()
捕获器:target是原型链上的对象时不触发回调。deleteProperty()
捕获器:只成功删除target
自身属性时触发回调。
构建依赖
还差最后一部分:构建依赖关系,即targetFnMap
。
通常回调函数fn
通过以下方式依赖target
的属性:
- 读取属性:
targetProxy.name
。 - 使用
in
操作符判断是否包含属性:if ('name' in targetProxy)
。 - 遍历属性:使用
Object.keys()
,Object.getOwnPropertyNames()
等遍历属性。
所以可以在fn
执行时通过get()
、has()
、ownKeys()
捕获器构建依赖关系。
因为fn
也需要响应初始数据,所以通过effect()
注册时会主动调用一次fn()
,此时是构建依赖的时机。这里存在的问题是捕获器执行时并不知道fn
,所以通过一个变量activeFn
记录当前被调用的fn
。
let activeFn;
export function effect(fn) {
try {
activeFn = fn;
fn();
} finally {
activeFn = null;
}
}
const handlers = {
set(target, key, value, receiver) {
// ...
},
deleteProperty(target, key) {
// ...
},
has(target, key) {
track(target, key);
return Reflect.has(target, key);
},
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key);
return res;
}
};
- 使用
effect()
注册回调时将activeFn
设置为当前fn
,然后执行fn()
。 - 如果
fn
中依赖了target
的某些属性,则会执行相应的捕获器。 - 在捕获器中记录依赖关系。
记录依赖关系的函数track()
也比较简单:将依赖target
key
属性的函数fn
记录到targetFnMap
中。
function track(target, key) {
if (!activeFn) {
return;
}
let depsMap = targetFnMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetFnMap.set(target, depsMap);
}
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
if (!deps.has(activeFn)) {
deps.add(activeFn);
}
}
由于使用effect()
注册时可能会有嵌套,如:
effect(() => {
console.log(`${userProxy.name}: ${userProxy.age} 岁,居住在 ${userProxy.city}`);
effect(() => {
console.log(`at (${pointProxy.x}, ${pointProxy.y})`)
});
console.log(`you are, ${userProxy.age}`)
});
所以需要一个栈activeFnStack
保存当前的fn
,并通过压退栈操作保证activeFn
的正确性,effect()
修改如下:
// reactive.js
let activeFn;
let activeFnStack = [];
export function effect(fn) {
try {
activeFnStack.push(fn);
activeFn = fn;
fn();
} finally {
activeFnStack.pop()
activeFn = activeFnStack[activeFnStack.length -1];
}
return effect;
}
最后实现ownKeys
捕获器。遍历target
属性的依赖比较特殊: fn
依赖全部属性。可以使用一个特殊的key
表示全部依赖的情况,为了避免名字冲突可以使用Symbol
。
// reactive.js
const ITERATE_KEY = Symbol('');
const handlers = {
ownKeys(target) {
track(target, ITERATE_KEY);
return Reflect.ownKeys(target);
}
};
trigger()
也要执行ITERATE_KEY
回调。
// reactive.js
function trigger(target, key) {
const depsMap = targetFnMap.get(target);
if (!depsMap) {
return;
}
const fns = new Set();
const normalFns = depsMap.get(key);
const iterFns = depsMap.get(ITERATE_KEY);
if (normalFns) {
normalFns.forEach(fn => fns.add(fn));
}
if (iterFns) {
iterFns.forEach(fn => fns.add(fn));
}
fns.forEach(fn => fn());
}
完整代码。
使用示例:
import { reactive, effect, targetFnMap } from './reactive.js'
const user = {
name: '张三',
age: 10,
};
const userProxy = reactive(user);
effect(() => {
const keys = Object.keys(userProxy);
console.log(keys.map(key => (`${key} = ${userProxy[key]}`)).join(', '));
});
effect(() => console.log(`${userProxy.name}: ${ 'city' in userProxy ? '有常住地' : '无常住地'}`));
userProxy.name = '李四';
userProxy.age = 11;
userProxy.city = '北京';
output:
name = 张三, age = 10
张三: 无常住地
name = 李四, age = 10
李四: 无常住地
name = 李四, age = 11
李四: 有常住地
name = 李四, age = 11, city = 北京
深度响应
由于Proxy
只代理直接属性,所以当前实现只是浅响应
,对于如下示例是无效的,因为address
不是响应对象。
// test.js
import { reactive, effect } from './reactive.js';
const user = {
name: '张三',
age: 10,
address: {
city: '北京',
},
};
const userProxy = reactive(user);
effect(() => {
console.log(userProxy.address.city);
});
userProxy.address.city = '上海';
回调函数执行console.log(userProxy.address.city)
时会访问address
属性,触发get()
捕获器,所以可以在get()
捕获器中判断属性address
是否为object
,如果是则将其变为响应对象,从而实现深度响应
。
const handlers = {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key);
// 属性是object,将其变为响应对象 实现深度响应。
if (res !== null && typeof res === 'object') {
return reactive(res);
}
return res;
}
};
@vue/reactivity
实现了更丰富的功能,比如:
- 实现了4种可响应对象:
reactive
,shallowReactive
,readonly
,shallowReadonly
。 - 对
Array
以及集合Map
,Set
,WeakMap
,WeakSet
的支持。 - 更健壮的边界和特殊值处理。
- ...
本示例只是个简陋的demo,用于介绍基于Proxy
的响应系统的基本原理。
-- EOF --