用原生 JS 實現 MVVM 框架2——單向綁定

上一篇寫了實現 MVVM 框架的一些基本概念node

本篇用代碼來實現一個完整的 MVVM 框架segmentfault

思考

假設有以下代碼,data裏面的name會和試圖中的{{name}}——一一映射,修改data的值,會直接引發試圖中對應數據的變化數組

<body>
<div id='app'>{{name}}</div>
<script>
function MVVM(){
    //todo...
}
var vm = new MVVM({
    el:'#app',
    data:{
        name:'zhangsan'
    }
})
</script>
</body>

如何實現上述 MVVM 呢?app

回想下這篇講的觀察者模式和數據監聽:框架

  1. 主題(subject)是什麼?
  2. 觀察者(observer)是什麼?
  3. 觀察者什麼時候訂閱主題?
  4. 主題什麼時候通知更新?

簡單回答下:
上面例子中,主題應該是dataname屬性,觀察者是試圖裏的{{name}},當一開始執行 MVVM 初始化(根據el解析模板發現{{name}})的時候訂閱主題,當data.name發生改變的時候,通知觀察者更新內容,咱們能夠在一開始監控data.name,當用戶修改data.name的時候調用主題的subject.ontifymvvm

單向綁定

有以下 HTML函數

<div id="app">
    <h1>{{name}}'is age is {{age}}</h1>
</div>

從上面 HTML 中咱們看出,操做的節點是div#app,須要的數據是nameage,因此實例化 MVVM 能夠須要傳遞兩個參數elementdatathis

let vm = MVVM({
    element:'#app',
    data:{
        name:'zhangsan',
        age:20
    }
})
setInterval(function(){
    vm.data.age++
},2000)

咱們 MVVM 的構造函數應該怎麼寫呢?咱們只須要作兩件事情:雙向綁定

  1. 咱們須要觀察這些數據,當之後這些數據變更時,會作一些事情去調用
  2. 須要解析這個模板,把模板中的一些符號替換成對應的數據

初始化是必須作的,將實例化的數據存在自身上面,後面要用,這裏就不敘述了。code

class MVVM{
    constructor(options){
        init(options)
        observe(this.data)
        this.compile()
    }
    init(options){
        this.element = document.querySelector(options.element)
        this.data = options.data
    }
}

先看compile這個方法,它就是在編譯頁面中的節點,若是節點裏還有孩子,須要再去遍歷這些孩子,若是遍歷到文本,就進行下一步文本替換。

compile(){    //雖然這裏能夠直接對節點進行遍歷,但最好仍是分開來比較好點
    this.traverse(this.el)
}
traverse(node){        //對節點進行遍歷,若是遇到元素節點,用遞歸繼續遍歷直到遍歷到都是文本爲止,進行下一步頁面渲染
    node.childNodes.forEach(childNode=>{
        if(childNode.nodeType === 1){
            this.traverse(childNode)
        }else if(childNode.nodeType === 3){
            this.renderText(childNode)
        }
    })
}
renderText(textNode){    //到這一步,已經獲取到頁面中的文本了,用正則去匹配
    let reg = /{{([^}]*)}}/g    //正則或者能夠寫稱/{{(.+?)}}/g
    let match
    while(match = reg.exec(textNode.textContent)){    //將匹配到的內容賦值給match,match是一個數組
        let raw = match[0]    
        let key = match[1].trim() 
        textNode.textContent = textNode.textContent.replace(raw,this.data[key])    //頁面渲染
        new Observer(this,key,function(val,oldVal){
                textNode.textContent = textNode.textContent.replace(oldVal,val)
            })    //建立一個觀察者
    }
}

假設用戶去修改數據時,那數據該如何進行實時的變更呢?

這裏就引入了觀察者和主題的概念,咱們在解析的過程當中建立一個個觀察者,這個觀察者就觀察這個屬性,解析到下個屬性在建立一個觀察者,並觀察這個屬性。

觀察這個屬性就是訂閱這個主題,咱們在this.compile()解析完後建立一個觀察者,它有個方法,若是這個屬性變更,我就會修改頁面。

function observe(data){
    if(!data || typeof data !== 'object')return
    for(let key in data){
        let val = data[key]
        let subject = new Subject()    //建立主題
        if(typeof val === 'object'){
            observe(val)
        }
        Object.defineProperty(data,key,{
            configurable:true,
            enumerable:true,
            get(){
                return val
            },
            set(newVal){
                val = newVal
                subject.notify()
            }
        })
    }
}

問題是建立了觀察者後何時去觀察這個主題?

