實現一個Vue的雙向綁定

前言

使用Vue技術棧也有2年了,對裏邊的各種API,屬性,內置組件封裝可以說是非常熟練了,一直知道雙向數據綁定的原理是通過數據劫持,結合發佈者-訂閱者模式的方式來實現的;可理論始終是理論,忍不住還是動動小手擼了一把 ; 請大家儘管吐槽吧 。

參考文章 簡書:https://www.jianshu.com/p/23180880d3aa

Github 源碼 - 來顆小星星我也不介意哦 biu ~ biu ~

我們先來看一張圖,相信大家在不少的博客,貼吧,論壇等都有看到過這張圖例 ,那麼它到底是什麼意思呢 ?下面聽我細細道來 。
mvvm
這張圖我先做個簡要的描述:

首先創建一個實例對象,分別觸發了 compile 解析指令 和 observer 監聽器,

compile 解析指令則循環遞歸 解析 類似 v-model 這樣的指令,初始化 data 綁定數據,同時爲每個節點創建一個訂閱者 watcher

observer 監聽器 則利用了 Object.defineProperty() 方法的描述屬性裏邊的 set,get 方法,來監聽數據變化,

get 方法是在創建實例對象,生成dom節點的時候都會觸發,所以:在compile 解析指令編譯的時候,依次給每一個節點添加了一個訂閱者 watcher 到主題對象 Dep (Dep 訂閱者集合,我們暫稱爲 訂閱器)

set 方法則是數據發生改變了,通知Dep訂閱器裏的所有wachter,然後找到對應訂閱者 wachter 觸發對應 update 更新視圖

簡單的說明就是這樣了。好吧,廢話不多說直接上代碼:

1,實現一個解析器Compile
可以掃描和解析每個節點的相關指令(v-model,v-on等指令),如果節點存在v-model,v-on等指令,則解析器Compile初始化這類節點的模板數據,使之可以顯示在視圖上,同時初始化相應的訂閱者(Watcher)

/*
  第一步
  1,創建文檔碎片,劫持所有dom節點,重繪dom節點
  2,重繪dom節點,初始化文檔碎片綁定數據 實現文檔編譯 compile
  3, 爲每一個節點創建一個 watcher  
  */
  function getDocumentFragment(node, vm) {
    var flag = document.createDocumentFragment();
    var child;
    while (child = node.firstChild) {
      /*
       while (child = node.firstChild)
       相當於 
       child = node.firstChild
       while (child)
       */
      compile(child, vm);
      flag.appendChild(child);
    }
    node.appendChild(flag);
  }
  function compile(node, vm) {
    /*
   nodeType 返回數字,表示當前節點類型    
   1 Element    代表元素    Element, Text, 
   2    Attr    代表屬性    Text, EntityReference
   3    Text    代表元素或屬性中的文本內容。
   . . . 更多請查看文檔
   */
    if (node.nodeType === 1) {
      // 獲取當前元素的attr屬性
      var attr = node.attributes;
      for (let i = 0; i < attr.length; i++) {
        // nodeName 是attr屬性 key 即名稱 , 匹配自定義 v-m
        if (attr[i].nodeName === 'v-m') {
          // 獲取當前值 即 v-m = "test" 裏邊的 test 
          let name = attr[i].nodeValue;
          // 當前節點輸入事件
          node.addEventListener('keyup', function (e) {
            vm[name] = e.target.value;
          });
          // 頁面元素寫值  vm.data[name] 即 vm.data['test'] 即 MVVM
          node.value = vm.data[name];
          //最後移除標籤中的 v-m 屬性
          node.removeAttribute('v-m');
          // 爲每一個節點創建一個 watcher  
          new Watcher(vm, node, name, "input");
        }
      }
      /*
      繼續遞歸調用 文檔編譯 實現 視圖更新 ;
      */
      if (child = node.firstChild) {
        /*
        if (child = node.firstChild)
        相當於 
        child = node.firstChild
        id(child)
        */
        compile(child, vm);
      }
    }
    if (node.nodeType === 3) {
      let reg = /\{\{(.*)\}\}/;
      if (reg.test(node.nodeValue)) {
        let name = RegExp.$1.trim();
        node.nodeValue = vm.data[name];
        // 爲每一個節點創建一個 watcher  
        new Watcher(vm, node, name, "text");
      }
    }
  }

