详解vue2 ,vue3响应式原理
响应式是什么
简单解释就是x有一个初始化的值,有一段代码在别的地方使用了,当x发生变化时,在刚才使用x的地方能够立马发生改变或者重新执行
简单的响应式实现
const obj = {
name: 'jack',
age: '18',
};
obj.name = 'mask';
在上面的代码中,当name发生改变时,在其他的地方使用了name的地方也要立马发生改变,这里需要解决两个问题,怎么知道哪里使用了,怎么通知更新,第一个问题我们称之为收集依赖,第二个问题叫做派发更新。
watchFn(function () {
console.log(obj.name, 'name收集依赖----------');
});
这里通过watch函数来收集依赖,解决的问题是怎么知道哪里使用了,类似于在vue中使用{{}}使用了name属性
// 封装一个响应式的函数
let reactiveFns = []
function watchFn(fn) {
reactiveFns.push(fn)
}
watch函数解决的收集依赖的问题,这里将收集到的依赖放在数组中,在进行派发操作的时候只需要遍历数组中的函数,进行执行即可
reactiveFns.forEach(fn => {
fn()
})
完整代码
let reactiveFns = [];
function watchFn(fn) {
reactiveFns.push(fn);
}
const obj = {
name: 'jack',
age: '18',
};
watchFn(function () {
console.log(obj.name, 'name收集依赖----------');
});
obj.name = 'mask';
reactiveFns.forEach((fn) => {
fn();
});
结果
在上面的简单例子中可以看出在下面修改的name属性,上面使用的name地方也发生改变了,这就是响应式最简单的原理,如果能够理解上面的例子,接下来就可以看完整的响应式原理了
封装收集依赖类
在上面例子中,收集依赖和派发更新时可以封装在一个类中
class Depend {
constructor() {
this.reactiveFns = [] //收集依赖的地方
}
addDepend(reactiveFn) {
this.reactiveFns.push(reactiveFn)//将依赖收集
}
notify() {
this.reactiveFns.forEach(fn => { //遍历依赖,执行依赖
fn()
})
}
}
在使用的时候,可以直接new一下上面的depend类
const depend = new Depend()
function watchFn(fn) {
depend.addDepend(fn) //添加依赖
}
const obj = {
name: 'jack',
age: '18',
};
watchFn(function () {
console.log(obj.name, 'name收集依赖----------');
});
obj.name = 'mask';
depend.notify() //派发更新操作
这样在其他地方使用的时候可以简化大量的代码,
现在还有问题就是,在进行派发更新的时候是手动进行派发的,如果再其他地方也需要进行派发更新的时候就会造成大量的代码冗余,最好是能给一个能自动收集依赖的操作,毕竟能自动绝不手动,接下来就要实现一个自动派发更新的操作
自动监听对象的改变
在js中监听对象的改变可以通过set和get来监听对象的属性变化,在vue2中用的是Object.defineProperty,但是这个属性只能监听对象属性的修改和访问,在进行修改和其他操作就不行了,vue3中用Proxy类来实现对象的监听,接下来先用Proxy来实现一下,下面会用到Proxy和define.Property对比还有Reflect,weakmap,和set在我的其他文章中有解释,可以先去看一下
const objproxy = new Proxy(obj, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
depend.notify();
},
});
Proxy监听的整个对象,set在属性改变时进行监听对象的属性改变,这里直接将派发更新的操作放在set函数中就可以实现自动派发更新的操作了。基本上整个功能实现了一大步,但是还有几个问题没有解决,这里的收集依赖都是放在一整个的数组中,只要有一个属性的值发生了改变其他的依赖也会相应的进行执行,
class Depend {
constructor() {
this.reactiveFns = []; //收集依赖的地方
}
addDepend(reactiveFn) {
this.reactiveFns.push(reactiveFn); //将依赖收集
}
notify() {
this.reactiveFns.forEach((fn) => {
//遍历依赖,执行依赖
fn();
});
}
}
// 封装一个响应式的函数
const depend = new Depend();
function watchFn(fn) {
depend.addDepend(fn);
}
const obj = {
name: 'jack',
age: '18',
};
const objproxy = new Proxy(obj, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver);
depend.notify();
},
});
watchFn(function () {
console.log(objproxy.name, ' name收集依赖----------');
});
watchFn(function () {
console.log(objproxy.age, ' age收集依赖----------');
});
objproxy.name = 'mask';
image.png
类似于这样的在这样的情况下,只改变name的值,但是age的依赖也发生变化了,这不是想要的结果,想要的结果是在当前属性发生变化的时候,收集的依赖只能是当前属性的依赖,派发的更新也是派发当前属性的更新,所以这里需要解决这个问题,需要通过一个结构来收集正确的依赖,派发正确的更新
依赖图
这里用上面的结构来收集正确的依赖,派发正确的更新
将所有的对象放在WeakMap中,单个对象放在Map中,每一个属性的依赖放在一个dep对象中,最后在Map中实现 obj:{key:[dep对象]},这样的数据结构,用这种数据结构的话,又有新的问题了,需要实现一个函数找到正确的依赖,并且返回Dep对象
接下来就是要先实现这个方法
//封装一个收集依赖的getdepend
//收集依赖
//1.封装一个weakmap收集所有的对象
//2.在从weakmap中获取到需要的对象
//3.在用map收集所有的对象属性添加依赖
const targetMap = new WeakMap();//第一步
function getDepend(target, key) { //传入的对象和key的值
let map = targetMap.get(target); //第二步
if (!map) { //没有这个对象的map类的话,先创建一个
map = new Map();
targetMap.set(target, map); //根据target和key设置对象的map
}
//根据key去取出相对应的依赖
let depend = map.get(key);
if (!depend) { //没有depend类先创建dep类,在设置(key, depend);
depend = new Depend();
map.set(key, depend);
}
return depend; //返回找到的key值的dep类
}
现在就可以在进行派发更新之前的先获取到depend,再更据depend进行派发正确的更新了,现在这里完成了正确的派发更新操作,还有一个问题是怎么收集正确的依赖
首先这里实现的watch函数
const depend = new Depend();
function watchFn(fn) {
depend.addDepend(fn);
}
在watch函数中是传入了相应的依赖,现在的问题是怎么把正确的依赖放到相应的key值上,解决的办法就是在获取值的时候即get操作的时候,先找到正确的dep类,在将依赖放入到依赖数组中,但是又出现了一个新的问题,在Proxy怎么添加watch函数中传入的依赖,这里需要对watch函数进行重构
// 封装一个响应式的函数
let activeReactiveFn = null; //用一个全局变量表示将要收集的依赖
function watchFn(fn) {
//将watch函数中传入的依赖赋值给全局变量,这样的话在Proxy类中就可以访问到要添加的依赖了
activeReactiveFn = fn;
fn(); //执行依赖
activeReactiveFn = null; //用完之后抛弃,防止对下一个依赖造成干扰
}
const objproxy = new Proxy(obj, {
get(target, key, receiver) {
const depend = getDepend(target, key); //找到当前属性的dep类
depend.addDepend(activeReactiveFn); //收集依赖
return Reflect.get(target, key, receiver);
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver);
const depend = getDepend(target, key); //找到当前属性的dep类
depend.notify(); //派发正确的依赖
},
});
测试收集正确的依赖,派发正确的依赖
watchFn(function () {
console.log(objproxy.name, ' name收集依赖----------');
});
watchFn(function () {
console.log(objproxy.age, ' age收集依赖----------');
});
objproxy.name = 'mask';
结果
在name属性改变之后只有name的依赖进行了执行,前面两句执行了是因为在watch函数执行了收集的依赖。 上面在收集正确的依赖和派发正确的依赖这里会有一点难理解,总的来说就是在get时找到正确的对象属性,即name收集自己的依赖,age收集自己的依赖,然后在set的时候先找到正确的属性,再派发正确的更新,建议自己动手实践一下,
class Depend {
constructor() {
this.reactiveFns = []; //收集依赖的地方
}
addDepend(reactiveFn) {
this.reactiveFns.push(reactiveFn); //将依赖收集
}
notify() {
this.reactiveFns.forEach((fn) => {
//遍历依赖,执行依赖
fn();
});
}
}
// 封装一个响应式的函数
//const depend = new Depend();
let activeReactiveFn = null;
function watchFn(fn) {
//depend.addDepend(fn);
activeReactiveFn = fn;
fn();
activeReactiveFn = null;
}
const obj = {
name: 'jack',
age: '18',
};
const targetMap = new WeakMap();
function getDepend(target, key) {
let map = targetMap.get(target);
if (!map) {
map = new Map();
targetMap.set(target, map);
}
//根据key去取出相对应的依赖
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(target, key);
depend.addDepend(activeReactiveFn); //收集依赖
return Reflect.get(target, key, receiver);
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver);
const depend = getDepend(target, key); //找到对应的dep类
depend.notify();
},
});
watchFn(function () {
console.log(objproxy.name, ' name收集依赖----------');
});
watchFn(function () {
console.log(objproxy.age, ' age收集依赖----------');
});
objproxy.name = 'mask';
优化
如果上面的没有看懂的话,先把上面的理解好在来看接下来的代码
上面的代码存在的问题
- 在一个依赖中重复的使用属性会有重复的更新操作
- 在set中添加依赖的时候可以不关心依赖(选择重构)
- 只能监听单个的对象
1.解决这一问题其实是非常简单的,首先这里是将所有的依赖放入到数组中的,数组是可以允许重复的,现在只要将数组中的数据进行去重既可,或者将数据放入到集合中,这里用集合解决这个问题
this.reactiveFns = new Set()
2.第二个问题是选择优化的,如果不想将依赖放入到Proxy对象中,可以在dep类中收集依赖的时候将依赖放进去,重构收集依赖的操作
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn)
}
}
- 在vue3中使用reactive将对象的转化为相应式,这里也用这种方法
function reactive(obj) {
return new Proxy(obj, {
get: function(target, key, receiver) {
// 根据target.key获取对应的depend
const depend = getDepend(target, key)
// 给depend对象中添加响应函数
// depend.addDepend(activeReactiveFn)
depend.depend()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver)
const depend = getDepend(target, key)
depend.notify()
}
})
}
其实很简单,传入过来一个对象,处理完之后在返回这个对象
最终代码
let activeReactiveFn = null; //全局变量提升
class Depend {
constructor() {
//优化一:将数组改为集合
this.reactiveFns = new Set(); // 收集依赖的地方
}
// addDepend(reactiveFn) {
// this.reactiveFns.add(reactiveFn); //将依赖收集
// }
//优化二:重构收集依赖
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn);
}
}
notify() {
this.reactiveFns.forEach((fn) => {
//遍历依赖,执行依赖
fn();
});
}
}
// 封装一个响应式的函数
//const depend = new Depend();
function watchFn(fn) {
//depend.addDepend(fn);
activeReactiveFn = fn;
fn();
activeReactiveFn = null;
}
const targetMap = new WeakMap();
function getDepend(target, key) {
let map = targetMap.get(target);
if (!map) {
map = new Map();
targetMap.set(target, map);
}
//根据key去取出相对应的依赖
let depend = map.get(key);
if (!depend) {
depend = new Depend();
map.set(key, depend);
}
return depend;
}
//优化三:reactive函数
function reactive(obj) {
return new Proxy(obj, {
get: function (target, key, receiver) {
// 根据target.key获取对应的depend
const depend = getDepend(target, key);
// 给depend对象中添加响应函数
// depend.addDepend(activeReactiveFn)
//优化后
depend.depend();
return Reflect.get(target, key, receiver);
},
set: function (target, key, newValue, receiver) {
Reflect.set(target, key, newValue, receiver);
const depend = getDepend(target, key);
depend.notify();
},
});
}
let obj = {
name: 'jack',
age: '18',
};
obj = reactive(obj);
watchFn(function () {
console.log(obj.name, ' name收集依赖----------');
});
watchFn(function () {
console.log(obj.age, ' age收集依赖----------');
});
obj.name = 'mask';
上面解释了vue3的响应式原理,在vue2也是一样的,唯一的区别就是Proxy换成Object.defineProperty,由于Proxy对对象的操作性更高,Object.defineProperty只能监听对象属性的改变,在删除或者其他的操作都需要进行其他的特殊处理,比较麻烦,所以vue3才升级为Proxy
function reactive(obj) {
// ES6之前, 使用Object.defineProperty
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get: function() {
const depend = getDepend(obj, key)
depend.depend()
return value
},
set: function(newValue) {
value = newValue
const depend = getDepend(obj, key)
depend.notify()
}
})
})
return obj
}