vue原理簡介

寫vue也有一段時間了,對vue的底層原理雖然有一些瞭解,這裏總結一下。html

vue.js中有兩個核心功能:響應式數據綁定,組件系統。主流的mvc框架都實現了單向數據綁定,而雙向綁定無非是在單向綁定基礎上給可輸入元素添加了change事件,從而動態地修改model和view。vue

1. MVC,MVP,MVVMnode

1.1 MVCweb

MVC模式將軟件分爲下面三個部分正則表達式

1.視圖(View):用戶界面
2.控制器(Controller):業務邏輯
3.模型(Model):數據保存windows

MVC各個部分之間通訊的方式以下:數組

1.視圖傳送指令到控制器
2.控制器完成業務邏輯後要求模型改變狀態
3.模型將新的數據發送到視圖,用戶獲得反饋架構

示意圖以下:mvc

以上全部通訊都是單向的。接受用戶指令的時候,MVC有兩種方式,一種是經過視圖接受指令,而後傳遞給控制器。另外一種是用戶直接給控制器發送指令。app

實際使用中可能更加靈活,下面是以Backbone.js爲例說明。

1.用戶能夠向視圖(View)發送指令(DOM事件),再由View直接要求Model改變狀態。
2.用戶也能夠向Controller發送指令(改變URL觸發hashChange事件),再由Controller發送給View。
3.Controller很薄只起到路由做用而View很是厚業務邏輯都放在View。因此Backbone索性取消了Controller,只保留了Router(路由器)

MVC模式體現了「關注點分離」這一設計原則,將一個人機交互應用涉及到的功能分爲三部分,Model對應應用狀態和業務功能的封裝,能夠將它理解爲同時包含數據和行爲的領域模型,Model接受Controller的請求並完成相應的業務處理,在應用狀態改變的時候能夠向View發出通知。View實現可視化界面的呈現和用戶的交互操做,VIew層能夠直接調用Model查詢狀態,Model也能夠在本身狀態發生變化的時候主動通知VIew。Controller是Model和View之間的鏈接器,用於控制應用程序的流程。View捕獲用戶交互操做後直接發送給Controller,完成相應的UI邏輯,若是涉及業務功能調用Controller會調用Model,修改Model狀態。Controller也能夠主動控制原View或者建立新的View對用戶交互予以響應。

1.2 MVP

MVP模式將Controller更名爲Presenter,同時改變了通訊方向,以下圖:

1.各部分之間的通訊都是雙向的。
2.視圖(View)和模型(Model)不發生聯繫,都是經過表現(Presenter)傳遞
3.View很是薄,不部署任何業務邏輯,稱爲被動視圖(Passive View),即沒有任何主動性,而Presenter很是厚,全部邏輯都這裏

MVP適用於事件驅動的應用架構中,如asp.net web form,windows forms應用。

1.3 MVVM
MVVM模式將Presenter層替換爲ViewModel,其餘與MVP模式基本一致,示意圖以下:

它和MVP的區別是,採用雙向綁定視圖層(View)的變更,自動反映在ViewModel,反之亦然。Angular和Vue,React採用這種方式。

MVVM的提出源於WPF,主要是用於分離應用界面層和業務邏輯層,WPF,Siverlight都基於數據驅動開發。

MVVM模式中,一個ViewModel和一個View匹配,徹底和View綁定,全部View中的修改變化,都會更新到ViewModel中,同時VewModel的任何變化都會同步到View上顯示。之因此自動同步是ViewModel中的屬性都實現了observable這樣的接口,也就是說當使用屬性的set方法,會同時觸發屬性修改的事件,使綁定的UI自動刷新。

2. 訪問器屬性

訪問器屬性是一種特殊的屬性,不能再對象中直接定義訪問器屬性,必須經過defineProperty()方法定義訪問器屬

Object.defineProperty()方法直接在對象上定義一個新屬性,或修改一個對象現有的屬性,並返回這個對象。該方法容許精確添加或者修改對象的屬性。經過賦值操做(例如object.name = xxx)添加的普通屬性是可枚舉的,可枚舉(for ... in或Object.keys方法),這些屬性的值能夠被修改或刪除。默認狀況下,使用Object.defineProperty()添加的屬性值是不可修改的。 方法的原型以下:

Object.defineProperty(obj, prop, descriptor)
obj:要在其上定義屬性的對象
prop: 要定義或者修改的屬性名字
descriptor: 將被定義或修改的屬性描述符

對象裏目前存在的屬性描述符能夠概括爲兩類:數據描述符存取描述符。數據描述符是一個具備值的屬性,configurable爲true時,這個值但是可寫的,不然不可寫。存取描述符是由getter,setter函數描述的屬性。描述符必須是這兩種類型(數據描述符讀取描述符)之一,不可能同時是這二者。

數據描述符和存取描述符必須有下面可選鍵值:

