從最簡單的資料劫持瞭解vue雙向繫結原理

CodeLST發表於2018-06-19

當我學習vue時,帶給我最大的感覺是雙向資料繫結太方便了,不用操作DOM,檢視會根據資料的改變而改變。所以我感覺學習vue的同學有必要了解一下它的實現原理

1.vue雙向繫結原理

var obj  = {};
Object.defineProperty(obj, 'name', {
        get: function() {
            console.log('我被獲取了')
            return val;
        },
        set: function (newVal) {
            console.log('我被設定了')
        }
})
obj.name = 'fei';//在給obj設定name屬性的時候,觸發了set這個方法
var val = obj.name;//在得到obj的name屬性,會觸發get方法複製程式碼

已經瞭解到vue是通過資料劫持的方式來做資料繫結的,其中最核心的方法便是通過Object.defineProperty()來實現對屬性的劫持,那麼在設定或者獲取的時候我們就可以在get或者set方法裡假如其他的觸發函式,達到監聽資料變動的目的,無疑這個方法是本文中最重要、最基礎的內容之一。

2.實現最簡單的雙向繫結

我們知道通過Object.defineProperty()可以實現資料劫持,是的屬性在賦值的時候觸發set方法。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="demo"></div>
    <input type="text" id="inp">
    <script>
        var obj  = {};
        var demo = document.querySelector('#demo')
        var inp = document.querySelector('#inp')
        Object.defineProperty(obj, 'name', {
            get: function() {
                return val;
            },
            set: function (newVal) {//當該屬性被賦值的時候觸發
                inp.value = newVal;
                demo.innerHTML = newVal;
            }
        })
        inp.addEventListener('input', function(e) {
            // 給obj的name屬性賦值,進而觸發該屬性的set方法
            obj.name = e.target.value;
        });
        obj.name = 'fei';//在給obj設定name屬性的時候,觸發了set這個方法
    </script>
</body>
</html>複製程式碼

這只是最簡單的實現程式碼,當然其中會有很多問題存在。

3.看看vue如何實現的

從最簡單的資料劫持瞭解vue雙向繫結原理

3.1 observer用來實現對每個vue中的data中定義的屬性迴圈用Object.defineProperty()實現資料劫持,以便利用其中的setter和getter,然後通知訂閱者,訂閱者會觸發它的update方法,對檢視進行更新。

3.2 我們介紹為什麼要訂閱者,在vue中v-model,v-name,{{}}等都可以對資料進行顯示,也就是說假如一個屬性都通過這三個指令了,那麼每當這個屬性改變的時候,相應的這個三個指令的html檢視也必須改變,於是vue中就是每當有這樣的可能用到雙向繫結的指令,就在一個Dep中增加一個訂閱者,其訂閱者只是更新自己的指令對應的資料,也就是v-model='name'和{{name}}有兩個對應的訂閱者,各自管理自己的地方。每當屬性的set方法觸發,就迴圈更新Dep中的訂閱者。

4.vue的程式碼實現

4.1 observer實現,主要是給每個vue的屬性用Object.defineProperty(),程式碼如下:

function defineReactive (obj, key, val) {
    var dep = new Dep();
        Object.defineProperty(obj, key, {
             get: function() {
                    //新增訂閱者watcher到主題物件Dep
                    if(Dep.target) {
                        // JS的瀏覽器單執行緒特性,保證這個全域性變數在同一時間內,只會有同一個監聽器使用
                        dep.addSub(Dep.target);
                    }
                    return val;
             },
             set: function (newVal) {
                    if(newVal === val) return;
                    val = newVal;
                    console.log(val);
                    // 作為釋出者發出通知
                    dep.notify();//通知後dep會迴圈呼叫各自的update方法更新檢視
             }
       })
}
        function observe(obj, vm) {
            Object.keys(obj).forEach(function(key) {
                defineReactive(vm, key, obj[key]);
            })
        }複製程式碼

4.2實現compile:

compile的目的就是解析各種指令稱真正的html。

function Compile(node, vm) {
    if(node) {
        this.$frag = this.nodeToFragment(node, vm);
        return this.$frag;
    }
}
Compile.prototype = {
    nodeToFragment: function(node, vm) {
        var self = this;
        var frag = document.createDocumentFragment();
        var child;
        while(child = node.firstChild) {
            console.log([child])
            self.compileElement(child, vm);
            frag.append(child); // 將所有子節點新增到fragment中
        }
        return frag;
    },
    compileElement: function(node, vm) {
        var reg = /\{\{(.*)\}\}/;
        //節點型別為元素(input元素這裡)
        if(node.nodeType === 1) {
            var attr = node.attributes;
            // 解析屬性
            for(var i = 0; i < attr.length; i++ ) {
                if(attr[i].nodeName == 'v-model') {//遍歷屬性節點找到v-model的屬性
                    var name = attr[i].nodeValue; // 獲取v-model繫結的屬性名
                    node.addEventListener('input', function(e) {
                        // 給相應的data屬性賦值,進而觸發該屬性的set方法
                        vm[name]= e.target.value;
                    });
                    new Watcher(vm, node, name, 'value');//建立新的watcher,會觸發函式向對應屬性的dep陣列中新增訂閱者,
                }
            };
        }
        //節點型別為text
        if(node.nodeType === 3) {
            if(reg.test(node.nodeValue)) {
                var name = RegExp.$1; // 獲取匹配到的字串
                name = name.trim();
                new Watcher(vm, node, name, 'nodeValue');
            }
        }
    }
}複製程式碼

4.3 watcher實現

function Watcher(vm, node, name, type) {
    Dep.target = this;
    this.name = name;
    this.node = node;
    this.vm = vm;
    this.type = type;
    this.update();
    Dep.target = null;
}

Watcher.prototype = {
    update: function() {
        this.get();
        this.node[this.type] = this.value; // 訂閱者執行相應操作
    },
    // 獲取data的屬性值
    get: function() {
        console.log(1)
        this.value = this.vm[this.name]; //觸發相應屬性的get
    }
}複製程式碼

4.4 實現Dep來為每個屬性新增訂閱者

function Dep() {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
        sub.update();
        })
    }
}複製程式碼

這樣一來整個資料的雙向繫結就完成了。

5.梳理

首先我們為每個vue屬性用Object.defineProperty()實現資料劫持,為每個屬性分配一個訂閱者集合的管理陣列dep;然後在編譯的時候在該屬性的陣列dep中新增訂閱者,v-model會新增一個訂閱者,{{}}也會,v-bind也會,只要用到該屬性的指令理論上都會,接著為input會新增監聽事件,修改值就會為該屬性賦值,觸發該屬性的set方法,在set方法內通知訂閱者陣列dep,訂閱者陣列迴圈呼叫各訂閱者的update方法更新檢視。


相關文章