響應(yīng)式是什么
簡單解釋就是x有一個初始化的值汉匙,有一段代碼在別的地方使用了,當x發(fā)生變化時,在剛才使用x的地方能夠立馬發(fā)生改變或者重新執(zhí)行
簡單的響應(yīng)式實現(xiàn)
const obj = {
name: 'jack',
age: '18',
};
obj.name = 'mask';
在上面的代碼中来累,當name發(fā)生改變時没隘,在其他的地方使用了name的地方也要立馬發(fā)生改變懂扼,這里需要解決兩個問題,怎么知道哪里使用了右蒲,怎么通知更新阀湿,第一個問題我們稱之為收集依賴,第二個問題叫做派發(fā)更新瑰妄。
watchFn(function () {
console.log(obj.name, 'name收集依賴----------');
});
這里通過watch函數(shù)來收集依賴陷嘴,解決的問題是怎么知道哪里使用了,類似于在vue中使用{{}}使用了name屬性
// 封裝一個響應(yīng)式的函數(shù)
let reactiveFns = []
function watchFn(fn) {
reactiveFns.push(fn)
}
watch函數(shù)解決的收集依賴的問題间坐,這里將收集到的依賴放在數(shù)組中灾挨,在進行派發(fā)操作的時候只需要遍歷數(shù)組中的函數(shù),進行執(zhí)行即可
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地方也發(fā)生改變了劳澄,這就是響應(yīng)式最簡單的原理,如果能夠理解上面的例子蜈七,接下來就可以看完整的響應(yīng)式原理了
封裝收集依賴類
在上面例子中秒拔,收集依賴和派發(fā)更新時可以封裝在一個類中
class Depend {
constructor() {
this.reactiveFns = [] //收集依賴的地方
}
addDepend(reactiveFn) {
this.reactiveFns.push(reactiveFn)//將依賴收集
}
notify() {
this.reactiveFns.forEach(fn => { //遍歷依賴,執(zhí)行依賴
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() //派發(fā)更新操作
這樣在其他地方使用的時候可以簡化大量的代碼砂缩,
現(xiàn)在還有問題就是作谚,在進行派發(fā)更新的時候是手動進行派發(fā)的,如果再其他地方也需要進行派發(fā)更新的時候就會造成大量的代碼冗余庵芭,最好是能給一個能自動收集依賴的操作妹懒,畢竟能自動絕不手動,接下來就要實現(xiàn)一個自動派發(fā)更新的操作
自動監(jiān)聽對象的改變
在js中監(jiān)聽對象的改變可以通過set和get來監(jiān)聽對象的屬性變化双吆,在vue2中用的是Object.defineProperty,但是這個屬性只能監(jiān)聽對象屬性的修改和訪問眨唬,在進行修改和其他操作就不行了,vue3中用Proxy類來實現(xiàn)對象的監(jiān)聽伊诵,接下來先用Proxy來實現(xiàn)一下单绑,下面會用到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監(jiān)聽的整個對象,set在屬性改變時進行監(jiān)聽對象的屬性改變笛坦,這里直接將派發(fā)更新的操作放在set函數(shù)中就可以實現(xiàn)自動派發(fā)更新的操作了区转。基本上整個功能實現(xiàn)了一大步版扩,但是還有幾個問題沒有解決废离,這里的收集依賴都是放在一整個的數(shù)組中,只要有一個屬性的值發(fā)生了改變其他的依賴也會相應(yīng)的進行執(zhí)行礁芦,
class Depend {
constructor() {
this.reactiveFns = []; //收集依賴的地方
}
addDepend(reactiveFn) {
this.reactiveFns.push(reactiveFn); //將依賴收集
}
notify() {
this.reactiveFns.forEach((fn) => {
//遍歷依賴蜻韭,執(zhí)行依賴
fn();
});
}
}
// 封裝一個響應(yīng)式的函數(shù)
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';
類似于這樣的在這樣的情況下,只改變name的值柿扣,但是age的依賴也發(fā)生變化了肖方,這不是想要的結(jié)果,想要的結(jié)果是在當前屬性發(fā)生變化的時候未状,收集的依賴只能是當前屬性的依賴俯画,派發(fā)的更新也是派發(fā)當前屬性的更新,所以這里需要解決這個問題司草,需要通過一個結(jié)構(gòu)來收集正確的依賴艰垂,派發(fā)正確的更新
這里用上面的結(jié)構(gòu)來收集正確的依賴,派發(fā)正確的更新
將所有的對象放在WeakMap中埋虹,單個對象放在Map中猜憎,每一個屬性的依賴放在一個dep對象中,最后在Map中實現(xiàn) obj:{key:[dep對象]}搔课,這樣的數(shù)據(jù)結(jié)構(gòu),用這種數(shù)據(jù)結(jié)構(gòu)的話拉宗,又有新的問題了,需要實現(xiàn)一個函數(shù)找到正確的依賴辣辫,并且返回Dep對象
接下來就是要先實現(xiàn)這個方法
//封裝一個收集依賴的getdepend
//收集依賴
//1.封裝一個weakmap收集所有的對象
//2.在從weakmap中獲取到需要的對象
//3.在用map收集所有的對象屬性添加依賴
const targetMap = new WeakMap();//第一步
function getDepend(target, key) { //傳入的對象和key的值
let map = targetMap.get(target); //第二步
if (!map) { //沒有這個對象的map類的話旦事,先創(chuàng)建一個
map = new Map();
targetMap.set(target, map); //根據(jù)target和key設(shè)置對象的map
}
//根據(jù)key去取出相對應(yīng)的依賴
let depend = map.get(key);
if (!depend) { //沒有depend類先創(chuàng)建dep類,在設(shè)置(key, depend);
depend = new Depend();
map.set(key, depend);
}
return depend; //返回找到的key值的dep類
}
現(xiàn)在就可以在進行派發(fā)更新之前的先獲取到depend急灭,再更據(jù)depend進行派發(fā)正確的更新了姐浮,現(xiàn)在這里完成了正確的派發(fā)更新操作,還有一個問題是怎么收集正確的依賴
首先這里實現(xiàn)的watch函數(shù)
const depend = new Depend();
function watchFn(fn) {
depend.addDepend(fn);
}
在watch函數(shù)中是傳入了相應(yīng)的依賴,現(xiàn)在的問題是怎么把正確的依賴放到相應(yīng)的key值上葬馋,解決的辦法就是在獲取值的時候即get操作的時候卖鲤,先找到正確的dep類,在將依賴放入到依賴數(shù)組中,但是又出現(xiàn)了一個新的問題畴嘶,在Proxy怎么添加watch函數(shù)中傳入的依賴蛋逾,這里需要對watch函數(shù)進行重構(gòu)
// 封裝一個響應(yīng)式的函數(shù)
let activeReactiveFn = null; //用一個全局變量表示將要收集的依賴
function watchFn(fn) {
//將watch函數(shù)中傳入的依賴賦值給全局變量,這樣的話在Proxy類中就可以訪問到要添加的依賴了
activeReactiveFn = fn;
fn(); //執(zhí)行依賴
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(); //派發(fā)正確的依賴
},
});
測試收集正確的依賴区匣,派發(fā)正確的依賴
watchFn(function () {
console.log(objproxy.name, ' name收集依賴----------');
});
watchFn(function () {
console.log(objproxy.age, ' age收集依賴----------');
});
objproxy.name = 'mask';
在name屬性改變之后只有name的依賴進行了執(zhí)行,前面兩句執(zhí)行了是因為在watch函數(shù)執(zhí)行了收集的依賴蒋院。 上面在收集正確的依賴和派發(fā)正確的依賴這里會有一點難理解亏钩,總的來說就是在get時找到正確的對象屬性,即name收集自己的依賴欺旧,age收集自己的依賴姑丑,然后在set的時候先找到正確的屬性,再派發(fā)正確的更新辞友,建議自己動手實踐一下栅哀,
class Depend {
constructor() {
this.reactiveFns = []; //收集依賴的地方
}
addDepend(reactiveFn) {
this.reactiveFns.push(reactiveFn); //將依賴收集
}
notify() {
this.reactiveFns.forEach((fn) => {
//遍歷依賴,執(zhí)行依賴
fn();
});
}
}
// 封裝一個響應(yīng)式的函數(shù)
//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);
}
//根據(jù)key去取出相對應(yīng)的依賴
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); //找到對應(yīng)的dep類
depend.notify();
},
});
watchFn(function () {
console.log(objproxy.name, ' name收集依賴----------');
});
watchFn(function () {
console.log(objproxy.age, ' age收集依賴----------');
});
objproxy.name = 'mask';
優(yōu)化
如果上面的沒有看懂的話称龙,先把上面的理解好在來看接下來的代碼
上面的代碼存在的問題
- 在一個依賴中重復(fù)的使用屬性會有重復(fù)的更新操作
- 在set中添加依賴的時候可以不關(guān)心依賴(選擇重構(gòu))
- 只能監(jiān)聽單個的對象
1.解決這一問題其實是非常簡單的留拾,首先這里是將所有的依賴放入到數(shù)組中的,數(shù)組是可以允許重復(fù)的茵瀑,現(xiàn)在只要將數(shù)組中的數(shù)據(jù)進行去重既可,或者將數(shù)據(jù)放入到集合中间驮,這里用集合解決這個問題
this.reactiveFns = new Set()
2.第二個問題是選擇優(yōu)化的,如果不想將依賴放入到Proxy對象中马昨,可以在dep類中收集依賴的時候?qū)⒁蕾嚪胚M去,重構(gòu)收集依賴的操作
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn)
}
}
- 在vue3中使用reactive將對象的轉(zhuǎn)化為相應(yīng)式竞帽,這里也用這種方法
function reactive(obj) {
return new Proxy(obj, {
get: function(target, key, receiver) {
// 根據(jù)target.key獲取對應(yīng)的depend
const depend = getDepend(target, key)
// 給depend對象中添加響應(yīng)函數(shù)
// 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() {
//優(yōu)化一:將數(shù)組改為集合
this.reactiveFns = new Set(); // 收集依賴的地方
}
// addDepend(reactiveFn) {
// this.reactiveFns.add(reactiveFn); //將依賴收集
// }
//優(yōu)化二:重構(gòu)收集依賴
depend() {
if (activeReactiveFn) {
this.reactiveFns.add(activeReactiveFn);
}
}
notify() {
this.reactiveFns.forEach((fn) => {
//遍歷依賴屹篓,執(zhí)行依賴
fn();
});
}
}
// 封裝一個響應(yīng)式的函數(shù)
//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);
}
//根據(jù)key去取出相對應(yīng)的依賴
let depend = map.get(key);
if (!depend) {
depend = new Depend();
map.set(key, depend);
}
return depend;
}
//優(yōu)化三:reactive函數(shù)
function reactive(obj) {
return new Proxy(obj, {
get: function (target, key, receiver) {
// 根據(jù)target.key獲取對應(yīng)的depend
const depend = getDepend(target, key);
// 給depend對象中添加響應(yīng)函數(shù)
// depend.addDepend(activeReactiveFn)
//優(yōu)化后
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的響應(yīng)式原理,在vue2也是一樣的匙奴,唯一的區(qū)別就是Proxy換成Object.defineProperty,由于Proxy對對象的操作性更高堆巧,Object.defineProperty只能監(jiān)聽對象屬性的改變,在刪除或者其他的操作都需要進行其他的特殊處理,比較麻煩谍肤,所以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
}