天天看點

Vue學習 -- 雙向資料綁定(二)初始化分析DocumentFragmentcompiledefineProperty訂閱者Dep 及 Watcher最終代碼

上一章雙向資料綁定(一)主要講解了Object.defineProperty() 的作用及用法。

github代碼位址

現在咱們一起實作一個MyVue

MyVue封裝

  • 初始化
    • html
    • myVue.js
  • 分析
  • DocumentFragment
    • 使用方式
    • 将子節點劫持到文檔
  • compile
    • nodeType = 1 :元素
    • nodeType = 3 :文本
    • 示例
    • 效果圖
  • defineProperty
  • 訂閱者
  • Dep 及 Watcher
  • 最終代碼

初始化

首先建立一個html、myVue.js ,然後按照vue的寫法,看看會是什麼樣子。

html

<body>
  <div id="app">
    <input id="inputId" placeholder="請輸入" v-model="text" />
    <div>
      這是内容text:
      <p id="txtId">{{text}}</p>
    </div>
  </div>
  <script src="./assets/myVue.js"></script>
  <script>
    var vm = new MyVue({
      el:'app',
      data:{
        text:'hello word'
      }
    })
  </script>
</body>
           

myVue.js

(function(win){

  function MyVue (opts){
    console.group('opts =======================')
    console.log(opts)
  }
  
  win.MyVue = MyVue;

})(window)
           
Vue學習 -- 雙向資料綁定(二)初始化分析DocumentFragmentcompiledefineProperty訂閱者Dep 及 Watcher最終代碼

分析

控制台列印的值,是咱們出入的對象,這裡沒有問題。

但是頁面展示的文本,卻不是咱們想要的,text 我們希望替換成 hello word 才對。那麼咱們首先就應該先替換dom節點。

DocumentFragment

documentFragment最大的特點是可以像document一樣,但不是真實 dom 樹的一部分,它的變化不會觸發 dom樹的重新渲染,且不會導緻性能等問題。

最常用的方法是使用文檔片段作為參數,這種情況下被添加(append)或被插入(inserted)的是片段的所有子節點, 而非片段本身。因為所有的節點會被一次插入到文檔中,而這個操作僅發生一個重渲染的操作,而不是每個節點分别被插入到文檔中,因為後者會發生多次重渲染的操作。

使用方式

  1. 建立documentFragment對象flag
  2. 取出ul中的所有子節點并儲存到flag
  3. 更新flag中的所有節點(app的内容)
  4. 将flag插入到app

關于DocumentFragment具體可以戳這裡~

将子節點劫持到文檔

(function(win){

  function MyVue (opts){
    console.group('opts =======================')
    console.log(opts)

    var id = opts.el;
    var dom =nodeToFragment(document.getElementById(id),this);
    document.getElementById(id).appendChild(dom)
  }

  //文檔片段
  function nodeToFragment (node,vm){
    var flag = document.createDocumentFragment();
    var child;

    // firstChild 屬性傳回被選節點的第一個子節點

    //如果 while (child = node.firstChild) 成立
    // appendChild 方法向節點添加最後一個子節點
    // appendChild 方法有個隐蔽的地方,就是調用以後 child 會從原來 DOM 中移除
    // 第二次循環時,node.firstChild 已經不再是之前的第一個子元素了
    // 直到  child = node.firstChild 不成立

    while (child = node.firstChild){

      //将子節點劫持到文檔片段中
      flag.appendChild(child);
    }

    return flag
  }


  win.MyVue = MyVue;
})(window)
           

這部分隻是對子節點進行了劫持,具體變化還需要資料綁定。

compile

compile的作用主要是資料初始化,确定資料綁定關系。使用node.nodeType 判斷節點類型。

nodeType = 1 :元素

元素的話,需要判斷兩個方面:

  1. 是否還有子節點,有的話遞歸循環重複一下compile操作
  2. 目前節點是否有屬性 v-model,有的話設定value值,并删除v-model屬性

nodeType = 3 :文本

文本的話,需要判斷其是否有 {{}} ,有的話從data中取值替換

示例

