實現一個簡單的雙向綁定

接觸Vue有一段時間了,可是對於其雙向綁定的實現一直是似懂非懂,今天看到一篇寫的比較好的文章 傳送門1 根據原做者的指導本身也去實現了一遍簡單的 demo (本文的demo均基於Object.defineProperty 實現數據劫持,利用了對Vue.js實現雙向綁定的思想)html

[注]本文全部圖片均來自於:傳送門2vue

前言

幾種主流的雙向綁定

1.發佈-訂閱模式
2.髒值檢測node

經過對比數據是否有變動,來決定是否更新視圖。最簡單的能夠經過定時輪詢去檢測數據的變更。固然Google不會這麼low, Angular 只有在指定事件觸發時進入髒值檢測:git

  • DOM事件,好比用戶輸入文本點擊按鈕等(ng-click)
  • XHR響應事件
  • 瀏覽器 Location 變動
  • Timer事件
  • 執行 $digidt() 或 $apply()

3.數據劫持
Vue.js 採用的是 數據劫持+發佈/訂閱模式 的方式,經過 Object.defineProperty() 來劫持各個屬性的 setter/getter, 在數據變更時發佈消息給訂閱者(Wacther), 觸發相應的監聽回調。下圖展現了Vue實現雙向綁定的流程
圖片描述github

實現一個簡單的雙向綁定

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>雙向綁定最最最初級demo</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <div id="app">
            <input type="text" id="txt">
            <p id="show-txt"></p>
        </div>
    </body>
    <script>
        var obj={}
        Object.defineProperty(obj,'txt',{
            get:function(){
                return obj
            },
            set:function(newValue){
                document.getElementById('txt').value = newValue
                document.getElementById('show-txt').innerHTML = newValue
            }
        })
        document.addEventListener('keyup',function(e){
            obj.txt = e.target.value
        })
    </script>
</html>

進階版demo

DOM操做是很是耗時和好性能,因此在優化過程當中先從DOM操做入手。由於遍歷解析過程當中有屢次DOM操做,爲了提升性能和效率,須要一種方法來避免對DOM元素的直接封裝操做。在Vue使用 DocumentFragment 做爲替代容器。
DocumentFragment 接口表示一個沒有父級文件的最小文檔對象。它被當作一個輕量版本的Document 使用。因此使用 DocumentFragment 代替DOM直接處理,能夠提升性能和速度。瀏覽器

封裝DOM節點爲 DocumentFragment

//將傳入 node 的子節點進行劫持,通過處理後從新掛載回目標節點
function convertNode(node,vm){
    var fragment = document.createDocumentFragment(),
        child
    while(child = node.firstChild){
        //將原生節點拷貝到 fragment,並刪除以前的child節點
        fragment.appendChild(child)
    }
    return fragment
}

var dom = convertNode(document.getElementById('app'))
document.getElmentById('app').appendChild(dom)

實現Complie解析模板指令

圖片描述

Complie 主要作的事情就是解析模板指令,將模板中的變量替換爲數據。因此要遍歷整個DOM樹,進行掃描解析編譯,調用對應的指令渲染函數進行渲染,並調用對應的指令更新函數進行綁定app

<div id="app">
    <input type="text" id="txt" h-model="text">
    {{text}}
</div>
function convertNode(node,vm){
    //...
    while(child = node.firstChild){
        Compile(child,vm)
        fragment.appendChild(child)
    }
    return fragment
}
function Compile(node,vm){
    var reg = /\{\{(.*)\}\}/
    if(node.nodeType===1){  
        var attr = node.attributes  //對全部屬性進行解析
        for(var i=0;i<attr.length;i++){
            if(attr[i].nodeName=='h-model'){
                //將元素與數據綁定
                var bindName = attr[i].nodeValue
                //爲輸入框添加事件監聽觸發
                node.addEventListener('input',function(e){
                    vm.data[bindName] = e.target.value
                    node.value = vm.data[bindName]
                })
                node.removeAttribute('h-model');
            }
            if(node.nodeType===3){
                if(reg.test(node.nodeValue)){
                    var bindName = RegExp.$1.trim()
                    console.log(RegExp.$1)
                    node.nodeValue = vm.data[bindName]
                }
            }
        }
    }

ViewModel 層向 View 層的數據綁定

接下來實現一個 Xin 構造器,經過 Compile 來解析模板指令,經過 Observer 監聽屬性數據的變化實現 Model 層向 View 層的數據綁定dom

function Xin(options){
    this.data = options.data
    Observer(this.data,this)
    var id = options.el
    var dom = convertNode(document.getElementById(id),this)
    document.getElementById(id).appendChild(dom)
}

新建一個 vm 實例來測試一下 Model --> View 的綁定狀況mvvm

var vm = new Xin({
    el:'app',
    data:{
        text:'Hello MVVM'
    }
})

View 層向 viewModel 層的數據綁定

實際上,在 Observer 中咱們已經經過 數據劫持 實現了監聽每一個數據的變化,在控制檯打印 console.log(val) 就能夠實時看到數據的變化。因此接下來實現的關鍵就是怎麼用監聽到的數據去更新視圖。
在這裏,咱們去實現一個 Wacther 能夠將它理解爲觀察者 ,他的做用是可以接收從 Observer 發過來的屬性變更通知, 而後根據屬性的變更更新視圖 update函數

在監聽過程當中,爲全部的 data 屬性生成一個主題對象 Dep,Dep中包含須要維護的觀察者列表。每當主題對象狀態發生變化時,其相關依賴都會獲得通知,而且被自動更新(數據變更會觸發notify,再調用訂閱者的update() 方法)

function Dep(){
    this.subs=[]    //訂閱者隊列
}
Dep.prototype={
    addSub:function(sub){
        this.subs.push(sub)
    },
    notify:function(){
        this.subs.forEach(function(sub){
            sub.update()
        })
    }
}

funcion Watcher(vm,node,bindName){
    //將全局Dep.target設置爲當前頁面元素node
    Dep.target = this
    //完成watcher的初始化
    this.name = bindName
    this.node = node
    this.vm = vm

    this.update()    //初次綁定時進行更新
    Dep.target = null    //保證Dep.target惟一
}

Watcher.prototype = {
    get:function(){
        this.value = this.vm.data[this.name]
    },
    update:function(){
        this.get()
        this.node.nodeValue = this.value
    }
}

function Observer(obj,vm){
    //...
    Object.defineProperty(obj,prop,{
        get:function(){...},
        set:function(newVal){
            if(val == newVal) return
            val = newVal
            //data屬性被修改,由dep觸發view層更新
            dep.notify()
        }
    })
}

考慮這樣一個問題,何時會有雙向綁定? viewModel --> view 可能會發生在全部類型的DOM節點上,而 view --> viewModel 只能發生在 input, select, textarea 等交互控件上。因此將文本節點包裝成 Watcher , 添加相關元素的觀察者列表中,Watcher 負責更新頁面元素

function Compile(node,vm){
    //...
    if(node.nodeType ===3){    //文本節點類型
        if(reg.test(node.nodeValue)){
            var bindName = RegExp.$1.trim()
            new Watcher = (vm,node,bindName)    //爲該頁面元素node生產watcher
        }
    }
}

更新

本文中實現模板渲染的方法借鑑了 Vue 1.x 中實現模板渲染的方法。
Vue 2.x 模板渲染 方法借鑑React 中的 VirtualDOM,基於 VirtualDOM。 Vue 2.x 還支持服務端渲染SSR

資料參考

1.https://github.com/DMQ/mvvm#_2

相關文章
相關標籤/搜索