Vue基本原理

前言

通過幾天的研究,發現學習框架的底層技術,收穫頗豐,相比只學習框架的使用要來的合算;若是工做急需,快速上手應用,掌握如何使用短時間內更加高效;若是有較多的時間來系統學習,建議研究一下框架的等層技術、原理。javascript

Vue、React、Angular三大框架對比

一、Vuehtml

Vue是尤雨溪編寫的一個構建數據驅動的Web界面的庫,準確來講不是一個框架,它聚焦在V(view)視圖層。前端

它有如下的特性:vue

1.輕量級的框架java

2.雙向數據綁定node

3.指令angularjs

4.插件化api

優勢:瀏覽器

  1. 簡單:官方文檔很清晰,比 Angular 簡單易學。
  2. 快速:異步批處理方式更新 DOM。
  3. 組合:用解耦的、可複用的組件組合你的應用程序。
  4. 緊湊:~18kb min+gzip,且無依賴。
  5. 強大:表達式 無需聲明依賴的可推導屬性 (computed properties)。
  6. 對模塊友好:能夠經過 NPM、Bower 或 Duo 安裝,不強迫你全部的代碼都遵循 Angular 的各類規定,使用場景更加靈活。

缺點:前端框架

  1. 新生兒:Vue.js是一個新的項目,沒有angular那麼成熟。
  2. 影響度不是很大:google了一下,有關於Vue.js多樣性或者說豐富性少於其餘一些有名的庫。
  3. 不支持IE8

二、React

React 起源於 Facebook 的內部項目,用來架設 Instagram 的網站, 並於 2013年 5 月開源。React 擁有較高的性能,代碼邏輯很是簡單,愈來愈多的人已開始關注和使用它。

它有如下的特性:

1.聲明式設計:React採用聲明範式,能夠輕鬆描述應用。

2.高效:React經過對DOM的模擬,最大限度地減小與DOM的交互。

3.靈活:React能夠與已知的庫或框架很好地配合。

優勢:

  1. 速度快:在UI渲染過程當中,React經過在虛擬DOM中的微操做來實現對實際DOM的局部更新。
  2. 跨瀏覽器兼容:虛擬DOM幫助咱們解決了跨瀏覽器問題,它爲咱們提供了標準化的API,甚至在IE8中都是沒問題的。
  3. 模塊化:爲你程序編寫獨立的模塊化UI組件,這樣當某個或某些組件出現問題是,能夠方便地進行隔離。
  4. 單向數據流:Flux是一個用於在JavaScript應用中建立單向數據層的架構,它隨着React視圖庫的開發而被Facebook概念化。
  5. 同構、純粹的javascript:由於搜索引擎的爬蟲程序依賴的是服務端響應而不是JavaScript的執行,預渲染你的應用有助於搜索引擎優化。
  6. 兼容性好:好比使用RequireJS來加載和打包,而Browserify和Webpack適用於構建大型應用。它們使得那些艱難的任務再也不讓人望而生畏。

缺點:

  1. React自己只是一個V而已,並非一個完整的框架,因此若是是大型項目想要一套完整的框架的話,基本都須要加上ReactRouter和Flux才能寫大型應用。

三、Angular

Angular是一款優秀的前端JS框架,已經被用於Google的多款產品當中。

它有如下的特性:

1.良好的應用程序結構

2.雙向數據綁定

3.指令

4.HTML模板

5.可嵌入、注入和測試

優勢:

  1. 模板功能強大豐富,自帶了極其豐富的angular指令。
  2. 是一個比較完善的前端框架,包含服務,模板,數據雙向綁定,模塊化,路由,過濾器,依賴注入等全部功能;
  3. 自定義指令,自定義指令後能夠在項目中屢次使用。
  4. ng模塊化比較大膽的引入了Java的一些東西(依賴注入),可以很容易的寫出可複用的代碼,對於敏捷開發的團隊來講很是有幫助。
  5. angularjs是互聯網巨人谷歌開發,這也意味着他有一個堅實的基礎和社區支持。

缺點:

  1. angular 入門很容易 但深刻後概念不少, 學習中較難理解.
  2. 文檔例子很是少, 官方的文檔基本只寫了api, 一個例子都沒有, 不少時候具體怎麼用都是google來的, 或直接問misko,angular的做者.
  3. 對IE6/7 兼容不算特別好, 就是能夠用jQuery本身手寫代碼解決一些.
  4. 指令的應用的最佳實踐教程少, angular其實很靈活, 若是不看一些做者的使用原則,很容易寫出 四不像的代碼, 例如js中仍是像jQuery的思想有不少dom操做.
  5. DI 依賴注入 若是代碼壓縮須要顯示聲明.

經過以上相比較,您更加傾向於學習哪個呢?

正題:Vue的基本原理

Vue的原理圖

一、創建虛擬DOM Tree,經過document.createDocumentFragment(),遍歷指定根節點內部節點,根據{{ prop }}、v-model等規則進行compile;
二、經過Object.defineProperty()進行數據變化攔截;
三、截取到的數據變化,經過發佈者-訂閱者模式,觸發Watcher,從而改變虛擬DOM中的具體數據;
四、經過改變虛擬DOM元素值,從而改變最後渲染dom樹的值,完成雙向綁定

完成數據的雙向綁定在於Object.defineProperty()

Vue雙向綁定的實現

一、簡易雙綁

首先,咱們把注意力集中在這個屬性上:Object.defineProperty。

Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。
語法:Object.defineProperty(obj, prop, descriptor)

什麼叫作,定義或修改一個對象的新屬性,並返回這個對象呢?

var obj = {};
Object.defineProperty(obj,'hello',{
  get:function(){
    //咱們在這裏攔截到了數據
    console.log("get方法被調用");
  },
  set:function(newValue){
    //改變數據的值,攔截下來額
    console.log("set方法被調用");
  }
});
obj.hello//輸出爲「get方法被調用」,輸出了值。
obj.hello = 'new Hello';//輸出爲set方法被調用,修改了新值

經過以上方法能夠看出,獲取對象屬性值觸發get、設置對象屬性值觸發set,所以咱們能夠想象到數據模型對象的屬性設置和讀取能夠驅動view層的數據變化,view的數據變化傳遞給數據模型對象,在set裏面能夠作不少事情。

在這基礎上,咱們能夠作到數據的雙向綁定:

let obj = {};
    Object.defineProperty(obj, 'name', {
        set: function(newValue){
            console.log('觸發setter');
            document.querySelector('.text-box').innerHTML = newValue;
            document.querySelector('.inp-text').value = newValue;
        },
        get: function(){
            console.log('觸發getter');
        }
    });

    document.querySelector('.inp-text').addEventListener('keyup', function(e){
        obj.name = e.target.value;
    }, false);

html

<input class="inp-text" type="text">
<div class="text-box"></div>

clipboard.png

clipboard.png

以上只是vue的核心思想,經過對象底層屬性的set和get進行數據攔截,vue的虛擬dom又是怎麼實現的,且看如下分解。

二、虛擬DOM樹

建立虛擬DOM:
var frag = document.createDocumentFragment();
view層的{{msg}}和v-model的編譯規則以下:
html:

<div id="container">
    {{ msg }}<br>
    <input class="inp-text" type="text" v-model="inpText">
    <div class="text-box">
        <p class="show-text">{{ msg }}</p>
    </div>
</div>

view層作了多層嵌套,這樣測試更多出現錯誤的可能性。

var container = document.getElementById('container');
    //這裏咱們把vue實例中的data提取出來,更加直觀
    var data = {
        msg: 'Hello world!',
        inpText: 'Input text'
    };
    var fragment = virtualDom(container, data);
    container.appendChild(fragment);

    //虛擬dom建立方法
    function virtualDom(node, data){
        let frag = document.createDocumentFragment();
        let child;
        // 遍歷dom節點
        while(child = node.firstChild){
            compile(child, data);
            frag.appendChild(child);
        }
        return frag;
    }
     
    //編譯規則
    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];
                }
            }
            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];
            }
        }
    }

解釋:
一、經過virtualDom建立虛擬節點,將目標盒子內全部子節點添加到其內部,注意這裏只是子節點;
二、子節點經過compile進行編譯,a:若是節點爲元素,其nodeType = 1,b:若是節點爲文本,其nodeType = 3,具體能夠查看詳情http://www.w3school.com.cn/js...
三、若是第二步子節點仍有子節點,經過hasChildNodes()來確認,若是有遞歸調用compile方法。

clipboard.png

三、響應式原理
核心思想: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進行實例化,則能夠看到如下頁面:

clipboard.png

至此咱們實現了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];
            }
        }
    }

這一步咱們操做頁面輸入框,能夠看到如下效果,證實監聽事件添加有效。
clipboard.png

到這裏咱們已經實現了MVVM的,即Model -> vm -> View || View -> vm -> Model 中間橋樑就是vm實例對象。

四、觀察者模式原理

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

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();

clipboard.png

這裏咱們能夠作一箇中間件來處理髮布者-訂閱者模式:

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

五、觀察者模式嵌入
到這一步,咱們已經實現了:
一、修改v-model屬性元素 -> 觸發修改vm的屬性值 -> 觸發set
二、發佈者添加訂閱 -> 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;
            }
        }
    };

六、完整效果

html:

<div id="container">
    {{ msg }}<br>
    <input class="inp-text" type="text" v-model="inpText">
    <p>{{ inpText }}</p>
    <div class="text-box">
        <p class="show-text">{{ msg }}</p>
    </div>
</div>

javascript:

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

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

    function virtualDom(node, data){
        let frag = document.createDocumentFragment();
        let child;
        // 遍歷dom節點
        while(child = node.firstChild){
            compile(child, data);
            frag.appendChild(child);
        }
        return frag;
    }

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

                    new Subscriber(node, data, name);

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


    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(Publisher.global); // 添加訂閱者
                }
                return value;
            }
        });
    }


    // 將data中數據綁定到vm實例對象上
    function observe(data, vm){
        Object.keys(data).forEach((key) => {
            defineReact(vm, key, data[key]);
        })
    }


    function Vue(options){
        this.data = options.data;
        let id = options.el;

        observe(this.data, this); // 將每一個data屬相綁定到Vue的實例vm上

        //------------------------
        let container = document.getElementById(id);
        let fragment = virtualDom(container, this); // 這裏經過vm對象初始化
        container.appendChild(fragment);

    }


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

未完待續......

相關文章
相關標籤/搜索