(function(win){

  function MyVue (opts){
    
    this.data = opts.data;

    var id = opts.el;

    var dom =nodeToFragment(document.getElementById(id),this);
    document.getElementById(id).appendChild(dom)
  }

  //文檔片段
  function nodeToFragment (node,vm){
    var flag = document.createDocumentFragment();
    var child;

    // firstChild 屬性傳回被選節點的第一個子節點
    //如果 while (child = node.firstChild) 成立
    // appendChild 方法向節點添加最後一個子節點
    // appendChild 方法有個隐蔽的地方,就是調用以後 child 會從原來 DOM 中移除
    // 第二次循環時,node.firstChild 已經不再是之前的第一個子元素了
    // 直到  child = node.firstChild 不成立

    while (child = node.firstChild){
      compile(child,vm);
      //将子節點劫持到文檔片段中
      flag.appendChild(child);
    }

    return flag
  }

  //資料初始化
  function compile (node,vm){
    var reg = /\{\{(.*)\}\}/;
  
    //節點類型:元素
    if(node.nodeType ===1){
      var attr = node.attributes;
      // 有屬性
      if(attr.length){
        for(var i=0;i<attr.length;i++){
        
          if(attr[i].nodeName == 'v-model'){
    
            var name = attr[i].nodeValue;
  
            node.value = vm.data[name];
    
            node.removeAttribute('v-model');
    
          }
        }
      }
      

      var childs = node.childNodes;
      //有子節點
      if(childs.length){
        
        for(var i=0;i<childs.length;i++){
          compile(childs[i],vm)
        }
      }
      
    }  
    //節點類型:text
    else if(node.nodeType ===3){
      if(reg.test(node.nodeValue)){
        //指的是與正規表達式比對的第一個 子比對(以括号為标志)字元串
        var name = RegExp.$1;
        name = name.trim();
        node.nodeValue = vm.data[name];
      }
    }
  }


  win.MyVue = MyVue;
})(window)
           

重點 :

1、MyVue 裡面需要指定

this.data = opts.data;

2、正則比對符合 /{{(.*)}}/ ,取其值,node.nodeValue = vm.data[name]

效果圖

Vue學習 -- 雙向資料綁定(二)初始化分析DocumentFragmentcompiledefineProperty訂閱者Dep 及 Watcher最終代碼

到這裡,已經可以實作 {{}} 替換成定義的值了。但是改變input的值,文案還無法改變,是以需要加defineProperty攔截。不清楚戳這裡~

defineProperty

// MyVue 添加observer 方法初始化
observer(this.data,this)



// v-model 判斷處,添加input方法
node.addEventListener('input', function (e) {
  // 給相應的 data 屬性指派,進而觸發該屬性的 set 方法
  vm[name] = e.target.value;
});


// observer
function observer (obj, vm) {
    Object.keys(obj).forEach(function (key) {
      defineReactive(vm, key, obj[key]);
    })
  }
  
  function defineReactive (obj, key, val) {
    
    Object.defineProperty(obj, key, {
      get: function () {

        return val
      },
      set: function (newVal) {
        if (newVal === val) return
        console.log(newVal,'newVal')
        val = newVal;
      }
    });
  }


           

效果圖

随着輸入框值的改變,set 裡的newVal 也會發生變化

Vue學習 -- 雙向資料綁定(二)初始化分析DocumentFragmentcompiledefineProperty訂閱者Dep 及 Watcher最終代碼

訂閱者

text屬性變化了,set方法觸發了,但是文本還沒有改變,如何讓文本同步,這裡又有一個知識點:訂閱釋出模式。

訂閱釋出模式(又稱觀察者模式)定義了一種一對多的關系,讓多個觀察者同時監聽某一個主題對象,這個主題對象的狀态發生改變時就會通知所有觀察者對象。

釋出者發出通知 => 主題對象收到通知并推送給訂閱者 => 訂閱者執行相應操作

var pub = {
      publish:function (){
        dep.notify()
      }
    }

    var sub1 = {
      update:function(){console.log(1)}
    }


    var sub2 = {
      update:function(){console.log(2)}
    }


    var sub3 = {
      update:function(){console.log(3)}
    }

    function Dep (){
      this.subs = [sub1,sub2,sub3];
    }

    Dep.prototype.notify = function(){
      this.subs.forEach(function(sub){
        sub.update()
      })
    };

    var dep = new Dep();

    pub.publish()
           

簡單來說dep是一個數組集合,每次觸發notify時,進行update更新

Dep 及 Watcher

//input監聽
new Watcher(vm,node,name,'input')
//text 監聽
new Watcher(vm,node,name,'text')