1. configurable:當且僅當改屬性的configurable爲true的時候,該屬性描述符才能被修改,同時該屬性也能從對應的對象上被刪除。默認爲false。
2. enumerable:當且僅當改屬性的enmerable爲true的時候,改屬性才能出如今對象的枚舉屬性中,默認爲false。

數據描述符同時具備如下可選鍵值:

1. value:該屬性對應的值。能夠是任何JavaScript有效值,數值,對象,函數等,默認爲undefined。
2. writable:當且僅當改屬性的writable爲true時,value才能被賦值運算符改變,就是用「=」賦值。默認爲false。

存取描述符同時具備如下可選鍵值:

1. get:一個給屬性提供getter的方法,若是沒有getter則爲undefined。訪問這個屬性的時候,該方法會被執行,沒有參數,可是會傳入this對象(因爲繼承關係,這裏的this並不必定是定義該屬性的對象)。默認爲undefined。
2. set:一個給屬性提供setter的方法,若是沒有setter則爲undefined。當屬性值被修改的時候,觸發這個setter方法。這個方法接受惟一參數,即改屬性新的參數值。默認爲undefined。

若是一個描述符沒有value,writable,get,set任意一個關鍵字,那麼它將被認爲是一個數據描述符。若是一個描述符同時有(value或writable)和(get或set)關鍵字,會產生一個異常。

這些選項不必定是自身屬性,若是是繼承來的也要考慮。爲了確認保留這些默認值是本身定義的,可能要在這以前凍結Object.property,明確指定全部的選項,或者經過Object.create(null)將__proto__屬性指向null,要否則使用起來就有些混亂。下面使用Object.create(null)方法來給對象obj定義一個「乾淨」的屬性。

    // 使用__prop__定義
   var obj = {}
   var descriptor = Object.create(null);
   // 默認沒有enumberable,configurable,writable
   descriptor.value = 'static';
   Object.defineProperty(obj, 'key', descriptor);
   console.log(obj); 

代碼輸出結果以下:

1. 定義一個空對象obj
2. 定義一個屬性描述符descriptor,使用Object.Create方法繼承null對象,這樣沒有繼承屬性
3. 設置數據描述符value,值爲‘static’
4. 使用Object.defineProperty方法給obj對象定義key屬性,使用descriptor描述符,描述符中只有一個數據描述符value,其餘的都是默認值

上面的語句和下面的效果是同樣的,就是使用Object.defineProperty方法給obj對象設置一個key屬性,屬性的屬性描述符都是默認值:

    // 顯示定義
    var obj = {}
    Object.defineProperty(obj, 'key', {
        enumerable: false,
        configurable: false,
        writable: false,
        value: "statics"
    });
    console.log(obj); 

輸出以下:

 

還能夠循環使用同一對象最爲對象描述符使用,代碼以下:

    // 循環使用統一對象
    function withValue (value) {
        var d = withValue.d || (
            withValue.d = {
                enumerable: false,
                writable: false,
                configurable: false,
                value: null
            }
        );
        d.value = value;
        return d;
    }

    var obj = {}
    Object.defineProperty(obj, 'key', withValue('static'));
    console.log(obj); 

輸出結果以下:

 

若是對象中不存在指定的屬性,Object.defineProperty()就建立這個屬性。當描述符中省略某些字段時,這些字段將使用它們的默認值。擁有布爾值的字段的默認值都是false,value,get,set字段默認值是undefined。一個沒有get,set,value,writable定義的屬性被稱爲「通用的」,並被鍵入爲一個數據描述符。

    // 在對象中添加一個屬性與數據描述符的實例, 對象o擁有了屬性a,值爲37
    var obj = {};
    Object.defineProperty(obj, "a", {
        value: 37,
        writable: false,
        enumerable: false,
        configurable: true
    });
    console.log(obj); 

輸出結果以下:

 

    // 在對象中添加一個屬性與數據描述符的實例, 對象o擁有了屬性a,值爲37
    var obj = {};
    Object.defineProperty(obj, "a", {
        value: 37,
        writable: false,
        enumerable: false,
        configurable: true
    });
    console.log(obj);

 輸出結果以下:

    // 在對象中添加一個屬性與存取描述符,對象o擁有了屬性b,值爲38
    var bValue;
    Object.defineProperty(obj, 'b', {
    get: function () {
       return bValue;
    },
    set: function (newValue) {
       bValue = newValue;
    },
    enumerable: true,
    configurable: true
    });
    // o.b的值如今老是與bValue相同,除非從新定義o.b
    bValue = 200;
    console.log(obj.b); 

 輸出結果以下: 

    // 數據描述符和存取描述符不能混合使用,不然會報錯
    var obj = {};
    Object.defineProperty(obj, 'confict', {
        value: '0x9f91102',
        get: function () {
            return 0xdeadbeef;
        }
    });
    // 報錯:Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object> 

修改屬性

