vue雙向綁定代碼實現

vue數據雙向綁定是經過數據劫持結合發佈-訂閱模式實現的,具體再也不贅述,一圖以蔽之:html

 

1. 前言

每當被問到Vue數據雙向綁定原理的時候,你們可能都會脫口而出:Vue內部經過Object.defineProperty方法屬性攔截的方式,把data對象裏每一個數據的讀寫轉化成getter/setter,當數據變化時通知視圖更新。雖然一句話把大概原理歸納了,可是其內部的實現方式仍是值得深究的,本文就以通俗易懂的方式剖析Vue內部雙向綁定原理的實現過程。vue

2. 思路分析

所謂MVVM數據雙向綁定,即主要是:數據變化更新視圖,視圖變化更新數據。以下圖:
node

也就是說:正則表達式

  • 輸入框內容變化時,data 中的數據同步變化。即 view => model 的變化。
  • data 中的數據變化時,文本節點的內容同步變化。即 model => view 的變化。

要實現這兩個過程,關鍵點在於數據變化如何更新視圖,由於視圖變化更新數據咱們能夠經過事件監聽的方式來實現。因此咱們着重討論數據變化如何更新視圖。segmentfault

數據變化更新視圖的關鍵點則在於咱們如何知道數據發生了變化,只要知道數據在何時變了,那麼問題就變得迎刃而解,咱們只需在數據變化的時候去通知視圖更新便可。數組

3. 使數據對象變得「可觀測」

數據的每次讀和寫可以被咱們看的見,即咱們可以知道數據何時被讀取了或數據何時被改寫了,咱們將其稱爲數據變的‘可觀測’。緩存

要將數據變的‘可觀測’,咱們就要藉助前言中提到的Object.defineProperty方法了,關於該方法,MDN上是這麼介紹的:app

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

在本文中,咱們就使用這個方法使數據變得「可觀測」。測試

首先,咱們定義一個數據對象car

let car = {
        'brand':'BMW',
        'price':3000
    }

 

咱們定義了這個car的品牌brandBMW,價格price是3000。如今咱們能夠經過car.brandcar.price直接讀寫這個car對應的屬性值。可是,當這個car的屬性被讀取或修改時,咱們並不知情。那麼應該如何作纔可以讓car主動告訴咱們,它的屬性被修改了呢?

接下來,咱們使用Object.defineProperty()改寫上面的例子:

    let car = {}
    let val = 3000
    Object.defineProperty(car, 'price', {
        get(){
            console.log('price屬性被讀取了')
            return val
        },
        set(newVal){
            console.log('price屬性被修改了')
            val = newVal
        }
    })

 

經過Object.defineProperty()方法給car定義了一個price屬性,並把這個屬性的讀和寫分別使用get()set()進行攔截,每當該屬性進行讀或寫操做的時候就會出發get()set()。以下圖:

能夠看到,car已經能夠主動告訴咱們它的屬性的讀寫狀況了,這也意味着,這個car的數據對象已是「可觀測」的了。

爲了把car的全部屬性都變得可觀測,咱們能夠編寫以下兩個函數:

/**
     * 把一個對象的每一項都轉化成可觀測對象
     * @param { Object } obj 對象
     */
    function observable (obj) {
        if (!obj || typeof obj !== 'object') {
            return;
        }
        let keys = Object.keys(obj);
        keys.forEach((key) =>{
            defineReactive(obj,key,obj[key])
        })
        return obj;
    }
    /**
     * 使一個對象轉化成可觀測對象
     * @param { Object } obj 對象
     * @param { String } key 對象的key
     * @param { Any } val 對象的某個key的值
     */
    function defineReactive (obj,key,val) {
        Object.defineProperty(obj, key, {
            get(){
                console.log(`${key}屬性被讀取了`);
                return val;
            },
            set(newVal){
                console.log(`${key}屬性被修改了`);
                val = newVal;
            }
        })
    }

 