//Dep
function Dep () {
    this.subs = []
  }
  
  Dep.prototype = {
    addSub: function(sub) {
      console.log(sub,'addSub')
      this.subs.push(sub);
    },
  
    notify: function() {
      this.subs.forEach(function(sub) {
        sub.update();
      });
    }
  }

// Watcher
  function Watcher (vm, node, name, nodeType) {
    Dep.target = this;
    this.name = name;
    this.node = node;
    this.vm = vm;
    this.nodeType = nodeType;
    this.update();
    Dep.target = null;
  }
  
  Watcher.prototype = {
    update: function () {
      this.get();
      if (this.nodeType == 'text') {
        this.node.nodeValue = this.value;
      }
      if (this.nodeType == 'input') {
        this.node.value = this.value;
      }
    },
    // 擷取 data 中的屬性值
    get: function () {
      this.value = this.vm[this.name]; // 觸發相應屬性的 get
    }
  }
           

最終代碼

(function(win){

  function MyVue (opts){
    this.data = opts.data;

    observer(this.data,this)

    var id = opts.el;
    var dom =nodeToFragment(document.getElementById(id),this);
    document.getElementById(id).appendChild(dom)
  }

  //文檔片段
  function nodeToFragment (node,vm){
    var flag = document.createDocumentFragment();
    var child;

    // firstChild 屬性傳回被選節點的第一個子節點
    //如果 while (child = node.firstChild) 成立
    // appendChild 方法向節點添加最後一個子節點
    // appendChild 方法有個隐蔽的地方,就是調用以後 child 會從原來 DOM 中移除
    // 第二次循環時,node.firstChild 已經不再是之前的第一個子元素了
    // 直到  child = node.firstChild 不成立

    while (child = node.firstChild){
      compile(child,vm);
      //将子節點劫持到文檔片段中
      flag.appendChild(child);
    }

    return flag
  }

  //資料初始化
  function compile (node,vm){
    var reg = /\{\{(.*)\}\}/;
  
    //節點類型:元素
    if(node.nodeType ===1){
      var attr = node.attributes;
      // 有屬性
      if(attr.length){
        for(var i=0;i<attr.length;i++){
        
          if(attr[i].nodeName == 'v-model'){
    
            var name = attr[i].nodeValue;

            node.addEventListener('input', function (e) {
              // 給相應的 data 屬性指派,進而觸發該屬性的 set 方法
              vm[name] = e.target.value;
            });
  
            node.value = vm.data[name];
    
            node.removeAttribute('v-model');
    
          }
        }

        new Watcher(vm,node,name,'input')
      }
      

      var childs = node.childNodes;
      //有子節點
      if(childs.length){
        
        for(var i=0;i<childs.length;i++){
          compile(childs[i],vm)
        }
      }
      
    }  
    //節點類型:text
    else if(node.nodeType ===3){
      if(reg.test(node.nodeValue)){
        //指的是與正規表達式比對的第一個 子比對(以括号為标志)字元串
        var name = RegExp.$1;
        name = name.trim();

        node.nodeValue = vm.data[name];

        new Watcher(vm,node,name,'text')
      }
    }
  }


  function observer (obj, vm) {
    Object.keys(obj).forEach(function (key) {
      defineReactive(vm, key, obj[key]);
    })
  }
  
  function defineReactive (obj, key, val) {
    var dep = new Dep();

    Object.defineProperty(obj, key, {
      get: function () {
        // 添加訂閱者 watcher 到主題對象 Dep
        if (Dep.target) {
          dep.addSub(Dep.target)
        };

        return val
      },
      set: function (newVal) {
        if (newVal === val) return
        
        val = newVal;

        // 作為釋出者發出通知
        dep.notify();
      }
    });
  }


  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, nodeType) {
    Dep.target = this;
    this.name = name;
    this.node = node;
    this.vm = vm;
    this.nodeType = nodeType;
    this.update();
    Dep.target = null;
  }
  
  Watcher.prototype = {
    update: function () {
      this.get();
      if (this.nodeType == 'text') {
        this.node.nodeValue = this.value;
      }
      if (this.nodeType == 'input') {
        this.node.value = this.value;
      }
    },
    // 擷取 data 中的屬性值
    get: function () {
      this.value = this.vm[this.name]; // 觸發相應屬性的 get
    }
  }


  win.MyVue = MyVue;
})(window)