若是屬性已經存在,Object.defineProperty()方法將嘗試根據描述符中的值以及對象當前的配置來修改這個屬性。若是舊對象描述符configurable爲false,則屬性被認爲是「不可配置的」,而且沒有屬性能夠改變(除了單向改變writable爲false)。當屬性不可配置時,不能再數據和訪問器屬性類型之間切換。當時圖改變不可配置屬性(除了writable屬性以外)的值時會拋出TypeError,除非當前值和心智相同。

Writable屬性

當writable屬性設置爲false時,改屬性稱爲「不可寫」。它不能被從新分配。

    // 建立一個新對象
    var o = {};
    Object.defineProperty(o, 'a', {
        value: 37,
        configurable: false,
        writable: false
    });
    console.log(o.a); // 輸出37
    o.a = 38;
    console.log(o.a); // writable屬性爲false,o.a的值仍然是37,若是是嚴格模式,這裏會拋錯:"a" is read-only

 Enumerable特性

enumerable定義了對象的屬性是否能夠在for...in循環和Object.keys()中枚舉。

    var o = {};
    Object.defineProperty(o, 'a', {value: 1, enumerable: true});
    Object.defineProperty(o, 'b', {value: 2, enumerable: false});
    // 沒有設置 enumberable屬性默認值是false
    Object.defineProperty(o, 'c', {value: 3});
    // 若是使用直接賦值的方式建立對象屬性,則這個屬性的enumerable爲true
    o.d = 4;
    console.log(Object.keys(o)); // 輸出["a", "d"]
    console.log(o.propertyIsEnumerable('a')); // 輸出true
    console.log(o.propertyIsEnumerable('b')); // 輸出false
    console.log(o.propertyIsEnumerable('c')); // 輸出false
    console.log(o.propertyIsEnumerable('d')); // 輸出true 

Configurable特性

configurable特性表示對象的屬性是否能夠被刪除,以及除writable特性外的其餘特性是否能夠被修改

    var o = {};
    Object.defineProperty(o, 'a', {
        get: function () {
            return 1;
        },
        configurable: false
    });
    console.log(o.a); // 輸出1

    Object.defineProperty(o, 'a', {configurable: true}); // Uncaught TypeError: Cannot redefine property: a
    Object.defineProperty(o, "a", {enumerable: true}); // Uncaught TypeError: Cannot redefine property: a
    Object.defineProperty(o, "a", {
        set: function () {
        }
    }); // Uncaught TypeError: Cannot redefine property: a
    Object.defineProperty(o, "a", {
        get: function () {
        }
    }); // Uncaught TypeError: Cannot redefine property: a
    delete o.a;
    console.log(o.a); // 對染delete語句沒有報錯,可是沒有真正刪除a屬性,輸出1

 添加多個屬性和默認值

使用點運算符和Object.defineProperty()爲對象的屬性賦值,數據描述符的屬性默認值是不一樣的。

    var o = {};
    o.a = 1;
    // 上面使用點語法定義屬性,等同於下面代碼,注意writable,configurable,enumerable的默認屬性爲false,可是這裏使用點語法是true
    Object.defineProperty(o, "a", {
        value: 1,
        writable: true,
        configurable: true,
        enumerable: true
    });

    Object.defineProperty(o, "a", {value: 1});
    // 上面使用Object.defineProperty()定義屬性,等同於下面代碼
    Object.defineProperty(o, "a", {
        value: 1,
        writable: false,
        configurable: false,
        enumerable: false
    });

 通常的getter和setter

下面的例子展現如何實現一個自存檔對象,當設置temperture屬性時,archive數組就會獲取日誌

   function Archiver () {
       var temperature = null;
       var archiver = [];
       Object.defineProperty(this, 'temperature', {
           get: function () {
               console.log('get!');
               return temperature;
           },
           set: function (value) {
               temperature = value;
               archiver.push({val: temperature});
           }
       });
       this.getArchive = function () {
           return archiver;
       }
   }

   var arc = new Archiver();
   console.log(arc.temperature); // 輸出get,可是arc.temperature是null
   arc.temperature = 11; // 觸發archiver.push({val: temperature})
   arc.temperature = 13; // 觸發archiver.push({val: temperature})
   console.log(arc.getArchive()); // 輸出[{val: 11}, {val: 13}]

1. 定義一個方法類Archive,在內部有私有變量temperature,archiver,
2. 在方法內使用Object.defineProperty()方法定義屬性temperature,定義存取描述符get,放回私有變量temperature,定義存取描述符set,用傳遞的參數給私有變量temperature賦值
3. 定義特權方法getArchive,返回私有變量temperature
4. 使用new操做符定義類Archive實例arc
5. 輸出實例arc的temperature屬性,調用get方法,返回私有變量temperature的值null
6. 給實例arc的temperatur屬性賦值,調用set方法,傳遞參數11,觸發archiver.push({val: 11})
7. 給實例arc的temperatur屬性賦值,調用set方法,傳遞參數13,觸發archiver.push({val: 13})
8. 輸出實例arc的temperature屬性,調用get方法,返回私有變量temperature的值[{val: 11}, {val: 13}]

   var pattern = {
       get: function () {
           return 'I alway return this string,whatever you have assigned';
       },
       set: function () {
           console.log('給屬性myname賦值')
           this.myname = 'this is my name string';
       }
   }
   function TestDefineSetAndGet () {
       Object.defineProperty(this, 'myproperty', pattern);
   }
   var instance = new TestDefineSetAndGet();
   instance.myproperty = 'test'; // 輸出 「給屬性myname賦值」
   console.log(instance.myproperty); // 輸出 「I alway return this string,whatever you have assigned」
   console.log(instance.myname); // 輸出 「this is my name string」