如今,咱們就能夠這樣定義car:

let car = observable({
        'brand':'BMW',
        'price':3000
    })

 

car的兩個屬性都變得可觀測了。

4. 依賴收集

完成了數據的'可觀測',即咱們知道了數據在何時被讀或寫了,那麼,咱們就能夠在數據被讀或寫的時候通知那些依賴該數據的視圖更新了,爲了方便,咱們須要先將全部依賴收集起來,一旦數據發生變化,就統一通知更新。其實,這就是典型的「發佈訂閱者」模式,數據變化爲「發佈者」,依賴對象爲「訂閱者」。

如今,咱們須要建立一個依賴收集容器,也就是消息訂閱器Dep,用來容納全部的「訂閱者」。訂閱器Dep主要負責收集訂閱者,而後當數據變化的時候後執行對應訂閱者的更新函數。

建立消息訂閱器Dep:

class Dep {
        constructor(){
            this.subs = []
        },
        //增長訂閱者
        addSub(sub){
            this.subs.push(sub);
        },
        //判斷是否增長訂閱者
        depend () {
            if (Dep.target) {
                this.addSub(Dep.target)
            }
        },

        //通知訂閱者更新
        notify(){
            this.subs.forEach((sub) =>{
                sub.update()
            })
        }
    }
Dep.target = null;

 

有了訂閱器,再將defineReactive函數進行改造一下,向其植入訂閱器:

function defineReactive (obj,key,val) {
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            get(){
                dep.depend();
                console.log(`${key}屬性被讀取了`);
                return val;
            },
            set(newVal){
                val = newVal;
                console.log(`${key}屬性被修改了`);
                dep.notify()                    //數據變化通知全部訂閱者
            }
        })
    }

 

從代碼上看,咱們設計了一個訂閱器Dep類,該類裏面定義了一些屬性和方法,這裏須要特別注意的是它有一個靜態屬性 target,這是一個全局惟一 的Watcher,這是一個很是巧妙的設計,由於在同一時間只能有一個全局的 Watcher 被計算,另外它的自身屬性 subs 也是 Watcher 的數組。

咱們將訂閱器Dep添加訂閱者的操做設計在getter裏面,這是爲了讓Watcher初始化時進行觸發,所以須要判斷是否要添加訂閱者。在setter函數裏面,若是數據變化,就會去通知全部訂閱者,訂閱者們就會去執行對應的更新的函數。

到此,訂閱器Dep設計完畢,接下來,咱們設計訂閱者Watcher.

5. 訂閱者Watcher

訂閱者Watcher在初始化的時候須要將本身添加進訂閱器Dep中,那該如何添加呢?咱們已經知道監聽器Observer是在get函數執行了添加訂閱者Wather的操做的,因此咱們只要在訂閱者Watcher初始化的時候出發對應的get函數去執行添加訂閱者操做便可,那要如何觸發get的函數,再簡單不過了,只要獲取對應的屬性值就能夠觸發了,核心緣由就是由於咱們使用了Object.defineProperty( )進行數據監聽。這裏還有一個細節點須要處理,咱們只要在訂閱者Watcher初始化的時候才須要添加訂閱者,因此須要作一個判斷操做,所以能夠在訂閱器上作一下手腳:在Dep.target上緩存下訂閱者,添加成功後再將其去掉就能夠了。訂閱者Watcher的實現以下:

 class Watcher {
        constructor(vm,exp,cb){
            this.vm = vm;
            this.exp = exp;
            this.cb = cb;
            this.value = this.get();  // 將本身添加到訂閱器的操做
        },

        update(){
            let value = this.vm.data[this.exp];
            let oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);
            },
        get(){
            Dep.target = this;  // 緩存本身
            let value = this.vm.data[this.exp]  // 強制執行監聽器裏的get函數
            Dep.target = null;  // 釋放本身
            return value;
        }
    }

 

過程分析:

訂閱者Watcher 是一個 類,在它的構造函數中,定義了一些屬性:

  • vm:一個Vue的實例對象;
  • exp:node節點的v-modelv-on:click等指令的屬性值。如v-model="name"exp就是name;
  • cb:Watcher綁定的更新函數;

當咱們去實例化一個渲染 watcher 的時候,首先進入 watcher 的構造函數邏輯,就會執行它的 this.get() 方法,進入 get 函數,首先會執行:

Dep.target = this;  // 緩存本身

 

實際上就是把 Dep.target 賦值爲當前的渲染 watcher ,接着又執行了:

let value = this.vm.data[this.exp]  // 強制執行監聽器裏的get函數

 

在這個過程當中會對 vm 上的數據訪問,其實就是爲了觸發數據對象的getter

每一個對象值的 getter都持有一個 dep,在觸發 getter 的時候會調用 dep.depend() 方法,也就會執行this.addSub(Dep.target),即把當前的 watcher 訂閱到這個數據持有的 dep 的 subs 中,這個目的是爲後續數據變化時候能通知到哪些 subs 作準備。

這樣實際上已經完成了一個依賴收集的過程。那麼到這裏就結束了嗎?其實並無,完成依賴收集後,還須要把 Dep.target 恢復成上一個狀態,即:

Dep.target = null;  // 釋放本身

 

由於當前vm的數據依賴收集已經完成,那麼對應的渲染Dep.target 也須要改變。

update()函數是用來當數據發生變化時調用Watcher自身的更新函數進行更新的操做。先經過let value = this.vm.data[this.exp];獲取到最新的數據,而後將其與以前get()得到的舊數據進行比較,若是不同,則調用更新函數cb進行更新。

至此,簡單的訂閱者Watcher設計完畢。

6. 測試

完成以上工做後,咱們就能夠來真正的測試了。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <h1 id="name"></h1>
    <input type="text">
    <input type="button" value="改變data內容" onclick="changeInput()">
    
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script>
    function myVue (data, el, exp) {
        this.data = data;
        observable(data);                      //將數據變的可觀測
        el.innerHTML = this.data[exp];           // 初始化模板數據的值
        new Watcher(this, exp, function (value) {
            el.innerHTML = value;
        });
        return this;
    }

    var ele = document.querySelector('#name');
    var input = document.querySelector('input');
    
    var myVue = new myVue({
        name: 'hello world'
    }, ele, 'name');
    
    //改變輸入框內容
    input.oninput = function (e) {
        myVue.data.name = e.target.value
    }
    //改變data內容
    function changeInput(){
        myVue.data.name = "xfcao"
    
    }
</script>
</body>
</html>

可是還有一個細節問題,咱們在賦值的時候是這樣的形式 ' myVue.data.name = "xfcao" ' 而咱們理想的形式是'  myVue.name = "xfcao" '爲了實現這樣的形式,咱們須要在new myVue的時候作一個代理處理,讓訪問myVue的屬性代理爲訪問myVue.data的屬性,實現原理仍是使用Object.defineProperty( )對屬性值再包一層:

index.js

function myVue (data, el, exp) {
    var self = this;
    this.data = data;
 
    Object.keys(data).forEach(function(key) {
        self.proxyKeys(key);  // 綁定代理屬性
    });
 
    observe(data);
    el.innerHTML = this.data[exp];  // 初始化模板數據的值
    new Watcher(this, exp, function (value) {
        el.innerHTML = value;
    });
    return this;
}
 
myVue.prototype = {
    proxyKeys: function (key) {
        var self = this;
        Object.defineProperty(this, key, {
            enumerable: false,
            configurable: true,
            get: function proxyGetter() {
                return self.data[key];
            },
            set: function proxySetter(newVal) {
                self.data[key] = newVal;
            }
        });
    }
}

 

observer.js

