跳到主要内容

仿 reactive 实现

reactive 函数可以把一个普通对象转换成“可监听”的对象。

const newObj = reactive(object);

最简单的实现(使用 Proxy API 实现)

function reactive<T extends object>(object: T) {
return new Proxy<T>(object, {
set(obj: any, prop, val) {
obj[prop] = val;
console.log(obj, prop, val);
return obj[prop];
},

get(obj: any, prop) {
console.log(obj, prop);
return obj[prop];
}
});
}

const newObj = reactive({ age: 17 });
// 重新赋值时就会触发 setter 函数
newObj.age = 18;

effect 函数

effect 函数可以传入一个回调函数作为参数,当可监听的对象属性发生变化时,如果这个属性在 effect 回调函数中被访问了,则该回调函数会被执行。

effect(() => {
// obj.name 值一旦发生变化,effect 回调函数就会被执行
console.log(obj.name);
});

初版实现逻辑:声明一个全局的 callbacks 数组用于收集所有的 effect 中的回调函数,当在 reactive setter 函数执行时就遍历 callbacks 数组执行所有的 effect 回调函数。

const callbacks: Function[] = [];

function effect(callback: Function) {
callbacks.push(callback);
}

function reactive<T extends object>(object: T) {
return new Proxy<T>(object, {
set(obj: any, prop, val) {
obj[prop] = val;
// 遍历执行 callbacks 函数
callbacks.forEach(cb => cb());
return obj[prop];
},
});
}

// test:
const obj = { a: 1, b: 2, };
const newObj = reactive(obj);
effect(() => {
console.log('hello~', obj);
});
// 更改属性值
newObj.a = 2;

上面代码实现很粗糙,无论 effect 函数有种有没有依赖,只要改变了可观察对象的属性值时,effect 回调函数就会全部被执行。

我们希望做到只执行有变更的依赖的对应的回调函数该怎么做?

可以把 callbacks 数组改造成一个 map map 的 key 就是被监听的对象,value 则还是一个 map,其键是对象属性名,值则是 effect 回调函数集合。ts 类型如下:

const callbacks = new Map<object, Map<string | Symbol, Set<Function>>>();

然后再声明一个全局变量 usedReactives 用于存储依赖,其类型如下:

/** 收集对象属性依赖,数组第 0 位是对象本身,第 1 位是属性名 */
let usedReactives: [object, string | Symbol][] = [];

改造后的 effect 函数内部会先执行一次 callback 函数,callback 函数中如果有访问依赖属性时就会触发 getter 函数然后收集依赖。代码如下:

const callbacks = new Map<object, Map<string | Symbol, Set<Function>>>();

let usedReactives: [object, string | Symbol][] = [];

function effect(callback: Function) {
usedReactives = []; // 先把依赖清空,
// 执行回调函数,其内部访问依赖属性时就会触发 getter 函数然后收集依赖
callback();
}

function reactive<T extends object>(object: T) {
return new Proxy<T>(object, {
get(obj: any, prop) {
// 收集依赖
usedReactives.push([obj, prop]);
return obj[prop];
}
});
}

当执行完 callback 回调函数后,依赖也已经收集完毕,然后再收集 callback 函数(用 callbacks 这个 map 去收集),最后当被监听的对象属性发生变化时就在 setter 函数中找到被监听对象的属性对应的 callback 集合,然后执行。

代码如下:

function effect(callback: Function) {
usedReactives = []; // 先把依赖清空,
// 执行回调函数,其内部访问依赖属性时就会触发 getter 函数然后收集依赖
callback();

// 遍历收集到的依赖,往 callbacks map 中存 effect callback
for (let [obj, key] of usedReactives) {
// 如果找不到依赖对象对应的 map,则生成一个新的 map
if (!callbacks.has(obj)) {
callbacks.set(obj, new Map());
}
// 如果找到了 map 但找不到 key 对应的回调函数集合,则生成新的集合
if (!callbacks.get(obj)?.has(key)) {
callbacks.get(obj)?.set(key, new Set());
}
// 往集合里 push 新的 effect 回调函数
callbacks.get(obj)?.get(key)?.add(callback);
}
}

function reactive<T extends object>(object: T) {
return new Proxy<T>(object, {
set(obj: any, prop, val) {
obj[prop] = val;
// 拿到属性对应的 effect 回调函数集合,然后执行
const cbs = callbacks.get(obj)?.get(prop);
cbs?.forEach(cb => cb());
return true;
},

get(obj: any, prop) {
// 收集依赖
usedReactives.push([obj, prop]);
return obj[prop];
}
});
}

// test:
const obj = { a: 1, b: 2 };
const newObj = reactive(obj);
// 先执行一遍回调函数,触发 getter 函数,收集 'a' 属性对应的 effect callback
effect(() => {
console.log('hello~', newObj.a);
});
// 赋值时会触发 setter 函数,执行 `a` 属性收集到的 effect callback
newObj.a = 10;

需要注意的是,如果上面测试代码中的 obj 是一个比较复杂的对象,例如:

const obj = { group: { name: 'ming' }, age: 18 };

obj.group.name 的值发生改变时是监听不到的,当 getter 函数触发时,obj[prop] 如果还是一个对象时,则还需要对其做一下代理包装。代码如下:

// key 是原始的对象,value 是代理对象,用于缓存代理对象
const activities = new Map<object, object>();

function reactive<T extends object>(object: T) {
// 如果命中缓存则直接使用缓存中的代理对象
if (activities.has(object)) {
return activities.get(object) as T;
}
const proxy: T = new Proxy<T>(object, {
set(obj: any, prop, val) {
obj[prop] = val;
const cbs = callbacks.get(obj)?.get(prop);
cbs?.forEach(cb => cb());
return true;
},

get(obj: any, prop) {
// 收集依赖
usedReactives.push([obj, prop]);
// 如果属性值还是一个对象,则还需要进一步代理
if (typeof obj[prop] === 'object') {
return reactive(obj[prop]);
}
return obj[prop];
}
});
activities.set(object, proxy);
return proxy;
}

// test:
const obj = { group: { name: 'ming' }, age: 18 };
const newObj = reactive(obj);

effect(() => {
console.log('hello~', newObj.group.name);
});

newObj.group.name = '';

与 DOM 结合使用

<input id="ipt" />

<script>
const input = document.getElementById('ipt');
const obj = reactive({ value: '' });
effect(() => {
input.value = obj.value;
});
obj.value = '1234'; // input 框的值就会变成 1234
</script>


<!-- 双向绑定 -->
<input id="ipt2" />
<script>
const obj = reactive({ value: '' });
const input2 = document.getElementById('ipt2');
effect(() => {
input2.value = obj.value;
});

effect(() => {
input2.addEventListener('input', event => {
obj.value = event.target.value;
});
});
</script>

Proxy 与 Object.defineProperty 对比

  • defineProperty 会直接修改原始对象,而 Proxy 会在原始对象之上创建一个代理层;
  • Proxy 是对整个对象的代理,而 defineProperty 只能代理某个属性;
  • 对象上新增属性,Proxy 可以监听到,defineProperty 不能;
  • 数组新增修改,Proxy 可以监听到,defineProperty 不能;
  • 若对象内部属性要全部递归代理,Proxy 可以只在触发 getter 的时候递归,而 definePropery 需要一次完成所有递归,性能比 Proxy 差;
  • Proxy 不兼容 IE,Object.defineProperty 不兼容 IE8 及以下;
  • Proxy 可以拦截更多的操作,比如 has、deleteProperty、ownKeys 等;