1. 定義屬性描述符pattern,屬性描述符上有存取描述符get,返回字符串「I alway return this string,whatever you have assigned」,存取描述符set,先輸出「給屬性myname賦值」,給當前對象的myname屬性賦值「this is my name string」
2. 定義類方法TestDefineSetAndGet,方法內部使用Object.defineProperty()給當前對象定義一個屬性「 myproperty」,使用屬性描述符pattern
3. 使用new操做符定義類TestDefineSetAndGet的實例instance
4. 給實例的屬性myproperty賦值「test」,由於使用Object.defineProperty給對象定義屬性的時候沒有指定writable,這裏賦值無效。在get函數裏返回的是固定值。在set函數裏輸出「給屬性myname賦值」
5. 輸出實例的屬性myproperty,訪問get函數,返回「I alway return this string,whatever you have assigned」
6. 輸出實例的屬性myname,由於訪問過set函數,在setg函數中給當前對象賦過值,因此myname的值爲「this is my name string」

繼承屬性

若是訪問者的屬性是被繼承的,它的get和set方法會在子對象的屬性被訪問或者修改時調用。若是這些方法用一個變量保存,會被全部對象共享。

    function myClass () {
    }
    var value;
    Object.defineProperty(myClass.prototype, 'x', {
        get () {
            return value
        },
        set (x) {
            value = x;
        }
    });
    var a = new myClass();
    var b = new myClass();
    a.x = 1;
    console.log(a.x); // 1
    console.log(b.x); // 1 

在類myClass的原型對象上定義了x屬性,這個屬性會被類myClass的全部實例共享。經過將值保存在另外一個屬性中固定,在get,set中,this指向某個被訪問和修改屬性的對象。

代碼以下: 

        var obj = {}
        Object.defineProperty(obj, 'hello', {
            get: function () {
                console.log('get方法被調用')
            },
            set: function (v) {
                console.log("set方法被調用了,參數是" + v)
            }
        })
        obj.hello; // get方法被調用
        obj.hello = 'abc'; // set方法被調用了,參數是abc 

 能夠像普通屬性同樣讀取,設置訪問器屬性,訪問器屬性比較特殊,讀取或設置訪問器屬性的值實際上是調用內部get,set方法來操做屬性。爲屬性賦值,就是調用set方法並使用參數給屬性賦值。get,set方法內部的this指針指向obj,這意味着get和set方法能夠操做對象內部的值。另外,訪問器屬性會覆蓋同名的普通屬性,由於訪問器屬性優先訪問,同名的屬性會被忽略。 

    function myClass () {
    }
    Object.defineProperty(myClass.prototype, 'x', {
        get () {
            return this.stored_x;
        },
        set (x) {
            this.stored_x = x;
        }
    });
    var a = new myClass();
    var b = new myClass();
    a.x = 1;
    console.log(a.x); // 1
    console.log(b.x); // undefined

 不像訪問者屬性,值屬性始終在對象自身上設置,而不是一個原型。若是一個不可寫的屬性被繼承,它仍然能夠防止修改對象的屬性。

    function myClass () {
    }
    myClass.prototype.x = 1;
    Object.defineProperty(myClass.prototype, 'y', {
        writable: false,
        value: 1
    });
    var a = new myClass();
    a.x = 2;
    console.log(a.x); // 2
    console.log(myClass.prototype.x); // 1
    a.y = 2;
    console.log(a.y); // 1
    console.log(myClass.prototype.y); // 1 

1. 定義方法類myClass
2. 在方法原型對象上經過點語法定義屬性x,值爲1,它是可寫的,可配置的,可枚舉的
3.  經過Object.defineProperty()方法在方法原型上定義屬性y,它是可寫的,不可配置的,不可枚舉的
4. 定義一個myClass類的實例
5. 訪問實例的屬性x,賦值爲2,對象自己沒有這個屬性,在它原型對象上有這個屬性,這個屬性是可寫的,賦值爲2
6. 輸出實例x的屬性爲2
7. 輸出方法類myClass的原型對象上的屬性x是1
8. 訪問實例的屬性y,它是經過Object.defineProperty()方法定義的,是不可寫的,賦值爲2,它的值仍然是1
9. 出事方法類myClass的原型對象上的屬性y,它仍然是1