/**
     * 把一個對象的每一項都轉化成可觀測對象
     * @param { Object } obj 對象
     */
    function observable (obj) {
        if (!obj || typeof obj !== 'object') {
            return;
        }
        let keys = Object.keys(obj);
        keys.forEach((key) =>{
            defineReactive(obj,key,obj[key])
        })
        return obj;
    }
    /**
     * 使一個對象轉化成可觀測對象
     * @param { Object } obj 對象
     * @param { String } key 對象的key
     * @param { Any } val 對象的某個key的值
     */
    function defineReactive (obj,key,val) {
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            get(){
                dep.depend();
                console.log(`${key}屬性被讀取了`);
                return val;
            },
            set(newVal){
                val = newVal;
                console.log(`${key}屬性被修改了`);
                dep.notify()                    //數據變化通知全部訂閱者
            }
        })
    }
    class Dep {
        
        constructor(){
            this.subs = []
        }
        //增長訂閱者
        addSub(sub){
            this.subs.push(sub);
        }
        //判斷是否增長訂閱者
        depend () {
            if (Dep.target) {
                this.addSub(Dep.target)
            }
        }

        //通知訂閱者更新
        notify(){
            this.subs.forEach((sub) =>{
                sub.update()
            })
        }
        
    }
    Dep.target = null;

 

watcher.js

 class Watcher {
        constructor(vm,exp,cb){
            this.vm = vm;
            this.exp = exp;
            this.cb = cb;
            this.value = this.get();  // 將本身添加到訂閱器的操做
        }
        get(){
            Dep.target = this;  // 緩存本身
            let value = this.vm.data[this.exp]  // 強制執行監聽器裏的get函數
            Dep.target = null;  // 釋放本身
            return value;
        }
        update(){
            let value = this.vm.data[this.exp];
            let oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);
            }
    }
}

 

最後,咱們來實現一個編譯器

編譯器:掃描和解析每一個節點元素,替換模版數據,綁定事件監聽函數,初始化訂閱者

/**
 * 編譯器
 * @param {String} el 根元素
 * @param {Object} vm vue對象
 */
function Compile(el, vm) {
  this.el = document.querySelector(el);
  this.vm = vm;
  this.fragment = null;
  this.init();
}
 
