最初手动响应的思路
无论vue2还是vue3,做到响应式数据都分为几个步骤,接下一步一步的实现响应式数据。
手动收集依赖和派发通知
实现一个Depend类,可以添加和调用使用对象数据的函数。代码如下:
class Depend {
constructor() {
this.reactiveFns = [];
}
// 添加依赖
addDepend(fn) {
this.reactiveFns.push(fn);
}
// 遍历执行收集的依赖函数
notify() {
this.reactiveFns.forEach(item => item());
}
}
const depend = new Depend();
const obj = { name: 'moon', age: 18 };
function foo1() {
console.log(obj.name, '第一次');
}
function foo2() {
console.log(obj.name, '第二次');
}
depend.addDepend(foo1);
depend.addDepend(foo2);
depend.notify();
//moon 第一次
// moon 第二次
自动派发通知
上面代码,只能手动调用notify方法才能派发通知,接下来让我们自动调用。
vue2使用Object.defineProperty劫持对象
class Depend {
constructor() {
this.reactiveFns = [];
}
// 添加依赖
addDepend(fn) {
this.reactiveFns.push(fn);
}
// 遍历执行收集的依赖函数
notify() {
this.reactiveFns.forEach(item => item());
}
}
const depend = new Depend();
const obj = { name: 'moon', age: 18 };
Object.keys(obj).forEach(key => {
//保留value值
let value = obj[key];
//劫持obj对象
Object.defineProperty(obj, key, {
//get可以监听对象的依赖,即谁使用了对象
get() {
return value;
},
//set可以监听对象属性的改变
set(newValue) {
value = newValue;
depend.notify();
},
});
});
function foo1() {
console.log(obj.name, '第一次');
}
function foo2() {
console.log(obj.name, '第二次');
}
depend.addDepend(foo1);
depend.addDepend(foo2);
obj.name = 'coder';
// coder 第一次
// coder 第二次
vue3使用Proxy构造函数和Reflect内置对象
class Depend {
constructor() {
this.reactiveFns = [];
}
// 添加依赖
addDepend(fn) {
this.reactiveFns.push(fn);
}
// 遍历执行收集的依赖函数
notify() {
this.reactiveFns.forEach(item => item());
}
}
const depend = new Depend();
const obj = { name: 'moon', age: 18 };
const objProxy = new Proxy(obj, {
get(target, key, receiver) {
// 获取对象身上某个属性的值,类似于target[key]
return Reflect.get(target, key, receiver);
},
set(target, key, newValue, receiver) {
// 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
Reflect.set(target, key, newValue, receiver);
depend.notify();
},
});
function foo1() {
console.log(objProxy.name, '第一次');
}
function foo2() {
console.log(objProxy.name, '第二次');
}
depend.addDepend(foo1);
depend.addDepend(foo2);
objProxy.name = 'vue3';
// vue3 第一次
// vue3 第二次
vue2和vue3的区别
- vue2通过Object.defineProperty直接修改obj对象,实际已经将obj的数据描述符改变为存取描述符,vue3是监控Proxy的对象(是obj对象外层的对象),不会修改obj的描述符。
- vue2是遍历obj对象的属性,所以对于新增的属性是无法监听的的,而vue3对于新增的数据依然可以监听。
- Object.defineProperty只有get,set捕获器,而proxy有13种捕获器(比如in,delete操作符都能监听到)能力更强。
- proxy可以监听数组
自动收集依赖
上面的代码可以自动派发通知,但是依赖仍然是手动收集的。接下来完成自动收集,后续代码使用proxy
思路是声明一个全局函数watchFn和一个全局变量reactiveFn,watchFn的参数是函数,然后调用watchFn将参数保存在reactiveFn中,调用参数,然后情况reactiveFn,代码如下:
class Depend {
constructor() {
this.reactiveFns = [];
}
// 添加依赖
addDepend() {
if (reactiveFn) {
this.reactiveFns.push(reactiveFn);
}
}
// 遍历执行收集的依赖函数
notify() {
this.reactiveFns.forEach(item => item());
}
}
// 声明变量
let reactiveFn = null;
// 使用全局函数收集依赖并保存在reactiveFn中
function watchFn(fn) {
reactiveFn = fn;
fn();
reactiveFn = null;
}
const depend = new Depend();
const obj = { name: 'moon', age: 18 };
const objProxy = new Proxy(obj, {
get(target, key, receiver) {
// 获取对象身上某个属性的值,类似于target[key]
depend.addDepend();
return Reflect.get(target, key, receiver);
},
set(target, key, newValue, receiver) {
// 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
Reflect.set(target, key, newValue, receiver);
depend.notify();
},
});
function foo1() {
console.log(objProxy.name, '第一次');
}
function foo2() {
console.log(objProxy.name, '第二次');
}
function foo3() {
console.log(objProxy.age, '第一次');
}
function foo4() {
console.log(objProxy.age, '第二次');
}
watchFn(foo1);
watchFn(foo2);
watchFn(foo3);
watchFn(foo4);
objProxy.name = 'vue3';
moon 第一次
moon 第二次
18 第一次
18 第二次
vue3 第一次
vue3 第二次
18 第一次
18 第二次
代码优化
目前代码可以实现自动收集依赖和自动派发,但是仍存在问题
- 所有的依赖函数都存在同一数组中,一个属性改变其他属性的依赖函数也会被调用。
- 使用数组保存依赖函数会造成添加过依赖的函数重复被添加。
依赖函数存放优化
第一个问题的解决办法是通过数据结构来规划依赖函数的存放,使用WeakMap来存放对象和相应依赖,如图所示
实现代码如下:
const targetMap = new WeakMap();
function getDepend(target, key) {
let map = targetMap.get(target);
if (!map) {
map = new Map();
targetMap.set(target, map);
}
let depend = map.get(key);
if (!depend) {
depend = new Depend();
map.set(key, depend);
}
return depend;
}
重复调用优化
将Depend类中的reactiveFns所使用的的数组改成set即可。 代码如下
class Depend {
constructor() {
this.reactiveFns = new Set();
}
// 添加依赖
addDepend() {
if (reactiveFn) {
this.reactiveFns.add(reactiveFn);
}
}
// 遍历执行收集的依赖函数
notify() {
this.reactiveFns.forEach(item => item());
}
}
完整代码
class Depend {
constructor() {
this.reactiveFns = new Set();
}
// 添加依赖
addDepend() {
if (reactiveFn) {
this.reactiveFns.add(reactiveFn);
}
}
// 遍历执行收集的依赖函数
notify() {
this.reactiveFns.forEach(item => item());
}
}
// 声明变量
let reactiveFn = null;
// 使用全局函数收集依赖并保存在reactiveFn中
function watchFn(fn) {
reactiveFn = fn;
fn();
reactiveFn = null;
}
const obj = { name: 'moon', age: 18 };
const targetMap = new WeakMap();
function getDepend(target, key) {
let map = targetMap.get(target);
if (!map) {
map = new Map();
targetMap.set(target, map);
}
let depend = map.get(key);
if (!depend) {
depend = new Depend();
map.set(key, depend);
}
return depend;
}
const objProxy = new Proxy(obj, {
get(target, key, receiver) {
const depend = getDepend(obj, key);
depend.addDepend();
// 获取对象身上某个属性的值,类似于target[key]
return Reflect.get(target, key, receiver);
},
set(target, key, newValue, receiver) {
// 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
Reflect.set(target, key, newValue, receiver);
const depend = getDepend(obj, key);
depend.notify();
},
});
function foo1() {
console.log(objProxy.name, '第一次');
}
function foo2() {
console.log(objProxy.name, '第二次');
}
function foo3() {
console.log(objProxy.age, '第一次');
}
function foo4() {
console.log(objProxy.age, '第二次');
}
watchFn(foo1);
watchFn(foo2);
watchFn(foo3);
watchFn(foo4);
objProxy.name = 'vue3';
// moon 第一次
// moon 第二次
// 18 第一次
// 18 第二次
// -------------以上是收集依赖时自动调用一次
// vue3 第一次
// vue3 第二次