介紹完訪問器屬性以後咱們來看看vue是如何實現雙向綁定的。

3. vue.js雙向綁定

3.1. 極簡雙向綁定

html代碼:

    <input type="text" id="a">
    <span id="b"></span>

 JavaScript代碼:

        var obj = {};
        Object.defineProperty(obj, 'hello', {
            set: function (newVal) {
              document.getElementById('a').value = newVal;
              document.getElementById('b').innerHTML = newVal;
            }
        })
        document.addEventListener('keyup', function (e) {
          obj.hello = e.target.value;
        });

 效果:

這個效果就是在文本框中輸入的值會顯示在旁邊的<span>標籤裏。這個例子就是雙向綁定的實現,可是僅僅爲了說明原理,這個和咱們平時用的vue.js還有差距,下面是咱們常見的vue.js寫法

html代碼:

    <input type="text" v-model="text">
    {{ text }} 

JavaScript代碼:

  var vm = new Vue({
    el: 'app',
    data: {
      text: 'hello world'
    }
  }) 

爲了實現這樣的容易理解的代碼vue.js背後作了不少工做,咱們一一分解。

1. 輸入框以及文本節點與data中的數據綁定
2. 輸入框變化的時候,data中的數據同步變化。即MVVM中 view => viewmodel的變化
3. data中的數據變化時,文本節點的內容同步變化。即MVVM中viewmode => view的變化

3.2 數據初始化綁定

介紹數據初始化綁定以前先說一下DocumentFragment。DocumentFragment(文檔片斷)能夠看作是節點容器,它能夠包含多個子節點,能夠把它插入到DOM中,只有它的子節點會插入目標節點,因此能夠把它看作是一組節點容器。使用DocumentFragment處理節點速度和性能優於直接操做DOM。Vue進行編譯的時候就是將掛載目標的全部子節點劫持到DocumentFragment中,通過處理後再將DocumentFragment總體返回到掛載目標。實例代碼以下:

        var dom = nodeToFragment(document.getElementById("app"));
        console.log(dom);
        function nodeToFragment (node, vm) {
            var flag = document.createDocumentFragment();
            var child;
            while (child = node.firstChild) {
                flag.appendChild(child); // 劫持node的全部節點
            }
            return flag;
        }
        document.getElementById("app").appendChild(dom); 

有了文檔片斷以後再看看初始化綁定。

html代碼:

<div id="app">
    <input type="text" v-model="text">
    {{text}}
</div> 

JavaScript代碼:

    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 === 'v-model') {
                    var name = attr[i].nodeValue; // 獲取v-model綁定的屬性名
                    node.value = vm.data[name]; // 將data的值賦給該node
                    node.removeAttribute('v-model');
                }
            }
        }
        // 節點類型爲text
        if (node.nodeType === 3) {
            if (reg.test(node.nodeValue)) {
                var name = RegExp.$1; // 獲取匹配到的字符串
                name = name.trim()
                node.nodeValue = vm.data[name]; // 將該data的值付給該node
            }
        }
    }

    function nodeToFragment (node, vm) {
        var flag = document.createDocumentFragment();
        var child;
        while (child = node.firstChild) {
            compile(child, vm);
            // 將子節點劫持到文檔片斷中
            flag.appendChild(child);
        }
        return flag;
    }

    // 構造函數
    function Vue (options) {
        this.data = options.data;
        var id = options.el;
        var dom = nodeToFragment(document.getElementById(id), this);
        // 編譯完成後把dom返回到app中
        document.getElementById(id).appendChild(dom);
    }

    var vm = new Vue({
        el: 'app',
        data: {
            text: 'hello world'
        }
    }); 

最終效果:

咱們看到hello word已經綁定到input標籤和節點中了

先看compile方法,這個方法主要負責給node節點賦值
1. compile方法接收兩個參數,第一個是DOM節點,第二個vm是當前對象
2. 判斷dom節點類型,若是是1,表示元素(這裏判斷不太嚴謹,只是爲了說明原理),在node節點的全部屬性中查找nodeName爲「v-model」的屬性,找到屬性值,這裏是「text」。用當前對象中名字爲「text」的屬性值給節點賦值,最後刪除這個屬性,就是刪除節點的v-model屬性。
3. 判斷dom節點類型,若是是3,表示是節點內容,用正則表達式判斷是「{{text}}」這樣的字符串,用當前對象中名字爲「text」的屬性值給節點賦值,直接覆蓋掉「{{text}}」

nodeToFragment方法負責建立文檔片斷,並將compile處理過的子節點劫持到這個文檔片斷中
1. 建立一個文檔片斷
2. 循環查找傳入的node節點,調用compile方法給節點賦值
3. 將賦值後的節點劫持到文檔片斷中

