在前端頁面中,把 Model 用純 JS 物件表示,View 負責顯示,兩者做到了最大化的分離
把 Model 和 View 關聯起來的就是 ViewModel。ViewModel 負責把 Model 的資料同步到 View 中顯示出來,還負責把 View 的修改同步回 Model。
MVVM 的設計思想:關注 Model 的變化,讓 MVVM 框架去自動更新 DOM 的狀態,從而把開發者從操作 DOM 的繁瑣步驟中解脫出來。
瞭解了 MVVM 思想後,自己用原生 JS 實現一個 MVVM 框架。
實現 MVVM 框架前先來看幾個基本用法:
Object.defineProperty
普通宣告物件,定義和修改屬性
let obj = {}
obj.name = 'zhangsan'
obj.age = 20
用ObjectdefineProperty
宣告物件
語法:
Object.defineProperty(obj,prop,descriptor)
-
obj
:要處理的目標物件 -
prop
:要定義或修改的屬性的名稱 -
descriptor
:將被定義或修改的屬性描述符
let obj = {}
Object.defineProperty(obj,'age',{
value = 14,
})
咋一看有點畫蛇添足,這不很雞肋嘛
別急,往下看
描述符
descriptor
有兩種形式:資料描述符和儲存描述符,他們兩個共有屬性:
-
configurable
,是否可刪除,預設為false
,定義後無法修改 -
enumerable
,是否可遍歷,預設為false
,定以後無法修改
共有屬性
configurable
設定為false
時,其內部屬性無法用delete
刪除;如要刪除,需要把configurable
設定為true
。
let obj = {}
Object.defineProperty(obj,'age',{
configurable:false,
value:20,
})
delete obj.age //false
enumerable
設定為false
時,其內部屬性無法遍歷;如需遍歷,要把enumerable
設定為true
let obj = {name:'zhangsan'}
Object.defineProperty(obj,'age',{
enumerable:false,
value:20,
})
for(let key in obj){
console.log(key) //name
}
資料描述符
value
:該屬性對應的值,預設為undefined
。writable
:當且緊當為true
時,value
才能被賦值運算子改變。預設為false
。
let obj = {}
Object.defineProperty(obj,'age',{
value:10,
writable:false
})
obj.age = 11
obj.age //10
writable
和configurable
的區別是前者是value
能否被修改,後者是value
能否被刪除。
儲存描述符
get()
:一個給屬性提供getter
的方法,預設為undefined
。set()
:一個給屬性提供setter
的方法,預設為undefined
。
let obj = {}
let age
Object.defineProperty(obj,'age',{
get:function(){
return age
},
set:function(newVal){
age = newVal
}
})
obj.age = 20
obj.age //20
當我呼叫obj.age
時,其實是在向obj
物件要age
這個屬性,它會幹嘛呢?它會呼叫obj.get()
方法,它會找到全域性變數age
,得到undefined
。
當我設定obj.age = 20
時,它會呼叫obj.set()
方法,將全域性變數age
設定為20
。
此時在呼叫obj.age
,得到20
。
注意:資料描述符和儲存描述符不能同時存在,否則會報錯
let obj = {}
let age
Object.defineProperty(obj,'age',{
value:10, //報錯
get:function(){
return age
},
set:function(newVal){
age = newVal
}
})
資料攔截
使用Object.defineProperty
來實現資料攔截,從而實現資料監聽。
首先有一個物件
let data = {
name:'zhangsan',
friends:[1,2,3,4]
}
下面寫一個函式,實現對data
物件的監聽,就可以在內部做一些事情
observe(data)
換句話說,就是data
內部的屬性都被我們監控的,當呼叫屬性時,就可以在上面做些手腳,使得返回的值變掉;當設定屬性時,不給他設定。
當然這樣做很無聊,只是想說明,我們可以在內部做手腳,實現我們想要的結果。
那observe
這個函式應該怎麼寫呢?
function observe(data){
if(!data || typeof data !== 'object')return //如果 data 不是物件,什麼也不做,直接跳出,也就是說只對 物件 操作
for(let key in data){ //遍歷這個物件
let val = data[key] //得到這個物件的每一個`value`
if(typeof val === 'object'){ //如果這個 value 依然是物件,用遞迴的方式繼續呼叫,直到得到基本值的`value`
observe(val)
}
Object.defineProperty(data,key,{ //定義物件
configurable:true, //可刪除,原本的物件就能刪除
enumerable:true, //可遍歷,原本的物件就能遍歷
get:function(){
console.log('這是假的') //呼叫屬性時,會呼叫 get 方法,所以呼叫屬性可以在 get 內部做手腳
//return val //這裡註釋掉了,實際呼叫屬性就是把值 return 出去
},
set:function(newVal){
console.log('我不給你設定。。。') //設定屬性時,會呼叫 set 方法,所以設定屬性可以在 set 內部做手腳
//val = newVal //這裡註釋掉了,實際設定屬性就是這樣寫的。
}
})
}
}
注意兩點:
- 我們在宣告
let val = data[key]
時,不能用var
,因為這裡需要對每個屬性進行監控,用let
每次遍歷都會建立一個新的val
,在進行賦值;如果用var
,只有第一次才是宣告,後面都是對一次宣告val
進行賦值,遍歷結束後,得到的是最後一個屬性,顯然這不是我們需要的。 -
get
方法裡,return
就是前面宣告的val
,這裡不能用data[key]
,會報錯。因為呼叫data.name
,就是呼叫get
方法時,得到的結果是data.name
,又繼續呼叫get
方法,就隨變成死迴圈,所以這裡需要用一個變數來儲存data[key]
,並將這個變數返回出去。
觀察者模式
一個典型的觀察者模式應用場景——微信公眾號
- 不同的使用者(我們把它叫做觀察者:Observer)都可以訂閱同一個公眾號(我們把它叫做主體:Subject)
- 當訂閱的公眾號更新時(主體),使用者都能收到通知(觀察者)
用程式碼怎麼實現呢?先看邏輯:
Subject 是建構函式,new Subject()建立一個主題物件,它維護訂閱該主題的一個觀察者陣列陣列(舉例來說:Subject 是騰訊推出的公眾號,new Subject() 是一個某個機構的公眾號——新世相,它要維護訂閱這個公眾號的使用者群體)
主題上有一些方法,如新增觀察者addObserver
、刪除觀察者removeObserver
、通知觀察者更新notify
(舉例來說:新世相將使用者分為兩組,一組是忠粉就是 addObserver,一組是黑名單就是:removeObserver,它在忠粉組可以新增使用者,可以在黑名單里拉黑一些槓精,如果有福利發放,它就會統治忠粉裡的使用者:notify)
Observer 是建構函式,new Observer() 建立一個觀察者物件,該物件有一個update
方法(舉例來說:Observer 是忠粉使用者群體,new Observer() 是某個具體的使用者——小王,他必須要開啟流量才能收到新世相的福利推送:updata)
當呼叫notify
時實際上呼叫全部觀察者observer
自身的update
方法(舉例來說:當新世相推送福利時,它會自動幫忠粉組的使用者開啟流量,這比較極端,只是用來舉例)
ES5 寫法:
function Subject(){
this.observers = []
}
Subject.prototype.addObserver = function(observer){
this.observers.push(observer)
}
Subject.prototype.removeObserver = function(observer){
let index = this.observers.indexOf(observer)
if(index > -1){
this.observers.splice(index,1)
}
}
Subject.prototype.notify = function(){
this.observers.forEach(observer=>{
observer.update()
})
}
function Observer(name){
this.name = name
this.update = function(){
console.log(name + ' update...')
}
}
let subject = new Subject() //建立主題
let observer1 = new Observer('xiaowang') //建立觀察者1
subject.addObserver(observer1) //主題新增觀察者1
let observer2 = new Observer('xiaozhang') //建立觀察者2
subject.addObserver(observer2) //主題新增觀察者2
subject.notify() //主題通知觀察者
/**** 輸出 *****/
xiaowang update...
xiaozhang update...
ES6 寫法:
class Subject{
constructor(){
this.observers = []
}
addObserver(observer){
this.observers.push(observer)
}
removeObserver(observer){
let index = this.observers.indexOf(observer)
if(index > -1){
this.observers.splice(index,1)
}
}
notify(){
this.observers.forEach(observer=>{
observer.update()
})
}
}
class Observer{
constructor(name){
this.name = name
this.update = function(){
console.log(name + ' update...')
}
}
}
let subject = new Subject() //建立主題
let observer1 = new Observer('xiaowang') //建立觀察者1
subject.addObserver(observer1) //主題新增觀察者1
let observer2 = new Observer('xiaozhang') //建立觀察者2
subject.addObserver(observer2) //主題新增觀察者2
subject.notify() //主題通知觀察者
/**** 輸出 *****/
xiaowang update...
xiaozhang update...
ES5 和 ES6 寫法效果一樣,ES5 的寫法更好理解,ES6 只是個語法糖
主題新增觀察者的方法subject.addObserver(observer)
很繁瑣,直接給觀察者下方許可權,給他們增加新增進忠粉組的許可權
class Observer{
constructor() {
this.update = function() {
console.log(name + ' update...')
}
}
subscribeTo(subject) { //只要使用者訂閱了主題就會自動新增進忠粉組
subject.addObserver(this) //這裡的 this 是 Observer 的例項
}
}
let subject = new Subject()
let observer = new Observer('lisi')
observer.subscribeTo(subject) //觀察者自己訂閱忠粉分組
subject.notify()
/****** 輸出 *******/
lisi update...
MVVM 框架的內部基本原理就是上面這些,下一篇用程式碼寫一遍完整的 MVVM 框架。
用原生 JS 實現 MVVM 框架MVVM 框架系列:
用原生 JS 實現 MVVM 框架2——單向繫結