Vue2.0源碼閱讀筆記--雙向綁定實現原理

  上一篇 文章 瞭解了Vue.js的生命週期。這篇分析Observe Data過程,瞭解Vue.js的雙向數據綁定實現原理。javascript

1、實現雙向綁定的作法

  前端MVVM最使人激動的就是雙向綁定機制了,實現雙向數據綁定的作法大體有以下三種:html

1.發佈者-訂閱者模式(backbone.js)

思路:使用自定義的data屬性在HTML代碼中指明綁定。全部綁定起來的JavaScript對象以及DOM元素都將「訂閱」一個發佈者對象。任什麼時候候若是JavaScript對象或者一個HTML輸入字段被偵測到發生了變化,咱們將代理事件到發佈者-訂閱者模式,這會反過來將變化廣播並傳播到全部綁定的對象和元素。前端

2.髒值檢查(angular.js)

思路:angular.js 是經過髒值檢測的方式比對數據是否有變動,來決定是否更新視圖,最簡單的方式就是經過 setInterval() 定時輪詢檢測數據變更,angular只有在指定的事件觸發時進入髒值檢測,大體以下:vue

  • DOM事件,譬如用戶輸入文本,點擊按鈕等。( ng-click )java

  • XHR響應事件 ( $http )node

  • 瀏覽器Location變動事件 ( $location )git

  • Timer事件( $timeout , $interval )github

  • 執行 $digest() 或 $apply()segmentfault

3.數據劫持(Vue.js)

思路: vue.js 則是採用數據劫持結合發佈者-訂閱者模式的方式,經過Object.defineProperty()來劫持各個屬性的settergetter,在數據變更時發佈消息給訂閱者,觸發相應的監聽回調。瀏覽器

因而可知,Object.defineProperty() 這個API是Vue實現雙向數據綁定的關鍵,咱們先簡單瞭解下這個API,瞭解更多戳這裏

2、Object.defineProperty()

簡單例子:

var obj = {};
    Object.defineProperty(obj, 'hello', {
        get: function() {
            console.log('get val:'+ val);
            return val;
       },
      set: function(newVal) {
            val = newVal;
            console.log('set val:'+ val);
        }
    });
obj.hello='111';
obj.hello;

結果:

若是去掉 obj.hello=‘111’ 這行代碼,則get的返回值val會報錯val is not defined。可見Object.defineProperty() 監控對數據的操做,能夠自動觸發數據同步。下面咱們先用Object.defineProperty()來實現一個很是簡單的雙向綁定。

3、實現簡單的雙向綁定

 最簡單例子:

<!DOCTYPE html>
 <head></head>
 <body>
  <div id="app">
    <input type="text" id="a">
    <span id="b"></span>
  </div>

  <script type="text/javascript">
   var obj = {};
   Object.defineProperty(obj, 'hello', {
       get: function() {
           console.log('get val:'+ val);
           return val;
       },
       set: function(newVal) {
            val = newVal;
            console.log('set val:'+ val);
            document.getElementById('a').value = val;
            document.getElementById('b').innerHTML = val;
       }
    });
    document.addEventListener('keyup', function(e) {
      obj.hello = e.target.value;
    });
   </script>
  </body>
</html>

實現效果以下:

上面例子直接用了dom操做改變了文本節點的值,並且是在咱們知道是哪一個id的狀況下,經過document.getElementById 獲取到相應的文本節點,而後直接修改文本節點的值,這種作法是最簡單粗暴的。

封裝成一個框架,確定不能是這種作法,因此咱們須要一個解析dom,並能修改dom中相應的變量的模塊。

4、實現簡單Compile

首先咱們須要獲取文本中真實的dom節點,而後再分析節點的類型,根據節點類型作相應的處理。

在上面例子咱們屢次操做了dom節點,爲提升性能和效率,會先將全部的節點轉換城文檔碎片fragment進行編譯操做,解析操做完成後,再將fragment添加到原來的真實dom節點中。