Vue構造函數
1. 用傳入參數的data屬性給當前對象的data屬性賦值
2. 用傳入參數的id標記查找掛載節點,調用nodeToFragment方法獲取劫持後的文檔片斷,這個過程稱爲編譯
3. 編譯完成後,將文檔片斷插入到指定的當前節點中

實例化vue
1. 實例化一個vue對象,el屬性爲掛載節點的id,data屬性爲要綁定的屬性及屬性值

3.3 響應式數據綁定

 初始化綁定只是實現了第一步,而後咱們要實現的是在文本框中輸入內容的時候,vue實例中的屬性值也跟着變化。思路是在文本框中輸入數據的時候,觸發文本框的input事件(也能夠是keyup,change),在相應的事件處理程序中,獲取輸入內容賦值給當前vue實例vm的text屬性。這裏利用上面介紹的Object.defeinProperty()方法來給vue實例中data中的屬性從新定義爲訪問器屬性,就是在定義這個屬性的時候添加get,set這兩個存取描述符,這樣給vm.text賦值的時候就會觸發set方法。而後在set方法中更新vue實例屬性的值。看下面的html,js代碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>響應式數據綁定</title>
</head>
<body>
<div id="app">
    <input type="text" v-model="text"/>
    {{ text }}
</div>
<script>
    /**
     * 使用defineProperty將data中的text設置爲vm的訪問器屬性
     * @param obj 對象
     * @param 屬性名
     * @param 屬性值
     * */
    function defineReactive (obj, key, val) {
        Object.defineProperty(obj, key, {
            get: function () {
                return val
            },
            set: function (newVal) {
                if (newVal === val) {
                    return
                }
                val = newVal
                // 輸出日誌
                console.log(`set方法觸發屬性值變化${val}`)
            }
        })
    }
    /**
     * 給vue實例定義訪問器屬性
     * @param obj vue實例中的數據
     * @param vm vue對象
     * */
    function observe (obj, vm) {
        Object.keys(obj).forEach(function (key) {
            defineReactive(vm, key, obj[key]);
        })
    }
    /**
     * 編譯過程,給子節點初始化綁定vue實例中的屬性值
     * @param node 子節點
     * @param vm vue實例
     * */
    function compile (node, vm) {
        let reg = /\{\{(.*)\}\}/
        // 節點類型爲元素
        if (node.nodeType === 1) {
            let attr = node.attributes
            // 解析屬性
            for (let i = 0; i < attr.length; i++) {
                if (attr[i].nodeName === 'v-model') {
                    // 獲取v-model綁定的屬性名
                    let name = attr[i].nodeValue
                    // 添加監聽事件
                    node.addEventListener('input', function (e) {
                        // 給相應的data屬性賦值,進而觸發該屬性的set方法
                        vm[name] = e.target.value;
                    });
                    // 將data的值賦給該node
                    node.value = vm.data[name];
                    node.removeAttribute('v-model')
                }
            }
        }
        // 節點類型爲text
        if (node.nodeType === 3) {
            if (reg.test(node.nodeValue)) {
                // 獲取匹配到的字符串
                let name = RegExp.$1
                name = name.trim()
                // 將data的值賦給該node
                node.nodeValue = vm.data[name]
            }
        }
    }
    /**
     * DocumentFragment文檔片斷,能夠看做節點容器,它能夠包含多個子節點,當將它插入到dom中時只有子節點插入到目標節點中。
     * 使用documentfragment處理節點速度和性能要高於直接操做dom。vue編譯的時候,就是將掛載目標的全部子節點劫持到documentfragment
     * 中,通過處理後再將documentfragment總體返回到掛載目標中。
     * @param node 節點
     * @param vm vue實例
     * */
    function nodeToFragment (node, vm) {
        var flag = document.createDocumentFragment();
        var child;
        while (child = node.firstChild) {
            compile(child, vm);
            flag.appendChild(child);
        }
        return flag;
    }
    /*vue類*/
    function Vue (options) {
        this.data = options.data
        let data = this.data
        // 給vue實例的data定義訪問器屬性,覆蓋原來的同名屬性
        observe(data, this)
        let id = options.el
        let dom = nodeToFragment(document.getElementById(id), this)
        // 編譯,劫持完成後將dom返回到app中
        document.getElementById(id).appendChild(dom)
    }

    /*定義一個vue實例*/
    let vm = new Vue({
        el: 'app',
        // 這裏的data屬性不是訪問器屬性
        data: {
            text: 'hello world!'
        }
    })
</script>
</body>
</html> 

修改文本框中的內容,vue實例中的屬性值也跟着變化,以下截圖:

下面再也不逐句分析,只說重點的。

1. 在defineReactive方法中,vue實例中的data的屬性從新定義爲訪問器屬性,並在set方法中將新的值更新到這個屬性
2. 在observe方法中,遍歷vue實例中data的屬性,逐一調用defineReactive方法,把他們定義爲訪問器屬性
3. 在compile方法中,若是是input這樣的標籤,給它添加事件(也能夠是keyup,change),監聽input值變化,並給vue實例中相應的訪問器屬性賦值
4. 在Vue類方法中,調用observer方法,傳入當前實例對象和對象的data屬性,將data屬性中的子元素從新定義爲當前對象的訪問器屬性

set方法被觸發以後,vue實例的text屬性跟着變化,可是<span>的內容並無變化,下面的內容將會介紹「訂閱/發佈模式」來解決這個問題。

3.4 雙向綁定的實現

在實現雙向綁定以前要先學習一下「訂閱/發佈模式」。訂閱發佈模式(又稱爲觀察者模式)定義一種一對多的關係,讓多個觀察者同時監聽一個主題對象,主題對象狀態發生改變的時候通知全部的觀察者

發佈者發出通知 => 主題對象收到通知並推送給訂閱者 => 訂閱者執行相應的操做

看下面的代碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>訂閱/發佈模式</title>
</head>
<body>
<script>
    /**
     * 定義一個發佈者publisher
     * */
    var pub = {
        publish: function () {
            dep.notify();
        }
    }
    /**
     * 三個訂閱者
     * */
    var sub1 = {
        update: function () {
            console.log(1);
        }
    };
    var sub2 = {
        update: function () {
            console.log(2);
        }
    };
    var sub3 = {
        update: function () {
            console.log(3);
        }
    }
    /**
     * 一個主題對象
     * */
    function Dep () {
        this.subs = [sub1, sub2, sub3];
    }
    Dep.prototype.notify = function () {
        this.subs.forEach(function (sub) {
            sub.update();
        })
    }
    // 發佈者發佈消息,主題對象執行notifiy方法,觸發全部訂閱者響應,執行update
    var dep = new Dep();
    pub.publish();
</script>
</body>
</html>

 運行結果以下截圖:

1. 定義發佈者對象pub,對象中定義publish方法,方法調用主題對象實例dep的notify()方法
2. 定義三個訂閱者對象,對象中定義update方法,三個對象的update方法分別輸出1,2,3
3. 定義一個主題方法類,主題對象中定義數組屬性subs,包含三個訂閱者對象
4. 在主題方法類的原型對象上定義通知方法notify,方法中循環調用三個訂閱者對象的update()方法
5. 實例化主題方法類獲得實例dep
6. 調用發佈者對象的通知方法notifiy(),分別輸出1,2,3

每當建立一個Vue實例的時候,主要作了兩件事情,第一是監聽數據:observe(data),第二個是編譯HTML:nodeToFragment(id)。
在監聽數據過程當中,爲data的每個屬性生成主題對象dep
在編譯HTML的過程當中,爲每一個與數據綁定相關的節點生成一個訂閱者watcherwatcher會將本身添加到相應屬性的dep中
前面已經實現了:修改輸入框內容 => 在事件回調函數中修改屬性值 => 觸發屬性set方法。
接下來咱們要實現的是:發出通知dep.notify() => 觸發訂閱者的updata方法 => 更新視圖,實現這個目標的關鍵是如何將watcher添加到關聯屬性的dep中去。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>雙向綁定的實現</title>
</head>
<body>
<div id="app">
    <input type="text" v-model="text">
    {{ text }}
