1. 程式人生 > >記一次對vue雙向繫結的理解

記一次對vue雙向繫結的理解

之前有看過一次vue雙向繫結原理實現相關的部落格,看得似懂非懂的,然後也就擱淺了。
昨天腦海裡又突然燃起了要不這塊搞懂的衝動,於是乎又開始了一輪部落格轟炸,綜合研究了多位大神寫得關於vue雙向繫結的實現原理,然後結合自己的一些理解,就有了這篇文章了。
關於雙向繫結,我所知的兩類:(小的不才,目前只接觸過vue和angular)
一是angular1的髒檢查機制
二是vue的資料劫持配合觀察者模式(網上有很多寫的都是訂閱-釋出模式,我特意去查看了這兩種模式有什麼區別,大致是說觀察者模式中觀察者是被動接收統一的訊息,而訂閱-釋出模式中是訂閱者是可以自定義接收行為的,而vue中watcher物件中update方法都是一致的,就是同步資料,所有鄙人覺得用觀察者模式來形容可能會更貼切些,如果理解的不對,歡迎大家輕吐~)
下面我先貼一張我整理的原理圖

這裡寫圖片描述
我的理解:
1.編譯器會解析DOM元素,比如碰到v-model指令時,會對該DOM元素新增input監聽事件,當事件發生時,給data屬性值賦值,進而觸發該屬性的setter方法(這樣就實現了從view到model的同步);除了增加監聽事件外,還會例項化一個watch物件。
2.例項化一個watch物件主要有兩個作用,一個是通過呼叫data元素的getter方法,觸發Dep的add方法將watcher物件加入到訂閱器中(為了防止在除watch物件中其他其他呼叫了getter方法,進而重複新增監聽器,我們在watcher物件中給Dep新增一個不為空全域性變數,當全域性變數不為空時才新增,新增完後又將全域性變數置為null),第二個作用是宣告一個update方法,用來接收到訂閱器的通知後同步資料給節點進行渲染。
3.上面我們已經在屬性的getter方法中將watcher物件加入了訂閱器,當model層屬性值發生變化時會觸發setter方法,我們在setter中再去觸發訂閱器的notify,並在notify中觸發watcher物件的update方法(這樣就實現了從model到view的同步)
從圖中也可看出,主要有4大塊,我們依照流程圖的順序來分析一下:

  • compile
    先擼一波
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'); } } }, }

程式碼分析:
(1)使用 DocumentFragment 處理節點,速度和效能遠遠優於直接操作 DOM。Vue 進行編譯時,就是將掛載目標的所有子節點劫持(真的是劫持,通過 append 方法,DOM 中的節點會被自動刪除)到 DocumentFragment 中,經過一番處理後,再將 DocumentFragment 整體返回插入掛載目標。
(2)node.nodeType判斷節點型別,如果是元素的話,判斷該元素有沒有v-model
屬性,有則監聽input事件,將輸入框的值同步到變數中,同時例項化一個watcher。如果是文字的話,看是不是符合{{}}正則表示式,符合則是我們需要加入訂閱器的物件。

  • watcher-觀察者
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
      }
    }

程式碼分析:
(1)例項化watcher物件是有獲取物件的值this.vm[this.name],這對出發該物件的get方法,而在物件的get方法中就可以把這個觀察者加入到Dep中了,後面oberver可以看到
(2)update()方法就是觀察者接收到通知後用來同步資料給節點進行渲染的作用。

  • Observe-資料監測器

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

程式碼分析:
(1)資料監測器用到的知識點就是ES5的Object.defineProperty方法,改寫物件的set和get方法,在set方法中通知觀察該物件的所有watcher更新,在get方法中實現的是當watcher第一次呼叫get方法的時候把自己繫結給訂閱器進行管理

  • Dep-訂閱器
function Dep() {
      this.subs = [];
    }
    Dep.prototype = {
      addSub: function(sub) {
        this.subs.push(sub);
      },
      notify: function() {
        this.subs.forEach(function(sub) {
          sub.update();
        })
      }
    }

(1)它的addsub方法在observer的get方法中被呼叫,notify方法在observer的set方法中被呼叫
至此,四大塊就分析完了,回過頭來再去看那張圖,你理解的會更深刻一點。
下面終極大boss出場了MVVM.js

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

index.html

<!DOCTYPE html>
  <head>
  </head>
  <body>
  <div id="app">
    <input type="text" id="a" v-model="text">
    {{text}}
  </div>
  <script src="src/Dep.js"></script>
  <script src="src/Observe.js"></script>
  <script src="src/Watcher.js"></script>
  <script src="src/Compile.js"></script>
  <script src="src/MVVM.js"></script>
  <script>
    var vm = new Vue({
      el: 'app',
      data: {
        text: 'hello world'
      }
    });
  </script>
  </body>
</html>

程式碼分析:
這裡自定義了vue物件,解析包含的html片段,實現的雙向繫結。