在建立後馬上觀察這個主題,但是主題在哪?觀察者有了,就是剛剛new的時候。主題是在observe遍歷屬性時建立的。主題存在在observe局部變量中,外面是訪問不到的,那觀察者怎樣訂閱這個主題呢?

思考到這裏發現行不通了,就須要換種思路了。

當建立觀察者時,會調用getValue(),它作什麼事情呢,把我設置爲場上權限最高的觀察者,由於頁面中有不少觀察者,此時this.key,就是我要訂閱的主題,當我調用this.vm.data[this.key]就等於調用了observeget方法,由於剛剛我已經把觀察者設置爲場上權限最高者,此時currentObserver是存在的,這時觀察者就開始訂閱主題,訂閱的以後在把權限去掉

let currentObserver = null
class Observer{
    constructor(vm,key,cb){
        this.subjects = {}
        this.vm = vm
        this.key = key
        this.cb = cb
        this.value = this.getValue()
    }
    getValue(){
        currentObserver = this
        let value = this.vm.data[this.key]
        currentObserver = null
        return value
    }
}

經過currentObserver去訂閱主題,由於在建立觀察者時調用了getValue方法,把currentObserver設置爲Observer,經過它去訂閱主題

get:function(){
    if(currentObserver){
        currentObserver.subscribeTo(subject)
    }
}

主題的構造函數

let id = 0
class Subject{
    constructor(){
        this.id = id++
        this.observers = []
    }
    addObserver(observer){
        this.observers.push(observer)
    }
    notify(){
        this.observers.forEach(observer=>{
            observer.update()
        })
    }
}

添加觀察者

subscribeTo(subject){
    if(!this.subjects[subject.id]){
        subject.addObserver(this)
        this.subjects[subject.id] = subject
    }
}

更新頁面數據,舊值經過自身屬性獲取,新值經過getValue方法獲取

update(){
    let oldVal = this.value
    let value = this.getValue()
    if(value !== oldVal){
        this.value = value
        this.cb.call(this.vm,value,oldVal)
    }
}

最後貼上完整的單向綁定的代碼

function observe(data){
    if(!data || typeof data !== 'object')return
    for(let key in data){
        let val = data[key]
        let subject = new Subject()
        if(typeof val === 'object'){
            observe(val)
        }
        Object.defineProperty(data,key,{
            configurable:true,
            enumerable:true,
            get(){
                if(currentObserver){
                    currentObserver.subscribeTo(subject)
                }
                return val
            },
            set(newVal){
                val = newVal
                subject.notify()
            }
        })
    }
}
let id = 0
class Subject{
    constructor(){
        this.id = id++
        this.observers = []
    }
    addObserver(observer){
        this.observers.push(observer)
    }
    notify(){
        this.observers.forEach(observer=>{
            observer.update()
        })
    }
}
let currentObserver = null
class Observer{
    constructor(vm,key,cb){
        this.subjects = {}
        this.vm = vm
        this.key = key
        this.cb = cb
        this.value = this.getValue()
    }
    update(){
        let oldVal = this.value
        let value = this.getValue()
        if(value !== oldVal){
            this.value = value
            this.cb.call(this.vm,value,oldVal)
        }
    }
    subscribeTo(subject){
        if(!this.subjects[subject.id]){
            subject.addObserver(this)
            this.subjects[subject.id] = subject
        }
    }
    getValue(){
        currentObserver = this
        let value = this.vm.data[this.key]
        currentObserver = null
        return value
    }
}
class mvvm{
    constructor(options){
        this.init(options)
        observe(this.data)
        this.compile()
    }
    init(options){
        this.el = document.querySelector(options.el)
        this.data = options.data
    }
    compile(){
        this.traverse(this.el)
    }
    traverse(node){
        node.childNodes.forEach(childNode=>{
            if(childNode.nodeType === 1){
                this.traverse(childNode)
            }else if(childNode.nodeType === 3){
                this.renderText(childNode)
            }
        })
    }
    renderText(textNode){
        let reg = /{{([^}]*)}}/g
        let match
        while(match = reg.exec(textNode.textContent)){
            let raw = match[0]
            let key = match[1].trim()
            textNode.textContent = textNode.textContent.replace(raw,this.data[key])
            new Observer(this,key,function(val,oldVal){
                textNode.textContent = textNode.textContent.replace(oldVal,val)
            })
        }
    }
}
let vm = new mvvm({
    el:'#app',
    data:{
        name:'uccs',
        age:20
    }
})
setInterval(function(){
    vm.data.age++
},2000)

本篇詳細講述了 MVVM 單項綁定的原理,下一篇講述雙向綁定

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

相關文章
相關標籤/搜索