1. 程式人生 > 其它 >Vue基本原理 (三)

Vue基本原理 (三)

3、響應式原理
核心思想:Object.defineProperty(obj, key, {set, get})

    function defineReact(obj, key, value){
        Object.defineProperty(obj, key, {
            set: function(newValue){
                console.log(`觸發setter`);
                value = newValue;
                console.log(value);
            },
            get: function(){
                console.log(`觸發getter`);
                return value;
            }
        });
    }

  這裡是針對data資料的屬性的響應式定義,但是如何去實現vue例項vm繫結data每個屬性,通過以下方法:

   function observe(obj, vm){
        Object.keys(obj).forEach((key) => {
            defineReact(vm, key, obj[key]);
        })
    }

  vue的建構函式:

    function Vue(options){
        this.data = options.data;
        let id = options.el;
 
        observe(this.data, this); // 將每個data屬相繫結到Vue的例項上this
    }

  

通過以上我們可以實現Vue例項繫結data屬性。

如何去實現Vue,通常我們例項化Vue是這樣的:

var vm = new Vue({
        el: 'container',
        data: {
            msg: 'Hello world!',
            inpText: 'Input text'
        }
    });
    
    console.log(vm.msg); // Hello world!
    console.log(vm.inpText); // Input text

 實現以上效果,我們必須在vue內部初始化虛擬Dom

function Vue(options){
        this.data = options.data;
        let id = options.el;
 
        observe(this.data, this); // 將每個data屬相繫結到Vue的例項上this
        
        //------------------------新增以下程式碼
        let container = document.getElementById(id);
        let fragment = virtualDom(container, this); // 這裡通過vm物件初始化
        container.appendChild(fragment);
        
    }

  這是我們再對Vue進行例項化,則可以看到以下頁面:

 至此我們實現了dom的初始化,下一步我們在v-model元素新增監聽事件,這樣就可以通過view層的操作來修改vm對應的屬性值。在compile編譯的時候,可以準確的找到v-model屬相元素,因此我們把監聽事件新增到compile內部。

function compile(node, data){
        let reg = /\{\{(.*)\}\}/g;
        if(node.nodeType === 1){ // 標籤
            let attr = node.attributes;
            for(let i = 0, len = attr.length; i < len; i++){
                // console.log(attr[i].nodeName, attr[i].nodeValue);
                if(attr[i].nodeName === 'v-model'){
                    let name = attr[i].nodeValue;
                    node.value = data[name];
 
                    // ------------------------新增監聽事件
                    node.addEventListener('keyup', function(e){
                        data[name] = e.target.value;
                    }, false);
                    // -----------------------------------
                }
            }
            if(node.hasChildNodes()){
                node.childNodes.forEach((item) => {
                    compile(item, data);
                });
            }
        }
        if(node.nodeType === 3){ // 文字節點
            if(reg.test(node.nodeValue)){
                let name = RegExp.$1;
                name = name.trim();
                node.nodeValue = data[name];
            }
        }
    }

  這一步我們操作頁面輸入框,可以看到以下效果,證明監聽事件新增有效。

var subscribe_1 = {
        update: function(){
            console.log('This is subscribe_1');
        }
    };
    var subscribe_2 = {
        update: function(){
            console.log('This is subscribe_2');
        }
    };
    var subscribe_3 = {
        update: function(){
            console.log('This is subscribe_3');
        }
    };

  

 到這裡我們已經實現了MVVM的,即Model -> vm -> View || View -> vm -> Model 中間橋樑就是vm例項物件。

4、觀察者模式原理

觀察者模式也稱為釋出者-訂閱者模式,這樣說應該會更容易理解,更加形象。
訂閱者:

var subscribe_1 = {
        update: function(){
            console.log('This is subscribe_1');
        }
    };
    var subscribe_2 = {
        update: function(){
            console.log('This is subscribe_2');
        }
    };
    var subscribe_3 = {
        update: function(){
            console.log('This is subscribe_3');
        }
    };

  三個訂閱者都有update方法。

釋出者:

function Publisher(){
        this.subs = [subscribe_1, subscribe_2, subscribe_3]; // 新增訂閱者
    }
    Publisher.prototype = {
        constructor: Publisher,
        notify: function(){
            this.subs.forEach(function(sub){
                sub.update();
            })
        }
    };

  釋出者通過notify方法對訂閱者廣播,訂閱者通過update來接受資訊。
例項化publisher:

 var publisher = new Publisher();
    publisher.notify();

 

  

 這裡我們可以做一箇中間件來處理髮布者-訂閱者模式:

 var publisher = new Publisher();
    var middleware = {
        publish: function(){
            publisher.notify();
        }
    };
    middleware.publish();

  

