手寫一個Vue數據綁定(配圖文)

學前準備:Object.defineProperty觀察者模式

index.html 的準備:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <script src="./MVVM.js"></script>
    <div id="app">
        <p>{{hi}}</p>
        <p>{{a}}</p>
        <p>{{b.b}}</p>
        <p>歡迎你</p>
    </div>
    <script>
        let vm = new MVVM({
            el: '#app',
            data: {
                hi: 'hello world',
                a: 1,
                b: {
                    a: 1,
                    b: 2
                },
            }
        })
    </script>
</body>
</html>

MVVM.js

注意:我們所有的vue數據綁定代碼的寫在裏面:

function MVVM(options) {
    this.$options = options;	// 將傳過來的 options 掛載在對象上
    this.$el = document.querySelector(options.el);	//獲取 options.el 中的元素,並將它也掛載在對象上;
    this.data = options.data;	// 這個也是掛載在對象上;

    compile(this.$el,this);		// 調用 compile函數,主要是把我們在 this.$el 上得到的元素進行編譯,將原來只有{{}}的元素節點替換成應該有的數據
    
    function compile(el,vm) {
        // 創建一個新的空白的文檔片段,暫時開闢的空間
        let fragment = document.createDocumentFragment();
        // 遍歷 this.$el 中的所有子元素,並將它們插入到 fragment 文檔片段中
        while(firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        
        
        // 調用 replace 函數 ,該函數的主要作用是:將 fragment 中沒數據的元素替換成有數據的元素;
        replace(fragment);
        
        
        function replace(fragment) {
            // 因爲 fragment.childNodes 是一個類數組,最好是轉換爲數組,那樣好操作
            [...fragment.childNodes].forEach((node) => {
               	// 定義我們要使用的正則表達式
                let reg = /\{\{(.*)\}\}/;      
                // 獲取 node 中的文本內容
                let content = node.textContent;
                // 判斷元素節點的類型,並對它的文本內容進行正則的匹配(就是匹配 {{}} )。
                if (node.nodeType === 1 && reg.test(content) ){
                    let arrs = RegExp.$1.split('.');		// 獲取我們匹配到的文本內容
                    let val = vm.data;						// 將 this.data 賦值給 val
                    // 循環取值,直到將取到值
                    arrs.forEach((item) => {
                        val = val[item];
                    })
                    
                    // 替換數據,例如將 {{b.b}}的數據替換爲 2
                    node.textContent = content.replace(/\{\{(.*)\}\}/,val);
                }
                if (node.childNodes) {	// 如果元素節點還有節點的話,再次調用 replace
                    replace(node);
                }
            })
        }

        
        
        // 將更新好的元素全部從fragment文檔片段插入到 this.$el 中
        el.appendChild(fragment);
    }


}
  • 如果不瞭解document.createDocumentFragment() 請參見:https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createDocumentFragment
  • 使用document.createDocumentFragment() 該方法的原因是:該方法會在內存中開闢一片空間,也就意味着它會給我們帶來更好的性能。
  • 如果不瞭解RegExp.$1,請參見:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp/n

這是原來的樣子:

在這裏插入圖片描述

現在的樣子:
在這裏插入圖片描述

但是有一個問題如果我們去修改 data 中的數據,在視圖中是不會進行修改的,這是爲什麼呢?其實就是我們開始提到過的 Object.defineProperty ,現在我們來了解一下它把。

該方法允許精確添加或修改對象的屬性,它可以來控制一個對象屬性的一些特有操作,比如讀寫權、是否可以枚舉,這裏我們主要先來研究下它對應的兩個描述屬性get和set,如果還不熟悉其用法,請參照MDN的文檔:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

這是一個例子,不是在 MVVM.js 中的代碼:

var data = {}
var name = '張三'
Object.defineProperty(data, 'name', {
    get: function () {			// 這是讀取操作
        return '我的名字是:' + name
    },
    set: function (newVal) {		// 這是修改操作
        if (newVal === name) {
            console.log('我的名字沒有修改:' + name);
        } else {
            name = newVal;
            console.log('我的名字修改成了:' + name);
        }
    }
})
 
console.log(data.name);  // 我的名字是:張三
data.name = '李四';	// 我的名字修改成了:李四
data.name = '李四';	// 我的名字沒有修改:李四

做了這個例子,應該理解了,但是還有一個觀察者模式,這個不好敘述,我還是上圖吧:

在這裏插入圖片描述

上圖中一些重要函數的作用:

  1. 監聽器Observe:用來劫持並監聽所有屬性,如果有變動的,就通知訂閱者

  2. 訂閱者Watcher:作爲連接Observer和Compile的橋樑,能夠訂閱並收到每個屬性變動的通知,執行指令綁定的相應回調函數,從而更新視圖

  3. 解析器Compile:可以掃描和解析每個節點的相關指令,並根據初始化模板數據以及初始化相應的訂閱器。

  4. Dep :用來保存訂閱者Watcher

修改代碼:


function MVVM(options) {
    this.data = options.data;	
    // 上面的都不變
    
    // 監聽器Observe:主要是劫持 this.data 中的所有的數據,如果有變動的,就通知Dep,而Dep再來通知訂閱者Watcher。
    observe(options.data);
    
    function observe(data) {
        if (typeof(data) !== "object") return;
        defineOneObserve(data);
    }
    function defineOneObserve(data) {
        let dep = new Dep();		// new 一個 dep 對象,在後面的 get 中進行添加,在 set 中進行發佈
        for (let key in data) {
            let val = data[key];
            observe(val);
            Object.defineProperty(data,key,{
                get:function(){
                    // 判斷靜態屬性 Dep.target是否有值(短路),如果沒有就不執行,如果有的話,就把 Dep.target 添加到 dep 中
                    Dep.target && dep.addSub(Dep.target);
                    return val;
                },
                set: function(newVal) {
                    if (val === newVal) return val;
                    val = newVal;
                    observe(val);                        
                    dep.notify(); 	// 修改了值就觸發 dep.notify 發佈,觸發 Watcher,進而更新視圖
                }
            })
        }
    }
    
    // Dep 用來存儲 Watcher,(觀察者模式)
    function Dep() {
        this.cacheList = [];	// 每項都是一個Watcher
    }
    Dep.prototype.addSub = function(sub) {
        this.cacheList.push(sub);	// 添加 Watcher
    }
    Dep.prototype.notify = function() {
        // 觸發 Watcher
        this.cacheList.forEach(item => item.update()); 
    }
    
    function Watcher(vm, exp, fn) {
        // vm 是 MVVM,
        // exp 是 {{}} 中的文本,也是屬性 : b.b
        // fn 是 回調函數,用來替換視圖
        this.vm = vm;
        this.exp = exp;
        this.fn = fn;
        let val = vm.data;
        let arrs = exp.split('.');
        Dep.target = this;	// 將 Watcher 掛載在 Dep.target 上
        arrs.forEach(key => {
            val = val[key];		// 觸發 get ,添加 Watcher
        })
        Dep.target = null;		// 添加完成,Dep.target 重新設置爲空
    }
    Watcher.prototype.update = function() {
        let val = this.vm.data;
        let arrs = this.exp.split('.');
        arrs.forEach(key => {
            val = val[key];
        })				// 拿到最新的值
        this.fn(val);	// 調用回調函數
    }
    
    compile(this.$el,this);
    function replace(fragment) {
        [...fragment.childNodes].forEach((node) => {
            // 省略...
            arrs.forEach((item) => {
                val = val[item];
            })
            if (node.nodeType === 1 && reg.test(content) ){
                // 添加以下
                // 觸發 Watcher ,並將它保存到 dep 中,並且這裏形成了閉包(回調函數)
                new Watcher(vm,RegExp.$1,function(newVal){		
                    node.textContent = content.replace(/\{\{(.*)\}\}/,newVal);
                });
                // 添加以上
            }
        })
    }
}

如圖:
在這裏插入圖片描述

完整代碼:MVVM.js:

function MVVM(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.data = options.data;
    observe(options.data);
    function observe(data) {
        if (typeof(data) !== "object") return;
        defineOneObserve(data);
    }
    function defineOneObserve(data) {
        let dep = new Dep();
        for (let key in data) {
            let val = data[key];
            observe(val);
            Object.defineProperty(data,key,{
                get:function(){
                    Dep.target && dep.addSub(Dep.target);
                    return val;
                },
                set: function(newVal) {
                    if (val === newVal) return val;
                    val = newVal;
                    observe(val);                        
                    dep.notify();  
                }
            })
        }
    }

    function Dep() {
        this.cacheList = [];
    }
    Dep.prototype.addSub = function(sub) {
        this.cacheList.push(sub);
    }
    Dep.prototype.notify = function() {
        this.cacheList.forEach(item => item.update());
    }
    function Watcher(vm, exp, fn) {
        this.vm = vm;
        this.exp = exp;
        this.fn = fn;
        let val = vm.data;
        let arrs = exp.split('.');
        Dep.target = this;
        arrs.forEach(key => {
            val = val[key];
        })
        Dep.target = null;
    }
    Watcher.prototype.update = function() {
        let val = this.vm.data;
        let arrs = this.exp.split('.');
        arrs.forEach(key => {
            val = val[key];
        })
        this.fn(val);
    }

    compile(this.$el,this);
    function compile(el,vm) {
        let fragment = document.createDocumentFragment();
        while(firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        function replace(fragment) {
            [...fragment.childNodes].forEach((node) => {
                let reg = /\{\{(.*)\}\}/;                    
                let content = node.textContent;
                if (node.nodeType === 1 && reg.test(content) ){
                    let arrs = RegExp.$1.split('.');
                    let val = vm.data;
                    arrs.forEach((item) => {
                        val = val[item];
                    })
                    new Watcher(vm,RegExp.$1,function(newVal){
                        node.textContent = content.replace(/\{\{(.*)\}\}/,newVal);
                    });
                    node.textContent = content.replace(/\{\{(.*)\}\}/,val);
                }
                if (node.childNodes) {
                    replace(node);
                }
            })
        }
        replace(fragment);
        el.appendChild(fragment);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章