2,實現一個監聽器Observer
用來劫持並監聽所有屬性,如果有變動的,就通知訂閱者

/* 
  第二步
  1,獲取當前實例對象的  data 屬性 key  
     observer(當前實例對象 data ,當前實例對象)
  2,使用 Object.defineProperty 方法 實現監聽 
  */
  function observe(data, vm) {
    Object.keys(data).forEach(function (key) {
      defineReactive(vm, key, data[key]);
    });
  }
  function defineReactive(vm, key, val) {
    /*
    Object.defineProperty
    obj
    要在其上定義屬性的對象。
    prop
    要定義或修改的屬性的名稱。
    descriptor
    將被定義或修改的屬性描述符。 描述符有很多,就包括我們要市用 set , get 方法
    */
    var dep = new Dep();
    Object.defineProperty(vm, key, {
      get: function () {
        /* 
        if (Dep.target) dep.addSub(Dep.target);
        看到這段代碼不要差異,生成每一個 dom節點,都會走 get 方法
        這裏爲每一個節點添加一個訂閱者到主題對象 Dep
        */
        if (Dep.target) dep.addSub(Dep.target);
        console.log(val)
        return val;
      },
      set: function (newValue) {
        if (newValue === val) return;
        val = newValue;
        console.log(val + "=>" + newValue)
        // 通知所有訂閱者
        dep.notify();
      }
    });
  }

3,實現一個訂閱者Watcher
每一個Watcher都綁定一個 update,watcher 可以收到屬性的變化通知並執行相應的 update ,從而更新視圖。

/*
  第三步
  1,實現一個 watcher 觀察者/訂閱者添加方法update 渲染視圖
  2,定義一個消息訂閱器
    很簡單,維護一個數組,用來收集訂閱者
    消息訂閱器原型掛載兩個方法 分別是  
    addSub 添加一個訂閱者   
    notify 數據變動 通知 這個訂閱者的 update 方法
  */
  function Watcher(vm, node, name, nodeType) {
    Dep.target = this;
    this.vm = vm;
    this.node = node;
    this.name = name;
    this.nodeType = nodeType;
    this.update();
    console.log(Dep.target)
    Dep.target = null;
  }
  Watcher.prototype = {
    update: function () {
      /*
      this.node 指向當前修改的 dom 元素
      this.vm 指向當前 dom 的實例對象
      根據 nodeType 類型 賦值渲染頁面
      */
      if (this.nodeType === 'text') {
        this.node.nodeValue = this.vm[this.name]
      }
      if (this.nodeType === 'input') {
        this.node.value = this.vm[this.name]
      }
    }
  }
  function Dep() {
    this.subs = [];
  }
  Dep.prototype = {
    addSub: function (sub) {
      this.subs.push(sub);
    },
    notify: function () {
      this.subs.forEach(function (sub) {
        sub.update();
      });
    }
  }

4,實現類似Vue的MVVM實例

/*
  創建一個構造函數,並生成實例化對象 vm
  */
  function Vue(o) {
    this.id = o.el;
    this.data = o.data;
    observe(this.data, this);
    getDocumentFragment(document.getElementById(this.id), this);
  }
  var vm = new Vue({
    el: 'app',
    data: {
      msg: 'HiSen',
      test: 'Hello,MVVM'
    }
  });

也許看到最後大家也沒有看出個所以然,曾幾何時的我跟你們一樣,看來看去,就是這麼幾段代碼;其實我也是參考,揣摩,調試,最後才成功的 ;
建議:拿下我的源碼,自己跑一跑,看一看,是騾子是馬拉出來溜溜。去溜溜 ~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章