Compile.prototype = {
  constructor: Compile,
  init: function() {
    if (this.el) {
      this.fragment = this.nodeToFragment(this.el); // 移除頁面元素生成文檔碎片
      this.compileElement(this.fragment); // 編譯文檔碎片
      this.el.appendChild(this.fragment);
    } else {
      console.log('DOM Selector is not exist');
    }
  },
  /**
   * 頁面DOM節點轉化成文檔碎片
   */
  nodeToFragment: function(el) {
    var fragment = document.createDocumentFragment();
    var child = el.firstChild;
    while(child) {
      fragment.appendChild(child); // append後,原el上的子節點被刪除了,掛載在文檔碎片上
      child = el.firstChild;
    }
    return fragment;
  },
  /**
   * 編譯文檔碎片,遍歷到當前是文本節點,則編譯文本節點;若是當前是元素節點,而且存在子節點,則繼續遞歸遍歷
   */
  compileElement: function(fragment) {
    var childNodes = fragment.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
      var reg = /\{\{\s*((?:.|\n)+?)\s*\}\}/g;
      var text = node.textContent;
 
      if (self.isElementNode(node)) {
        self.compileAttr(node);
      } else if (self.isTextNode(node) && reg.test(text)) { // test() 方法用於檢測一個字符串是否匹配某個模式
        reg.lastIndex = 0
        self.compileText(node, reg.exec(text)[1]); // exec() 方法用於檢索字符串中的正則表達式的匹配
      }
 
      if (node.childNodes && node.childNodes.length) { // 遞歸遍歷
        self.compileElement(node);
      }
    })
  },
  /**
   * 編譯屬性
   */
  compileAttr: function(node) {
    var self = this;
    var nodeAttrs = node.attributes;
 
    Array.prototype.forEach.call(nodeAttrs, function(attr) {
      var attrName = attr.name; // attrName是DOM屬性名,而exp是vue對象屬性名
      
      if (self.isDirective(attrName)) { // 只對vue自己指令進行操做
        var exp = attr.value; // 屬性名或函數名
        if (self.isOnDirective(attrName)) { // v-on指令
          self.compileOn(node, self.vm, attrName, exp);
        } else if (self.isBindDirective(attrName)) { // v-bind指令
          self.compileBind(node, self.vm, attrName, exp);
        } else if (self.isModelDirective(attrName)) { // v-model
          self.compileModel(node, self.vm, attrName, exp);
        }
        
        node.removeAttribute(attrName);
      }
    })
  },
  /**
   * 編譯v-on指令
   */
  compileOn: function(node, vm, attrName, exp) {
    var onReg = /^v-on:|^@/;
    var eventType = attrName.replace(onReg, '');
    var cb = vm.methods[exp];
 
    node.addEventListener(eventType, cb.bind(vm), false);
  },
  /**
   * 編譯v-bind指令
   */
  compileBind: function(node, vm, attrName, exp) {
    var bindReg = /^v-bind:|^:/;
    var attr = attrName.replace(bindReg, '');
    
    node.setAttribute(attr, vm.data[exp]);
 
    new Watcher(vm, exp, function(val) {
      node.setAttribute(attr, val);
    });
  },
  /**
   * 編譯v-model指令
   */
  compileModel: function(node, vm, attrName, exp) {
    var self = this;
    var modelReg = /^v-model/;
    var attr = attrName.replace(modelReg, '');
    var val = vm.data[exp];
    
    self.updateModel(node, val); // 初始化視圖
 
    new Watcher(vm, exp, function(value) { // 添加一個訂閱者到訂閱器
     self.updateModel(node, value);
    });
 
    node.addEventListener('input', function(e) { // 綁定input事件
      var newVal = e.target.value;
      if (val == newVal) {
        return;
      }
      self.vm.data[exp] = newVal;
    }, false);
  },
 
  /**
   * 屬性是不是vue指令,包括v-xxx:,:xxx,@xxx
   */
  isDirective: function(attrName) {
    var dirReg = /^v-|^:|^@/;
    return dirReg.test(attrName);
  },
  /**
   * 屬性是不是v-on指令
   */
  isOnDirective: function(attrName) {
    var onReg = /^v-on:|^@/;
    return onReg.test(attrName);
  },
  /**
   * 屬性是不是v-bind指令
   */
  isBindDirective: function(attrName) {
    var bindReg = /^v-bind:|^:/;
    return bindReg.test(attrName);
  },
  /**
   * 屬性是不是v-model指令
   */
  isModelDirective: function(attrName) {
    var modelReg = /^v-model/;
    return modelReg.test(attrName);
  },
  /**
   * 編譯文檔碎片節點文本,即對標記替換
   */
  compileText: function(node, exp) {
    var self = this;
    var initText = this.vm.data[exp];
 
    this.updateText(node, initText); // 初始化視圖
 
    new Watcher(this.vm, exp, function(val) {
      self.updateText(node, val); // node?
    });
  },
  /**
   * 更新文本節點
   */
  updateText(node, val) {
    node.textContent = typeof val == 'undefined'? '': val;
  },
  updateModel(node, val, oldVal) {
    node.value = typeof val == 'undefined'? '': val;
  },
  /**
   * 判斷元素節點
   */
  isElementNode(node) {
    return node.nodeType == 1;
  },
  /**
   * 判斷文本節點
   */
  isTextNode(node) {
    return node.nodeType == 3;
  }
}

參考:http://www.javashuo.com/article/p-zwrvfaia-gr.html

http://www.javashuo.com/article/p-amemhaoy-gq.html

http://www.javashuo.com/article/p-vbvongpy-ea.html

相關文章
相關標籤/搜索