<!DOCTYPE html>
  <head></head>
  <body>
  <div id="app">
    <input type="text" id="a" v-model="text">
    {{text}}
  </div>
 <script type="text/javascript">
  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) {
          self.compileElement(child, vm);
          frag.append(child); // 將全部子節點添加到fragment中,child是指向元素首個子節點的引用。將child引用指向的對象append到父對象的末尾,原來child引用的對象就跳到了frag對象的末尾,而child就指向了原本是排在第二個的元素對象。如此循環下去,連接就逐個日後跳了
        }
        return frag;
      },
      compileElement: function(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.addEventListener('input', function(e) {
                // 給相應的data屬性賦值,進而觸發該屬性的set方法
                 vm.data[name]= e.target.value;
              });
              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 Vue(options) { this.data = options.data; var data = this.data; var id = options.el; var dom =new Compile(document.getElementById(id),this); // 編譯完成後,將dom返回到app中 document.getElementById(id).appendChild(dom); } var vm = new Vue({ el: 'app', data: { text: 'hello world' } }); </script> </body> </html>

結果:

到這,咱們作到了獲取文本中真實的dom節點,而後分析節點的類型,並能處理節點中相應的變量如上面代碼中的{{text}},最後渲染到頁面中。接着咱們須要和雙向綁定聯繫起來,實現{{text}}響應式的數據綁定。

5、實現簡單observe

簡單的observe定義以下:

須要監控data的屬性值,這個對象的某個值賦值,就會觸發setter,這樣就能監聽到數據變化。而後注意vm.data[name]屬性將改成vm[name]

完整代碼以下:

<!DOCTYPE html>
  <head></head>
  <body>
  <div id="app">
    <input type="text" id="a" v-model="text">
    {{text}}
  </div>
<script type="text/javascript">
  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) {
          self.compileElement(child, vm);
          frag.append(child); // 將全部子節點添加到fragment中
        }
        return frag;
      },
      compileElement: function(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.addEventListener('input', function(e) {
                // 給相應的data屬性賦值,進而觸發該屬性的set方法
                 vm[name]= e.target.value;
              });
              node.value = vm[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[name]; // 將data的值賦給該node
            // new Watcher(vm, node, name);
          }
        }
      },
    }
    function defineReactive (obj, key, val) {
      Object.defineProperty(obj, key, {
        get: function() {
          return val;
        },
        set: function (newVal) {
          if(newVal === val) return;
          val = newVal;
          console.log(val);
        }
      })
    }
    function observe(obj, vm) {
      Object.keys(obj).forEach(function(key) {
        defineReactive(vm, key, obj[key]);
      })
    }
   function Vue(options) {
      this.data = options.data;
      var data = this.data;
      observe(data, this);
      var id = options.el;
      var dom =new Compile(document.getElementById(id),this);
      // 編譯完成後,將dom返回到app中
      document.getElementById(id).appendChild(dom);
    }
    var vm = new Vue({
      el: 'app',
      data: {
        text: 'hello world'
      }
    });
  </script>
  </body>
</html>
View Code

結果:

到這,雖然set方法觸發了,可是文本節點{{text}}的內容沒有變化,要讓綁定的文本節點同步變化,咱們須要引入訂閱發佈模式。

6、訂閱發佈模式

訂閱發佈模式(又稱觀察者模式)定義了一種一對多的關係,讓多個觀察者同時監聽某一個主題對象,這個主題對象的狀態發生改變時就會通知全部觀察者對象。

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

 首先咱們要一個收集訂閱者的容器,定義一個Dep做爲主題對象

而後定義訂閱者Watcher

添加訂閱者Watcher到主題對象Dep,發佈者發出通知放到屬性監聽裏面

最後須要訂閱的地方

至此,比較簡單地實現了咱們第三步用dom操做實現的雙向綁定效果,代碼:

<!DOCTYPE html>
  <head></head>
  <body>
  <div id="app">
    <input type="text" id="a" v-model="text">
    {{text}}
  </div>
  <script type="text/javascript">
  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) {
          self.compileElement(child, vm);
          frag.append(child); // 將全部子節點添加到fragment中
        }
        return frag;
      },
      compileElement: function(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.addEventListener('input', function(e) {
                // 給相應的data屬性賦值,進而觸發該屬性的set方法
                 vm[name]= e.target.value;
              });
              // node.value = vm[name]; // 將data的值賦給該node
              new Watcher(vm, node, name, 'value');
            }
          };
        }
        //節點類型爲text
        if(node.nodeType === 3) {
          if(reg.test(node.nodeValue)) {
            var name = RegExp.$1; // 獲取匹配到的字符串
            name = name.trim();
            // node.nodeValue = vm[name]; // 將data的值賦給該node
            new Watcher(vm, node, name, 'nodeValue');
          }
        }
      },
    }
    function Dep() {
      this.subs = [];
    }
    Dep.prototype = {
      addSub: function(sub) {
        this.subs.push(sub);
      },
      notify: function() {
        this.subs.forEach(function(sub) {
          sub.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() {
        this.value = this.vm[this.name]; //觸發相應屬性的get
      }
    }
    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;
          console.log(val);
          // 做爲發佈者發出通知
          dep.notify();
        }
      })
    }
    function observe(obj, vm) {
      Object.keys(obj).forEach(function(key) {
        defineReactive(vm, key, obj[key]);
      })
    }

   function Vue(options) {
      this.data = options.data;
      var data = this.data;
      observe(data, this);
      var id = options.el;
      var dom =new Compile(document.getElementById(id),this);
      // 編譯完成後,將dom返回到app中
      document.getElementById(id).appendChild(dom);
    }
    var vm = new Vue({
      el: 'app',
      data: {
        text: 'hello world'
      }
    });
  </script>
  </body>
</html>
View Code

7、總結

關於雙向綁定的實現,看了網上不少資料,開始看到是對Vue源碼的解析,看的過程似懂非懂。後來找到參考資料1,而後本身跟着實現一遍,才理解許多。感謝這篇文章的做者,寫的由淺入深,比較好理解。爲了加深本身的理解,因而本身順着這個思路寫下這個筆記。本文主要了解了幾種雙向綁定的作法,而後先用原生JS,dom操做實現一個最簡單雙向綁定,在這個基礎上進行改裝,爲減小dom操做,實現簡單的Compile(編譯HTML);接着爲了實現數據監聽,實現observe;最後爲了實現數據的雙向綁定實現訂閱發佈模式。

雖然實現的比較簡單,有不少功能沒有考慮,不過這個過程仍是能夠理解到Vue實現雙向綁定的原理。過程當中,有思考:

1. Vue的源代碼中,用了文檔碎片fragment做爲真實節點的存儲嗎?

以前有據說用VDOM,在Vue源代碼中,也找過是否有建立文檔碎片,結果沒找到。看了參考資料4中,VDOM的介紹,好像是把節點用JS對象模擬。相似:

;模板
<ul id='list'>
  <li class='item'>Item 1</li>
  <li class='item'>Item 2</li>
  <li class='item'>Item 3</li>
</ul>

;js對象
var element = {
  tagName: 'ul', // 節點標籤名
  props: { // DOM的屬性,用一個對象存儲鍵值對
    id: 'list'
  },
  children: [ // 該節點的子節點
    {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
  ]
}

恩,這就又牽扯出模板了。先收住,我先儘可能把簡單的搞懂。

2.Compile模塊對v-model節點的解析,事件的綁定,我只實現簡單的,特定的v-model,還有其它事件綁定如v-on等沒有分析,看了別人的代碼,狀況一多起來,看得就有些吃力,但願後面本身會再來完善,給本身定一個這樣的框架在這,代碼github:戳這裏

參考資料:

1.http://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension

2.http://www.javashuo.com/article/p-brudryzf-hq.html

3.https://github.com/fwing1987/MyVue

4.http://www.kancloud.cn/zmwtp/vue2/149485

5.http://blog.cgsdream.org/2016/11/05/vue-source-analysis-1/

相關文章
相關標籤/搜索