vue雙向綁定原理


vue雙向綁定原理

  • 原理主要通過數據劫持和發布訂閱模式實現的
  • 通過Object.defineProperty()來劫持各個屬性的settergetter,監聽數據的變化
  • 在數據變動時發布消息給訂閱者(watcher),訂閱者觸發響應的回調(update)更新視圖。

一、什么是數據劫持

  • 訪問或者修改對象的某個屬性時,都會觸發相對應的函數,在這個函數里進行額外的操作或者修改返回結果。
  • 在觸發函數的時候,在函數中所做的操作,就是劫持操作。

Object.defineProperty

語法:

  • Object.defineProperty(obj,prop,descriptor)

參數:

  • obj:目標對象

  • prop:需要定義的屬性或方法的名稱

  • descriptor:目標屬性所擁有的特性

    • value:屬性的值
    • writable:如果為false,屬性的值就不能被重寫。
    • get:一旦目標屬性被訪問就會調回此方法,並將此方法的運算結果返回用戶。
    • set:一旦目標屬性被賦值,就會調回此方法。
    • configurable:如果為false,則任何嘗試刪除目標屬性或修改屬性性以下特性(writable, configurable, enumerable)的行為將被無效化。
    • enumerable:是否能在for...in循環中遍歷出來或在Object.keys中列舉出來。

使用:

var info = {
    name:'hhh'
}
Object.keys(info).forEach(function(key){
    Object.defineProperty(info,key,{
        enumerable:true, // 是否能在for...in循環中遍歷出來或在Object.keys中列舉出來。
        configurable:true, // false,不可修改、刪除目標屬性或修改屬性性以下特性
        get:function(){
            console.log('被訪問了調用get');
        },
        set:function(){
            console.log('被設置了調用set');
        }
    })
})
/*
    控制台:
    輸入:info.name
    打印:被訪問了調用get

    輸入:info.name = 'hjj'
    打印:被設置了調用set
*/

二、實現最簡單的雙向綁定

<div id="demo"></div>
<input type="text" id="inp">
<script>
    var info  = {};
    var demo = document.querySelector('#demo')
    var inp = document.querySelector('#inp')
    Object.defineProperty(info, 'name', {
        get: function() {
            return val;
        },
        set: function (newVal) {//當該屬性被賦值的時候觸發
            inp.value = newVal;
            demo.innerHTML = newVal;
        }
    })
    inp.addEventListener('input', function(e) {
        // 給obj的name屬性賦值,進而觸發該屬性的set方法
        info.name = e.target.value;
    });
    info.name = 'hhhhh';//在給obj設置name屬性的時候,觸發了set這個方法
</script>

二、vue如何實現

2.1.原理圖

2.2.observer

  • observer用來實現對每個vue中的data中定義的屬性循環用Object.defineProperty()實現數據劫持,以便利用其中的setter和getter

  • 為每個屬性都分配一個訂閱者集合的管理者—dep,負責記錄和通知訂閱者

  • 當數據發生變化時發出一個notice(預告),通知訂閱者,訂閱者會觸發它的update方法,對視圖進行更新。

2.3.compile

  • 在vue中v-model,v-bind,{{}}等都可以對數據進行顯示,假如一個屬性都通過這三個指令了,那么每當這個屬性改變的時候,相應的這個三個指令的html視圖也必須改變,

  • 於是vue中就是每當有這樣的可能用到雙向綁定的指令,就在一個Dep中增加一個訂閱者,其訂閱者只是更新自己的指令對應的數據,也就是v-model='name'和{{name}}有兩個對應的訂閱者,各自管理自己的地方。

  • 每當屬性的set方法觸發,就循環更新Dep中的訂閱者。

三、vue代碼實現

3.1.observer實現

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;
           // 作為發布者發出通知,通知后dep會循環調用各自的update方法更新視圖
            dep.notify();
        }
    })
}
//遍歷,對每個屬性進行Object.defineProperty(),並添加至dep中,每個屬性都new了一個Dep(訂閱者集合的管理數組)
function observe(obj, vm) {
    Object.keys(obj).forEach(function(key) {
        defineReactive(vm, key, obj[key]);
    })
}

3.2.實現compile

compile指令解析器,對每個元素節點的指令進行掃描和解析,目的就是解析各種模板指令替換成對數據。

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');
            }
        }
    }
}

3.3.watcher實現

作為連接Observer和Compile的一個中介點,在接收數據變更的同時,讓Dep添加當前Watcher,並及時通知視圖進行update。

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
    }
}

3.4.實現Dep來為每個屬性添加訂閱者

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

四、梳理

  • 首為每個vue屬性用Object.defineProperty()實現數據劫持
  • 為每個屬性分配一個訂閱者集合的管理數組dep
  • 然后在編譯的時候在該屬性的數組dep中添加訂閱者,v-model會添加一個訂閱者,{{}}也會,v-bind也會,只要用到該屬性的指令理論上都會
  • 接着為input會添加監聽事件,修改值就會為該屬性賦值,觸發該屬性的set方法
  • 在set方法內通知訂閱者數組dep,訂閱者數組循環調用各訂閱者的update方法更新視圖。

轉自:
vue雙向綁定原理分析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM