1.响应式原理
1.1 什么是响应式
我们先看一下响应式意味着什么?我们先来看一段代码
- m有一个初始化的值,有一段代码使用了这个值
- 那么在m有一个新的值时,这段代码可以自动重新执行
let m=100;
// * 当m发生变化时,这段代码会自动重新执行
// 一段代码
console.log(m);
console.log(m*2);
console.log(m**2);
m=200;
1.2 响应式函数的封装
如果我们想要对obj进行响应式收集,如果obj发生了变化,就执行了一部分代码,但是执行前将需要响应式的代码进行收集,所以就创建了一个函数,来收集当前obj对象的一些响应式收集
-
在全局中创建了一个变量reactiveFns来收集 obj的响应式函数
- 因为obj的响应式函数肯定不止一个
-
声明一个watchFn函数,参数是一个函数,在函数内部用reactiveFns来收集响应式函数
-
使用watchFn(fn)来收集想要响应式函数
-
最后将reactiveFns遍历执行内部的响应式函数
// * 封装一个响应式的函数
let reactiveFns=[];
function watchFn(Fn){
reactiveFns.push(Fn)
}
const obj={
name:"wjy",
age:18
}
watchFn(function (){
const newName=obj.name;
console.log("你好~何沅洲");
console.log("Hello World");
console.log(obj.name);
})
watchFn(function (){
console.log(obj.name);
})
function bar(){
console.log("普通的其他的函数");
console.log("这个函数不需要有任何响应式");
}
obj.name="hyz";
reactiveFns.forEach(fn=>fn())
1.3 依赖收集类的封装
前面的设计存在缺陷:
- 一个数组存取是这个对象的所有的响应式依赖
- 而在实际中,应该对象的每一个属性都对应一个数组来收集,但如果在全局创建大量的数组来保存,势必不好操作。
所以我们设计了一个依赖收集类Depend:
- constructor()
- 初始化一个reactiveFns来存储响应式函数
- addDepend(reactiveFn)
- 将响应式函数收集
- notify()
- 将对应的响应式函数执行
// * 每个属性对应一个类
class Depend{
constructor() {
this.reactiveFns=[];
}
addDepend(reactiveFn){
this.reactiveFns.push(reactiveFn)
}
notify(){
this.reactiveFns.forEach(fn=>fn())
}
}
将对应的watchFn函数修改:
// * 封装一个响应式的函数
const depend=new Depend();
function watchFn(Fn){
depend.addDepend(Fn)
}
所有的代码如下:
// * 每个属性对应一个类
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:"wjy",
age:18
}
watchFn(function (){
const newName=obj.name;
console.log("你好~何沅洲");
console.log("Hello World");
console.log(obj.name);
})
watchFn(function (){
console.log(obj.name);
})
function bar(){
console.log("普通的其他的函数");
console.log("这个函数不需要有任何响应式");
}
obj.name="hyz";
depend.notify()
但是在代码最后是手动去调用对象的响应式函数。
1.4 监听对象的变化
在前面设计中存在缺陷:
- 当对象发生了变化,应该是自动去调用对应的响应式函数,而不是手动去调用
监听对象的变化有两种方式:
- Proxy
- Object.defineProperty()
所以我们给obj创建一个Proxy对象(代理对象),使用代理对象代替原来对象的操作,因为代理对象内部有很多捕捉器,可以实时捕捉用户对代理对象操作。
- 在set捕捉器内部,去调用所有的响应式函数。(在对象内部属性发生变化时去调所有的响应式函数)
- depend.notify()
// * 监听对象的属性变化 : Proxy(vue3)和Object.defineProperty(vue2)
const objProxy=new Proxy(obj,{
get:function(target,key,receiver){
return Reflect.get(target,key,receiver);
},
set:function(target,key,newValue,receiver){
Reflect.set(target,key,newValue,receiver);
depend.notify();
}
})
所有的代码如下:
// * 每个属性对应一个类
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:"wjy",
age:18
}
// * 监听对象的属性变化 : Proxy(vue3)和Object.defineProperty(vue2)
const objProxy=new Proxy(obj,{
get:function(target,key,receiver){
return Reflect.get(target,key,receiver);
},
set:function(target,key,newValue,receiver){
Reflect.set(target,key,newValue,receiver);
depend.notify();
}
})
watchFn(function (){
// const newName=obj.name;
console.log("你好~何沅洲");
console.log("Hello World");
console.log(obj.name);
})
watchFn(function (){
console.log(obj.name);
})
function bar(){
console.log("普通的其他的函数");
console.log("这个函数不需要有任何响应式");
}
objProxy.name="hyz";
objProxy.name="tqy";
objProxy.name="lzy"
// depend.notify()
1.5 依赖收集的管理
- 使用一个WeakMap对象来存储每个对象。
- 为每个对象创建一个Map对象,对象的每个属性对应一个Depend对象,将key为属性,value为Depend对象添加到Map对象内部。
所以我们封装了一个获取Depend函数:
- 在全局中,创建一个变量targetMap(为了保存所有所有对应的映射关系)
// * 封装一个获取depend的函数
const targetMap=new WeakMap();
function getDepend(target,key){
// * 根据target对象获取map的过程
const map=targetMap.get(target);
if(!map){//第一次在使用的需要去创建一个
map=new Map();
targetMap.set(target,map);
}
// * 根据key获取depend对象
const depend=map.get(key);
if(!depend){ //* 第一次可能没有depend
depend=new Depend();
map.set(key,depend);
}
return depend;
}
修改响应式对象内部的set捕捉器:
- 在里面获取对应的depend对象,然后再去调用它本身的所有依赖函数
// * 监听对象的属性变化 : Proxy(vue3)和Object.defineProperty(vue2)
const objProxy=new Proxy(obj,{
get:function(target,key,receiver){
return Reflect.get(target,key,receiver);
},
set:function(target,key,newValue,receiver){
Reflect.set(target,key,newValue,receiver);
// depend.notify();
const depend=getDepend(target,key);
depend.notify();
}
})
所有的代码如下:
// * 每个属性对应一个类
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)
}
// * 封装一个获取depend的函数
const targetMap=new WeakMap();
function getDepend(target,key){
// * 根据target对象获取map的过程
const map=targetMap.get(target);
if(!map){//第一次在使用的需要去创建一个
map=new Map();
targetMap.set(target,map);
}
// * 根据key获取depend对象
const depend=map.get(key);
if(!depend){ //* 第一次可能没有depend
depend=new Depend();
map.set(key,depend);
}
return depend;
}
const obj={
name:"wjy",
age:18
}
// * 监听对象的属性变化 : Proxy(vue3)和Object.defineProperty(vue2)
const objProxy=new Proxy(obj,{
get:function(target,key,receiver){
return Reflect.get(target,key,receiver);
},
set:function(target,key,newValue,receiver){
Reflect.set(target,key,newValue,receiver);
// depend.notify();
const depend=getDepend(target,key);
depend.notify();
}
})
watchFn(function (){
const newName=obj.name;
console.log("你好~何沅洲");
console.log("Hello World");
console.log(obj.name);
})
watchFn(function (){
console.log(obj.name);
})
function bar(){
console.log("普通的其他的函数");
console.log("这个函数不需要有任何响应式");
}
objProxy.name="hyz";
objProxy.name="tqy";
objProxy.name="lzy"
// depend.notify()
// * 什么时候去收集封装依赖最合适呢
const info={
address:"怀化"
}
// obj对象
// name depend
// age depend
//const objMap=new Map();
//objMap.set("name","nameDepend");
//objMap.set("age","ageDepend");
// info对象
// address depend
// const infoMap=new Map();
// infoMap.set("address","addressDepend")
// const targetMap=new WeakMap();
// targetMap.set(obj,objMap);
// targetMap.set(info,infoMap);
// // * 如果obj.name发生变化了
// targetMap.get(obj).get("name");//* 去找到name的depend
// depend.notify();//* 然后去执行 name的depend
1.6 正确的收集依赖
-
重构了watchFn函数
-
在外设置了一个变量activeReactiveFn用来暂存当前需要收集的响应式函数
-
让函数先执行,在获取obj的key,进行响应式函数收集
-
在函数执行前,将activeReactiveFn设置为Fn
-
在函数执行后,将activeReactiveFn设置为null
// * 封装一个响应式的函数 let activeReactiveFn=null; function watchFn(Fn){ activeReactiveFn=Fn; Fn(); activeReactiveFn=null; }
-
-
将收集依赖设置在 Proxy对象的get捕捉器里面
- 先获取target的key属性的依赖收集对象
- 进行判断,如果不为null,就将这个当前需要收集的响应式函数放入到depend数组中
get:function(target,key,receiver){ //* 1. 在这里能拿到target,还能拿到对应的key,获取对应的depend const depend=getDepend(target,key); // * 2. 给depend对象中添加响应函数 if(activeReactiveFn){ depend.addDepend(activeReactiveFn); } return Reflect.get(target,key,receiver); },
// * 每个属性对应一个类
class Depend{
constructor() {
this.reactiveFns=[];
}
addDepend(reactiveFn){
this.reactiveFns.push(reactiveFn)
}
notify(){
this.reactiveFns.forEach(fn=>fn())
}
}
// * 封装一个响应式的函数
let activeReactiveFn=null;
function watchFn(Fn){
activeReactiveFn=Fn;
Fn();
activeReactiveFn=null;
}
// * 封装一个获取depend的函数
const targetMap=new WeakMap();
function getDepend(target,key){
// * 根据target对象获取map的过程
let map=targetMap.get(target);
if(!map){//第一次在使用的需要去创建一个
map=new Map();
targetMap.set(target,map);
}
// * 根据key获取depend对象
let depend=map.get(key);
if(!depend){ //* 第一次可能没有depend
depend=new Depend();
map.set(key,depend);
}
return depend;
}
const obj={
name:"wjy",
age:18
}
// * 监听对象的属性变化 : Proxy(vue3)和Object.defineProperty(vue2)
const objProxy=new Proxy(obj,{
get:function(target,key,receiver){
//* 1. 在这里能拿到target,还能拿到对应的key,获取对应的depend
const depend=getDepend(target,key);
// * 2. 给depend对象中添加响应函数
if(activeReactiveFn){
depend.addDepend(activeReactiveFn);
}
return Reflect.get(target,key,receiver);
},
set:function(target,key,newValue,receiver){
Reflect.set(target,key,newValue,receiver);
// depend.notify();
const depend=getDepend(target,key);
depend.notify();
}
})
watchFn(function (){
console.log("--------------------第一个name函数开始------------");
console.log("你好~何沅洲");
console.log("Hello World");
console.log(objProxy.name);
console.log("--------------------第一个name函数结束------------");
})
watchFn(function (){
console.log(objProxy.name,"demo function ------------");
})
watchFn(function(){
console.log(objProxy.age,"age 发生变化是需要执行的--------1");
})
watchFn(function(){
console.log(objProxy.age,"age 发生变化是需要执行的--------2");
})
function bar(){
console.log("普通的其他的函数");
console.log("这个函数不需要有任何响应式");
};
console.log("-------------------------------------------------------------改变obj的name值-----------------------------------");
objProxy.age=33;
1.7对Depend类的优化 (重构)
我们前面设计存在一些缺陷,例如不应该在Proxy对象内部的get中去操作全局变量,存在一定的不合理性。
我们应该在Depend类中进行收集依赖,而在get捕捉器,只需执行depend.depend()即可收集依赖。
所以进行第一步的Depend类的重构:
添加一个实例方法 depend:
- 如果activeReactiveFn不为空就去收集依赖
修改了Proxy对象的get方法
// * 当前需要收集的响应式函数
let activeReactiveFn=null;
// * 每个属性对应一个类
class Depend{
//以上的代码省略,只介绍重要的部分
depend(){
if(activeReactiveFn){
this.reactiveFns.add(activeReactiveFn);
}
}
}
const objProxy=new Proxy(obj,{
get:function(target,key,receiver){
//* 1. 在这里能拿到target,还能拿到对应的key,获取对应的depend
const depend=getDepend(target,key);
// * 2. 给depend对象中添加响应函数
depend.depend();
return Reflect.get(target,key,receiver);
},
//以下代码暂时省略
})
如果我给watchFn函数添加如下的的一个函数:
watchFn(()=>{
console.log(objProxy.name,"---------");
console.log(objProxy.name,"+++++++");
})
console.log("--------------------objProxy发生了改变---------------");
objProxy.name="hyz"
image-20220513231508764.png
在objProxy.name发生变化的时候,执行了两次。
原因是:在新添加的函数内部使用了两次objProxy.name所以就添加了两次。
但实际上,无论在当前函数内部使用多少次name属性,就应该只为name的depend只收集一次当前的响应式函数。
所以:为了在当前属性的depend不应该重复收集响应式函数,我们应该将Depend类的属性设置为Set对象(本质:存放在Set内部的元素是不会重复的。)
- Set对象和数组对象添加元素方法不一样,应该将push改为add
// * 当前需要收集的响应式函数
let activeReactiveFn=null;
// * 每个属性对应一个类
class Depend{
constructor() {
// * 解决方案是将原来的数组类型转化为set类型
this.reactiveFns=new Set();
}
addDepend(reactiveFn){
this.reactiveFns.add(reactiveFn)
}
notify(){
this.reactiveFns.forEach(fn=>fn())
}
depend(){
if(activeReactiveFn){
this.reactiveFns.add(activeReactiveFn);
}
}
}
再执行一次:结果恢复我想看到的了,无论在当前函数内部使用多少次name属性,就应该只为name的depend只收集一次当前的响应式函数。
image-20220513232357915.png总的代码如下:
// * 当前需要收集的响应式函数
let activeReactiveFn=null;
// * 每个属性对应一个类
class Depend{
constructor() {
// * 解决方案是将原来的数组类型转化为set类型
this.reactiveFns=new Set();
}
addDepend(reactiveFn){
this.reactiveFns.add(reactiveFn)
}
notify(){
this.reactiveFns.forEach(fn=>fn())
}
depend(){
if(activeReactiveFn){
this.reactiveFns.add(activeReactiveFn);
}
}
}
// * 封装一个响应式的函数
function watchFn(Fn){
activeReactiveFn=Fn;
Fn();
activeReactiveFn=null;
}
// * 封装一个获取depend的函数
const targetMap=new WeakMap();
function getDepend(target,key){
// * 根据target对象获取map的过程
let map=targetMap.get(target);
if(!map){//第一次在使用的需要去创建一个
map=new Map();
targetMap.set(target,map);
}
// * 根据key获取depend对象
let depend=map.get(key);
if(!depend){ //* 第一次可能没有depend
depend=new Depend();
map.set(key,depend);
}
return depend;
}
const obj={
name:"wjy",
age:18
}
// * 监听对象的属性变化 : Proxy(vue3)和Object.defineProperty(vue2)
const objProxy=new Proxy(obj,{
get:function(target,key,receiver){
//* 1. 在这里能拿到target,还能拿到对应的key,获取对应的depend
const depend=getDepend(target,key);
// * 2. 给depend对象中添加响应函数
depend.depend();
return Reflect.get(target,key,receiver);
},
set:function(target,key,newValue,receiver){
Reflect.set(target,key,newValue,receiver);
// * 获取对应对象的key的依赖收集
const depend=getDepend(target,key);
// * 依次执行依赖收集的响应式函数
depend.notify();
}
})
watchFn(()=>{
// * 这里面有两个地方用到了name,所以如果objProxy.name发生了变化,,这里其实应该是只要执行一次的,但是实际上原来设计会执行两次
console.log(objProxy.name,"---------");
console.log(objProxy.name,"+++++++");
})
console.log("--------------------objProxy发生了改变---------------");
objProxy.name="hyz"
1.8 创建响应式对象(Vue3)
但如果我们有新的对象要进行响应式处理呢?那新创建一个 Proxy对象,然后在写Proxy对象内部写一大串同样的代码,如果有很多对象需要进行收集,那代码的数量就大大增加了,阅读性差。就比如以下,以下代码还是只是新增了一个响应式对象
// * 但如果我们有新的对象要进行响应式处理呢?那新创建一个 Proxy对象,然后在写Proxy对象内部写一大串同样的代码,如果有很多对象需要进行收集,那代码的数量就大大增加了,阅读性差。
const info={
address:"广州市",
height:1.88
}
const infoProxy=new Proxy(info,{
get:function(target,key,receiver){
//* 1. 在这里能拿到target,还能拿到对应的key,获取对应的depend
const depend=getDepend(target,key);
// * 2. 给depend对象中添加响应函数
depend.depend();
return Reflect.get(target,key,receiver);
},
set:function(target,key,newValue,receiver){
Reflect.set(target,key,newValue,receiver);
// * 获取对应对象的key的依赖收集
const depend=getDepend(target,key);
// * 依次执行依赖收集的响应式函数
depend.notify();
}
})
watchFn(()=>{
console.log("info",infoProxy.address,new Date());
})
infoProxy.address="怀化";
所以我决定封装一个函数:创建响应式对象,并返回
// * 封装一个响应式对象的函数
function reactive(obj){
return new Proxy(obj,{
get:function(target,key,receiver){
//* 1. 在这里能拿到target,还能拿到对应的key,获取对应的depend
const depend=getDepend(target,key);
// * 2. 给depend对象中添加响应函数
depend.depend();
return Reflect.get(target,key,receiver);
},
set:function(target,key,newValue,receiver){
Reflect.set(target,key,newValue,receiver);
// * 获取对应对象的key的依赖收集
const depend=getDepend(target,key);
// * 依次执行依赖收集的响应式函数
depend.notify();
}
})
}
之前的代码又可以简写为下面的样子:
// * 当前需要收集的响应式函数
let activeReactiveFn=null;
// * 每个属性对应一个类
class Depend{
constructor() {
// * 解决方案是将原来的数组类型转化为set类型
this.reactiveFns=new Set();
}
addDepend(reactiveFn){
this.reactiveFns.add(reactiveFn)
}
notify(){
this.reactiveFns.forEach(fn=>fn())
}
depend(){
if(activeReactiveFn){
this.reactiveFns.add(activeReactiveFn);
}
}
}
// * 封装一个响应式的函数
function watchFn(Fn){
activeReactiveFn=Fn;
Fn();
activeReactiveFn=null;
}
// * 封装一个获取depend的函数
const targetMap=new WeakMap();
function getDepend(target,key){
// * 根据target对象获取map的过程
let map=targetMap.get(target);
if(!map){//第一次在使用的需要去创建一个
map=new Map();
targetMap.set(target,map);
}
// * 根据key获取depend对象
let depend=map.get(key);
if(!depend){ //* 第一次可能没有depend
depend=new Depend();
map.set(key,depend);
}
return depend;
}
// * 封装一个响应式对象的函数
function reactive(obj){
return new Proxy(obj,{
get:function(target,key,receiver){
//* 1. 在这里能拿到target,还能拿到对应的key,获取对应的depend
const depend=getDepend(target,key);
// * 2. 给depend对象中添加响应函数
depend.depend();
return Reflect.get(target,key,receiver);
},
set:function(target,key,newValue,receiver){
Reflect.set(target,key,newValue,receiver);
// * 获取对应对象的key的依赖收集
const depend=getDepend(target,key);
// * 依次执行依赖收集的响应式函数
depend.notify();
}
})
};
const obj=reactive({
name:"wjy",
age:18
})
// * 但如果我们有新的对象要进行响应式处理呢?那新创建一个 Proxy对象,然后在写Proxy对象内部写一大串同样的代码,如果有很多对象需要进行收集,那代码的数量就大大增加了,阅读性差。
const info=reactive({
address:"广州市",
height:1.88
})
watchFn(()=>{
console.log("info",info.address,new Date());
})
info.address="怀化";
1.9 创建响应式对象(Vue2)
将reactive函数进行改造,使用Object.definedProperty去截取对象的get和set操作。以便在里面进行相关操作。
// * 封装一个响应式对象的函数
function reactive(obj){
// {name:"wjy",age:18}
// * 根据对象的所有key定义属性描述符
Object.keys(obj).forEach(key=>{
let value= obj[key];
Object.defineProperty(obj,key,{
get:function(){
// * 获取当前对象的key的收集对象
const depend=getDepend(obj,key);
// * 收集当前响应式依赖
depend.depend();
return value;
},
set:function(v){
value=v;
// * 获取当前对象的key的收集对象
const depend=getDepend(obj,key);
// * 去调用收集对象的响应式依赖,因为值发生了变化
depend.notify();
}
})
})
return obj
};
所有代码:
// * 当前需要收集的响应式函数
let activeReactiveFn=null;
// * 每个属性对应一个类
class Depend{
constructor() {
// * 解决方案是将原来的数组类型转化为set类型
this.reactiveFns=new Set();
}
addDepend(reactiveFn){
this.reactiveFns.add(reactiveFn)
}
notify(){
this.reactiveFns.forEach(fn=>fn())
}
depend(){
if(activeReactiveFn){
this.reactiveFns.add(activeReactiveFn);
}
}
}
// * 封装一个响应式的函数
function watchFn(Fn){
activeReactiveFn=Fn;
Fn();
activeReactiveFn=null;
}
// * 封装一个获取depend的函数
const targetMap=new WeakMap();
function getDepend(target,key){
// * 根据target对象获取map的过程
let map=targetMap.get(target);
if(!map){//第一次在使用的需要去创建一个
map=new Map();
targetMap.set(target,map);
}
// * 根据key获取depend对象
let depend=map.get(key);
if(!depend){ //* 第一次可能没有depend
depend=new Depend();
map.set(key,depend);
}
return depend;
}
// * 封装一个响应式对象的函数
function reactive(obj){
// {name:"wjy",age:18}
// * 根据对象的所有key定义属性描述符
Object.keys(obj).forEach(key=>{
let value= obj[key];
Object.defineProperty(obj,key,{
get:function(){
// * 获取当前对象的key的收集对象
const depend=getDepend(obj,key);
// * 收集当前响应式依赖
depend.depend();
return value;
},
set:function(v){
value=v;
// * 获取当前对象的key的收集对象
const depend=getDepend(obj,key);
// * 去调用收集对象的响应式依赖,因为值发生了变化
depend.notify();
}
})
})
return obj
};
const obj=reactive({
name:"wjy",
age:18
})
// * 但如果我们有新的对象要进行响应式处理呢?那新创建一个 Proxy对象,然后在写Proxy对象内部写一大串同样的代码,如果有很多对象需要进行收集,那代码的数量就大大增加了,阅读性差。
const info=reactive({
address:"广州市",
height:1.88
})
watchFn(()=>{
console.log("info",info.address,new Date());
})
info.address="怀化市"
网友评论