</div>
<script>
    /**
     * 使用defineProperty將data中的text設置爲vm的訪問器屬性
     * @param obj 對象
     * @param key 屬性名
     * @param val 屬性值
     */
    function defineReactive (obj, key, val) {
        var dep = new Dep();
        Object.defineProperty(obj, key, {
            get: function () {
                //  若是主題對象類的靜態屬性target有值, 此時Watcher方法被調用,給主題對象添加訂閱者
                if (Dep.target) dep.addSub(Dep.target);
                return val;
            },
            set: function (newVal) {
                if (newVal === val) return
                val = newVal;
                // 主題對象做爲發佈者收到通知推送給訂閱者
                dep.notify();
            }
        })
    }
    /**
     * 給vue實例定義訪問器屬性
     * @param obj vue實例中的數據
     * @param vm vue對象
     */
    function observe (obj, vm) {
        Object.keys(obj).forEach(function (key) {
            defineReactive(vm, key, obj[key])
        })
    }
    /**
     * DocumentFragment文檔片斷,能夠看做節點容器,它能夠包含多個子節點,當將它插入到dom中時只有子節點插入到目標節點中。
     * 使用documentfragment處理節點速度和性能要高於直接操做dom。vue編譯的時候,就是將掛載目標的全部子節點劫持到documentfragment
     * 中,通過處理後再將documentfragment總體返回到掛載目標中。
     * @param node 節點
     * @param vm vue實例
     * */
    function nodeToFragment (node, vm) {
        var flag = document.createDocumentFragment();
        var child;
        while (child = node.firstChild) {
            compile(child, vm);
            flag.appendChild(child);
        }
        return flag;
    }

    /**
     * 給子節點初始化綁定vue實例中的屬性值
     * @param node 子節點
     * @param vm vue實例
     */
    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 === 'v-model') {
                    // 獲取v-model綁定的屬性名
                    var name = attr[i].nodeValue;
                    node.addEventListener('input', function (e) {
                        // 給相應的data屬性賦值,觸發set方法
                        vm[name] = e.target.value
                    });
                    // 將data的值賦給該node
                    node.value = vm[name];
                    node.removeAttribute('v-model');
                }
            }
            new Watcher(vm, node, name, 'input')
        }
        if (node.nodeType === 3) {
            if (reg.test(node.nodeValue)) {
                var name = RegExp.$1; // 獲取匹配到的字符串
                name = name.trim();
                // 將data的值賦給該node
                new Watcher(vm, node, name, 'text');
            }
        }
    }

    /**
     * 編譯 HTML 過程當中,爲每一個與 data 關聯的節點生成一個 Watcher
     * @param vm
     * @param node
     * @param name
     * @param nodeType
     * @constructor
     */
    function Watcher (vm, node, name, nodeType) {
        // 將當前對象賦值給全局變量Dep.target
        Dep.target = this;
        this.name = name;
        this.node = node;
        this.vm = vm;
        this.nodeType = nodeType;
        this.update();
        Dep.target = null;
    }
    Watcher.prototype = {
        update: function () {
            this.get();
            if (this.nodeType === 'text') {
                this.node.nodeValue = this.value;
            }
            if (this.nodeType === 'input') {
                this.node.value = this.value;
            }
        },
        get: function () {
            this.value = this.vm[this.name];
        }
    }

    /**
     * 定義一個主題對象
     * @constructor
     */
    function Dep () {
        this.subs = [];
    }

    /**
     * 定義主題對象的添加方法和通知變化方法
     * @type {{addSub: Dep.addSub, notify: Dep.notify}}
     */
    Dep.prototype = {
        addSub: function (sub) {
            this.subs.push(sub);
        },
        notify: function () {
            this.subs.forEach(function (sub) {
                sub.update();
            });
        }
    };

    /**
     * 定義Vue類
     * @param options Vue參數選項
     * @constructor
     */
    function Vue (options) {
        this.data = options.data;
        var data = this.data;
        observe(data, this);
        var id = options.el;
        var dom = nodeToFragment(document.getElementById(id), this);
        // 編譯完成後,將dom返回到app中
        document.getElementById(id).appendChild(dom);
    }
    // 定義Vue實例
    var vm = new Vue({
        el: 'app',
        data: {
            text: 'hello world'
        }
    })
</script>
</body>
</html> 

最終效果以下截圖:

這裏再也不逐句分析,只把重點說明一下
1. 定義主題對象Dep,對象中有addSub和notify兩個方法,前者負責向當前對象中添加訂閱者,後者輪詢訂閱者,調用訂閱者的更新方法update()
2. 定義觀察者對象方法Watcher,在方法中先將本身賦給一個全局變量Dep.target,實際上是給主題類Dep定義了一個靜態屬性target,能夠直接使用Dep.target訪問這個靜態屬性。而後給類定義共有屬性name(vue實例中的訪問器屬性名「text」),node(html標籤,如<input>,{{text}}),vm(當前vue實例),nodeType(html標籤類型),其次執行update方法,進而執行了原型對象上的get方法,get方法中的this.vm[this.name]讀取了vm中的訪問器屬性,從而觸發了訪問器屬性的get方法,get方法中將wathcer添加到對應訪問器屬性的dep中,同時將屬性值賦給臨時變量value。再者,獲取屬性的值(保存在臨時變量value中),而後更新視圖。最後將Dep.target設爲空。由於它是全局變量,也是watcher與dep關聯的惟一橋樑,任什麼時候刻都必須保證Dep.target只有一個值。
3. 在編譯方法compile中,劫持子節點的時候,在節點上定義一個觀察者對象Watcher
4. defineReactive方法中,定義訪問器屬性的時候,在存取描述符get中,若是主題對象類的靜態屬性target有值, 此時Watcher方法被調用,給主題對象添加訂閱者。

data中的數據從新定義爲訪問器屬性,get中將當前數據對應的節點添加到主題對象中,set方法中通知數據對應的節點更新。編譯過程將data數據生成數據節點,並生成一個觀察者來觀察節點變化。

4. 總結

本文介紹了vue.js的簡單實現以及相關的知識,包含MVC,MVP,MVVM的原理,對象的訪問器屬性,html的文檔片斷(DocumentFragment),觀察者模式。vue.js的實現主要介紹數據編譯(compile),經過文檔片斷實現數據劫持掛載,經過觀察者模式(訂閱發佈模式)的實現數據雙向綁定等內容。

參考:

https://www.cnblogs.com/icebutterfly/p/7977033.htmlhttp://www.ruanyifeng.com/blog/2015/02/mvcmvp_mvvm.htmlhttps://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/definePropertyhttps://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension#!comments

相關文章
相關標籤/搜索