用原生 JS 實現 MVVM 框架1——觀察者模式和數據監控

在前端頁面中,把 Model 用純 JS 對象表示,View 負責顯示,二者作到了最大化的分離前端

把 Model 和 View 關聯起來的就是 ViewModel。ViewModel 負責把 Model 的數據同步到 View 中顯示出來,還負責把 View 的修改同步回 Model。segmentfault

MVVM 的設計思想:關注 Model 的變化,讓 MVVM 框架去自動更新 DOM 的狀態,從而把開發者從操做 DOM 的繁瑣步驟中解脫出來。數組

瞭解了 MVVM 思想後,本身用原生 JS 實現一個 MVVM 框架。微信

實現 MVVM 框架前先來看幾個基本用法:框架

Object.defineProperty

普通聲明對象,定義和修改屬性函數

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

writableconfigurable的區別是前者是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    //這裏註釋掉了,實際設置屬性就是這樣寫的。
            }
        })
    }
}

注意兩點:

  1. 咱們在聲明let val = data[key]時,不能用var,由於這裏須要對每一個屬性進行監控,用let每次遍歷都會建立一個新的val,在進行賦值;若是用var,只有第一次纔是聲明,後面都是對一次聲明val進行賦值,遍歷結束後,獲得的是最後一個屬性,顯然這不是咱們須要的。
  2. get方法裏,return就是前面聲明的val,這裏不能用data[key],會報錯。由於調用data.name,就是調用get方法時,獲得的結果是data.name,又繼續調用get方法,就隨變成死循環,因此這裏須要用一個變量來存儲data[key],並將這個變量返回出去。

觀察者模式

一個典型的觀察者模式應用場景——微信公衆號

  1. 不一樣的用戶(咱們把它叫作觀察者:Observer)均可以訂閱同一個公衆號(咱們把它叫作主體:Subject)
  2. 當訂閱的公衆號更新時(主體),用戶都能收到通知(觀察者)

用代碼怎麼實現呢?先看邏輯:

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——單向綁定

相關文章
相關標籤/搜索