5、觀察者模式嵌入
到這一步,我們已經實現了:
1、修改v-model屬性元素 -> 觸發修改vm的屬性值 -> 觸發set
2、釋出者新增訂閱 -> notify分發訂閱 -> 訂閱者update資料 
接下來我們要實現:更新檢視,同時把訂閱——釋出者模式嵌入。

釋出者:

function Publisher(){
        this.subs = []; // 訂閱者容器
    }
    Publisher.prototype = {
        constructor: Publisher,
        add: function(sub){
            this.subs.push(sub); // 新增訂閱者
        },
        notify: function(){
            this.subs.forEach(function(sub){
                sub.update(); // 釋出訂閱
            });
        }
    };

  訂閱者:
考慮到要把訂閱者繫結data的每個屬性,來觀察屬性的變化,引數:name引數可以有compile中獲取的name傳參。由於傳入的node節點型別分為兩種,我們可以分為兩訂閱者來處理,同時也可以對node節點型別進行判斷,通過switch分別處理。

function Subscriber(node, vm, name){
        this.node = node;
        this.vm = vm;
        this.name = name;
    }
    Subscriber.prototype = {
        constructor: Subscriber,
        update: function(){
            let vm = this.vm;
            let node = this.node;
            let name = this.name;
            switch(this.node.nodeType){
                case 1:
                    node.value = vm[name];
                    break;
                case 3:
                    node.nodeValue = vm[name];
                    break;
                default:
                    break;
            }
        }
    };

  我們要把訂閱者新增到compile進行虛擬dom的初始化,替換掉原來的賦值:

 function compile(node, data){
        let reg = /\{\{(.*)\}\}/g;
        if(node.nodeType === 1){ // 標籤
            let attr = node.attributes;
            for(let i = 0, len = attr.length; i < len; i++){
                // console.log(attr[i].nodeName, attr[i].nodeValue);
                if(attr[i].nodeName === 'v-model'){
                    let name = attr[i].nodeValue;
                    // --------------------這裡被替換掉
                    // node.value = data[name]; 
                    new Subscriber(node, data, name);
 
                    // ------------------------新增監聽事件
                    node.addEventListener('keyup', function(e){
                        data[name] = e.target.value;
                    }, false);
                }
            }
            if(node.hasChildNodes()){
                node.childNodes.forEach((item) => {
                    compile(item, data);
                });
            }
        }
        if(node.nodeType === 3){ // 文字節點
            if(reg.test(node.nodeValue)){
                let name = RegExp.$1;
                name = name.trim();
                // ---------------------這裡被替換掉
                // node.nodeValue = data[name];
                new Subscriber(node, data, name);
            }
        }
    }

  既然是對虛擬dom編譯初始化,Subscriber要初始化,即Subscriber.update,因此要對Subscriber作進一步的處理:

function Subscriber(node, vm, name){
        this.node = node;
        this.vm = vm;
        this.name = name;
        
        this.update();
    }
    Subscriber.prototype = {
        constructor: Subscriber,
        update: function(){
            let vm = this.vm;
            let node = this.node;
            let name = this.name;
            switch(this.node.nodeType){
                case 1:
                    node.value = vm[name];
                    break;
                case 3:
                    node.nodeValue = vm[name];
                    break;
                default:
                    break;
            }
        }
    };

  釋出者新增到defineReact,來觀察資料的變化:

function defineReact(data, key, value){
        let publisher = new Publisher();
        Object.defineProperty(data, key, {
            set: function(newValue){
                console.log(`觸發setter`);
                value = newValue;
                console.log(value);
                publisher.notify(); // 釋出訂閱
            },
            get: function(){
                console.log(`觸發getter`);
                if(Publisher.global){ //這裡為什麼來新增判斷條件,主要是讓publisher.add只執行一次,初始化虛擬dom編譯的時候來執行
                    publisher.add(Publisher.global); // 新增訂閱者
                }
                return value;
            }
        });
    }

  

這一步將訂閱者新增到釋出者容器內,對訂閱者改造:

function Subscriber(node, vm, name){
        Publisher.global = this;
        this.node = node;
        this.vm = vm;
        this.name = name;
        
        this.update();
        Publisher.global = null;
    }
    Subscriber.prototype = {
        constructor: Subscriber,
        update: function(){
            let vm = this.vm;
            let node = this.node;
            let name = this.name;
            switch(this.node.nodeType){
                case 1:
                    node.value = vm[name];
                    break;
                case 3:
                    node.nodeValue = vm[name];
                    break;
                default:
                    break;
            }
        }
    };

                                         點選這裡=》             效果圖