引言
一直想要好好學一下關於框架原理的東西,奈何以前的課程任務太重,現在終於得空可以學習學習,跟着一個牛逼的博客做下去,相信自己能夠得到很大的提升的哈哈哈哈哈哈,不想看我的可以直接去他那裏seesee,傳送門
前期知識
先了解一下MVC和MVVM地工作原理:
MVC可謂是十分經典:
- 視圖(View):用戶界面。
- 控制器(Controller):業務邏輯
- 模型(Model):數據保存
各部分之間的通信方式如下: - View 傳送指令到 Controller
- Controller 完成業務邏輯後,要求 Model 改變狀態
- Model 將新的數據發送到 View,用戶得到反饋
- 所有通信都是單向的。
MVVM現今也是十分的流行:
- 所有的通信都是雙向的
- view和model層不發生關係,所有的都通過ViewModel傳遞~~
- view層其實比較薄,而ViewModel比較厚,所有的邏輯代碼什麼的都在這裏面
- view和ViewModel採用的是雙向綁定,view的變動自動反映在viewModel中,反之亦然~
先嚐試一個小demo
在vue中其實比較核心的是Object.defineProperty(obj, prop, descriptor)
這個API。
我們需要注意的是,使用的時候不能同時設置訪問器(getter和setter)和writeable或者value,否則會會報這樣的錯誤:
TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute
,
step1:defineProperty
let callback={
target: null
}
let defineReactive=function(object,key,value){
let array=[]
Object.defineProperty(object,key,{
configurable: true,
enumerable:true,
get: function(){
if(callback.target){
array.push(callback.target)
}
return value
},
set: function(newValue){
if(newValue!=value){
array.forEach(fun => {
fun(newValue,value)
});
}
value=newValue;
}
})
}
let object={};
defineReactive(object,'test','test');
callback.target=function(newValue,oldValue){
console.log("我被添加進去了,新的值是",newValue);
}
object.test;
callback.target=null;
object.test='test2';
callback.target=function(newValue,oldValue){
console.log("添加第二個函數,新的value",newValue);
}
object.test
callback.target=null;
object.test='test3';
當我們取值的時候,函數自動幫我們添加針對當前值的依賴,當我們的值發生變化的時候,會自動處理這些依賴,比如我們的dom變化的時候。
這其實也是vue
的核心代碼,只是它的實現要遠比我們的複雜許多!
step2 Dep
上一步我們實現了VUE的依賴收集和觸發,但是我們只是將它放在了一個內置的數組裏面,雖然容易理解但是十分不好維護,爲了更加好地維護,需要事先一個維護依賴的類!
類下屬性:target 函數,用於存放需要添加的依賴
實例下屬性:
subs/array:存放依賴
addsub/function: 添加依賴
removesub/function:刪除依賴
notify/function:執行依賴
let Dep=function(){
//實例屬性
this.subs=[]
//實例方法
this.addSub=function(sub){
this.subs.push(sub);
}
this.removeSub=function(sub){
const index=this.subs.indexOf(sub);
if(index>-1){
this.sub.splice(index,1);
}
}
this.notify=function(newValue,oldValue){
this.subs.forEach(func => {
func(newValue,oldValue)
});
}
}
//類屬性
Dep.target=null;
let defineReactive=function(object,key,value){
let dep=new Dep();
Object.defineProperty(object,key,{
configurable:true,
enumerable:true,
get: function(){
if(Dep.target){
dep.addSub(Dep.target)
}
return value
},
set:function(newValue){
if(newValue!=value){
dep.notify(newValue,value);
}
value=newValue;
}
})
}
let object={};
defineReactive(object,'test','test');
Dep.target=function(newValue,oldValue){
console.log('我被添加進去了,新的值',newValue);
}
object.test;
Dep.target=null;
object.test='test2';
Dep.target=function(newValue,oldValue){
console.log('添加第二個函數,新的值是',newValue);
}
object.test;
Dep.target=null
object.test='test3';
- dep類將監聽屬性和處理依賴進行了一定的解耦,但是解耦不是很完全,在我們觸發依賴的時候還是得傳遞新舊值。
- 刪除依賴肯定在外部環境中,但是我們的dep實例是在
defineReactive
作用域裏面,外部沒有辦法直接進行調用!
vue中的Watcher
類處理了以上兩個問題,後面說!
剛剛的Dep類可以在es6的語法下:
class Dep{
constructor(){
this.subs=[];
}
addSub(sub){
this.subs.push(sub);
}
removeSub(sub){
const index=this.subs.indexOf(sub);
if(index>-1){
this.subs.splice(index,1);
}
}
notify(){
this.subs.forEach(func=>func(newValue,oldValue))
}
}
//類屬性
Dep.target=null;
step3 Watcher
- 解耦不完全,需要傳遞參數
- 沒有地方可以移除依賴
//其中wacher是存入subs數組裏面的一個個元素
let Watcher= function(object,key,callback){
this.obj=object;
this.getter=key;
this.cb=callback;
this.dep=null;
this.value=undefined;
this.get=function(){
Dep.target=this;
let value=this.obj[this.getter];//有個object、get操作
Dep.target=null;
return value;
}
this.update=function(){
const value = this.obj[this.getter];
const oldValue=this.value;
this.value=value;
this.cb.call(this.obj,value,oldValue);
}
this.addDep=function(dep){
this.dep=dep;
}
this.value=this.get();
}
let Dep=function(){
this.subs=[];
this.addSub=function(sub){
this.subs.push(sub);
}
this.removeSub=function(sub){
const index=this.subs.indexOf(sub);
if(index>-1){
this.subs.splice(index,1)
}
}
this.notify=function(){
this.subs.forEach(watcher => {
watcher.update();
});
}
}
Dep.target=null;
let defineReactive=function(object,key,value){
let dep=new Dep();
Object.defineProperty(object,key,{
configurable:true,
enumerable:true,
get:function(){
if(Dep.target){
dep.addSub(Dep.target)
// 添加 watcher 對 dep 的引用
Dep.target.addDep(dep)
}
return value;
},
set: function(newValue){
if(newValue!=value){
value=newValue;
dep.notify();
}
}
})
}
let object = {}
defineReactive(object, 'test', 'test')
let watcher = new Watcher(object, 'test', function(newValue, oldValue){
console.log('作爲 watcher 添加的第一個函數,很自豪。新值:' + newValue)
})
object.test = 'test2'
// 作爲 watcher 添加的第一個函數,很自豪。新值:test2
let watcher2 = new Watcher(object, 'test', function(newValue, oldValue){
console.log('作爲 watcher 添加的第二個函數,也很自豪。新值:' + newValue)
})
object.test = 'test3'
// 作爲 watcher 添加的第一個函數,很自豪。新值:test3
// 作爲 watcher 添加的第二個函數,也很自豪。新值:test3
// 接着我們來試一下刪除依賴,把 watcher2 給刪除
watcher2.dep.removeSub(watcher2)
object.test = 'test4'
// 作爲 watcher 添加的第一個函數,很自豪。新值:test4
上面的代碼實現了一個Watcher,實例裏面保存了:需要監聽的對象(object)、取值方法(key)、對應的回調(callback)、需要監聽的值(value)、以及取值函數(get)和出觸發函數(update),這樣我們就把依賴相關的所有內容都保存在了這個實例裏面。
爲了保存對Dep的引用,在Watcher中設置了dep,用於保存這個監聽被那個Dep引用了。
編寫代碼的時候我們不用特地的觸發get來添加依賴因爲我們將之前的get值的代碼放在了watcher裏面:
修改前:
Dep.target = function(newValue, oldValue){console.log('我被添加進去了,新的值是:' + newValue)}
object.test
Dep.target = null
修改後:
this.get = function(){
Dep.target = this
let vaule = this.obj[this.getter]
Dep.target = null
return value
}
this.value = this.get()
最終,通過以上的代碼我們解決了之前的問題。
問題:這時候呢我們又出現了一個問題:那就當前的Watcher還是需要優化的,比如如果被多個Dep引用的話,就得存一個數組嚶嚶嚶
setp4 優化Watcher
目前爲止我們做了三個東西
- 通過
defineReactive
函數,實現了對數據取值和設置的監聽!!Object.defineProperty - 通過
Dep
類,實現了依賴的管理 - 通過
Watcher
類,實現瞭解耦等等。
問題:
- 一個
watcher
對應多個屬性。 - 多個
Dep
依賴於同一個Watcher
的情況。
//其中wacher是存入subs數組裏面的一個個元素
let Watcher= function(object,getter,callback){
this.obj=object;
this.getter=getter;
this.cb=callback;
this.deps=[];
this.value=undefined;
this.get=function(){
Dep.target=this;
let value=this.getter.call(object)//有個object、get操作
Dep.target=null;
return value;
}
this.update=function(){
const value=this.getter.call(object);
const oldValue=this.value;
this.value=value;
this.cb.call(this.obj,value,oldValue);
}
this.addDep=function(dep){
this.deps.push(dep);
}
//新調節的取消依賴的方法
this.teardown=function(){
let i=this.deps.length;
while(i--){
this.deps[i].removeSub(this)
}
this.deps=[];
}
this.value=this.get();
}
let Dep=function(){
this.subs=[];
this.addSub=function(sub){
this.subs.push(sub);
}
this.removeSub=function(sub){
const index=this.subs.indexOf(sub);
if(index>-1){
this.subs.splice(index,1)
}
}
this.notify=function(){
this.subs.forEach(watcher => {
watcher.update();
});
}
}
Dep.target=null;
let defineReactive=function(object,key,value){
let dep=new Dep();
Object.defineProperty(object,key,{
configurable:true,
enumerable:true,
get:function(){
if(Dep.target){
dep.addSub(Dep.target)
// 添加 watcher 對 dep 的引用
Dep.target.addDep(dep)
}
return value;
},
set: function(newValue){
if(newValue!=value){
value=newValue;
dep.notify();
}
}
})
}
let object={};
defineReactive(object,'num1',2);
defineReactive(object,'num2',4);
let watcher=new Watcher(object,function(){
return this.num1+this.num2
},function(newValue,oldValue){
console.log(`這是一個監聽函數,${object.num1} + ${object.num2} = ${newValue}`)
});
let watcher2=new Watcher(object,function(){
return this.num1*this.num2
},function(newValue,oldValue){
console.log(`這是一個監聽函數,${object.num1} * ${object.num2} = ${newValue}`);
});
object.num1=4;
object.num2=11;
//測試取消
watcher2.teardown();
object.num1=5;
object.num2=12;
其實最終優化就是兩點:
- 傳入
Watcher
的不再是個key值,而是一個函數,這個函數裏面呢,我們觸發了多個屬性的get
。 - 需要保存多個dep的話,將它用一個數組保存起來就可以了。
- 當然,爲了方便取消這個watcher,我們需要的是添加函數爲了取消所有dep對watcher的依賴。
自己小結一下:
可能有點亂,之前我甚至沒有把watcher和dep之間的關係弄懂,於是我花了個小圖圖,其實就是要實現一個一對多的關係,但是同時爲了watcher能夠保存對dep的引用,我們需要將依賴於此的dep保存到watcher裏面(作用域啥的知識),以便於 引用。
step5 Observe
我們已經有了:
defineReactive
控制了對象屬性,變爲可監聽結構Dep
收集管理依賴Watcher
一個抽象的依賴
defineReactive
和Dep
改造了對象下面的某一個屬性,將一個目標變成了觀察者模式中的目標,目標發生變化的時候會調度觀察者;
Watcher
就是一個觀察者,會註冊到目標裏面。
需要優化的地方:
4. 如果想要對象的屬性都得以響應,必須對對象下的所有屬性進行遍歷,依次調用 defineReactive
不是很方便。
5. 代碼都在一個文件裏面十分不便與管理
最後實現看 https://github.com/CoCoManYY/MyWheel/tree/master/myMVVM/demo/step5
其中的配置採坑筆記
step6 Array
之前的幾個step實現了對象屬性的監聽,但是有關於數組的行爲一直沒有進行處理。
- 調用方法:
arr.splice(1,2,"something","something2")
- 直接賦值:
arr[1]='something'
總結對原數組造成影響的方法:push、pop、shift、unshift、splice、sort、reverse。數組長度發生變化,數組內元素順序發生變化。