在前端頁面中,把 Model 用純 JS 對象表示,View 負責顯示,二者作到了最大化的分離前端
把 Model 和 View 關聯起來的就是 ViewModel。ViewModel 負責把 Model 的數據同步到 View 中顯示出來,還負責把 View 的修改同步回 Model。segmentfault
MVVM 的設計思想:關注 Model 的變化,讓 MVVM 框架去自動更新 DOM 的狀態,從而把開發者從操做 DOM 的繁瑣步驟中解脫出來。數組
瞭解了 MVVM 思想後,本身用原生 JS 實現一個 MVVM 框架。微信
實現 MVVM 框架前先來看幾個基本用法:框架
普通聲明對象,定義和修改屬性函數
let obj = {} obj.name = 'zhangsan' obj.age = 20
用ObjectdefineProperty
聲明對象
語法:this
Object.defineProperty(obj,prop,descriptor)
obj
:要處理的目標對象prop
:要定義或修改的屬性的名稱descriptor
:將被定義或修改的屬性描述符let obj = {} Object.defineProperty(obj,'age',{ value = 14, })
咋一看有點多此一舉,這不很雞肋嘛prototype
別急,往下看設計
descriptor
有兩種形式:數據描述符和存儲描述符,他們兩個共有屬性:code
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]
,並將這個變量返回出去。一個典型的觀察者模式應用場景——微信公衆號
用代碼怎麼實現呢?先看邏輯:
Subject 是構造函數,new Subject()建立一個主題對象,它維護訂閱該主題的一個觀察者數組數組(舉例來講:Subject 是騰訊推出的公衆號,new Subject() 是一個某個機構的公衆號——新世相,它要維護訂閱這個公衆號的用戶羣體)
主題上有一些方法,如添加觀察者addObserver
、刪除觀察者removeObserver
、通知觀察者更新notify
(舉例來講:新世相將用戶分爲兩組,一組是忠粉就是 addObserver,一組是黑名單就是:removeObserver,它在忠粉組能夠添加用戶,能夠在黑名單里拉黑一些槓精,若是有福利發放,它就會統治忠粉裏的用戶:notify)
Observer 是構造函數,new Observer() 建立一個觀察者對象,該對象有一個update
方法(舉例來講:Observer 是忠粉用戶羣體,new Observer() 是某個具體的用戶——小王,他必需要打開流量才能收到新世相的福利推送:updata)
當調用notify
時實際上調用所有觀察者observer
自身的update
方法(舉例來講:當新世相推送福利時,它會自動幫忠粉組的用戶打開流量,這比較極端,只是用來舉例)
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...
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——單向綁定