vue學習之響應式原理的demo實現

Vue.js 核心:
一、響應式的數據綁定系統
二、組件系統。javascript

訪問器屬性html

訪問器屬性是對象中的一種特殊屬性,它不能直接在對象中設置,而必須經過 defineProperty() 方法單獨定義。vue

var obj = { };

       // 爲obj定義一個名爲 hello 的訪問器屬性

       Object.defineProperty(obj, "hello", {

         get: function () {return sth},

         set: function (val) {/* do sth */}

       })

       obj.hello // 能夠像普通屬性同樣讀取訪問器屬性

訪問器屬性的"值"比較特殊,讀取或設置訪問器屬性的值,其實是調用其內部特性:get和set函數。java

obj.hello // 讀取屬性,就是調用get函數並返回get函數的返回值

       obj.hello = "abc" // 爲屬性賦值,就是調用set函數,賦值實際上是傳參

get 和 set 方法內部的 this 都指向 obj,這意味着 get 和 set 函數能夠操做對象內部的值。另外,訪問器屬性的會"覆蓋"同名的普通屬性,由於訪問器屬性會被優先訪問,與其同名的普通屬性則會被忽略。node

預期達到的效果:
一、隨文本框輸入文字的變化,span 中會同步顯示相同的文字內容;
二、在js或控制檯顯式的修改 obj.hello 的值,視圖會相應更新。這樣就實現了 model => view 以及 view => model 的雙向綁定。react

模型圖:
app

子任務:dom

一、輸入框以及文本節點與 data 中的數據綁定
二、輸入框內容變化時,data 中的數據同步變化。即 view => model 的變化。
三、data 中的數據變化時,文本節點的內容同步變化。即 model => view 的變化。異步

一、輸入框以及文本節點與 data 中的數據綁定

這裏須要對 DOM 進行編譯,這裏引入一個知識點:DocumentFragment。函數

DocumentFragment

DocumentFragment(文檔片斷)能夠看做節點容器,它能夠包含多個子節點,當咱們將它插入到 DOM 中時,只有它的子節點會插入目標節點,因此把它看做一組節點的容器。使用 DocumentFragment 處理節點,速度和性能遠遠優於直接操做 DOM。Vue 進行編譯時,就是將掛載目標的全部子節點劫持(真的是劫持,經過 append 方法,DOM 中的節點會被自動刪除)到 DocumentFragment 中,通過一番處理後,再將 DocumentFragment 總體返回插入掛載目標。

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

<script type="text/javascript">
    var dom = nodeToFragment(document.getElementById('app'));
    console.log(dom);

    function nodeToFragment (node) {
        var flag = document.createDocumentFragment();
        var child;
        while (child = node.firstChild) {
            flag.append(child);  // 劫持node的全部子節點
        }
        return flag;
    }

    document.getElementById('app').appendChild(dom);  // 返回到app中
</script>
</body>
</html>

數據初始化綁定

function compile (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.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];
        }
    }
}

function nodeToFragment (node, vm) {
    var flag = document.createDocumentFragment();
    var child;

    while (child  = node.firstChild) {
        compile(child, vm);
        flag.append(child);
    }
    return flag;
}


function Vue (options) {
    this.data = options.data;
    var id = options.el;
    var dom = nodeToFragment(document.getElementById(id), this);

    // 編譯完成後,將dom返回到app中
    document.getElementById(id).appendChild(dom);
}

var vm = new Vue({
    el: 'app',
    data: {
        text: 'hello world'
    }
})

效果

響應式的數據綁定

二、輸入框內容變化時,data 中的數據同步變化。即 view => model 的變化。

思路:當咱們在輸入框輸入數據的時候,首先觸發 input 事件(或者 keyup、change 事件),在相應的事件處理程序中,咱們獲取輸入框的 value 並賦值給 vm 實例的 text 屬性。咱們利用 defineProperty 將 data 中的 text 設置爲 vm 的訪問器屬性,所以給 vm.text 賦值,就會觸發 set 方法。這裏set主要作了跟新屬性值得操做。

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

    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 = nodeToFragment(document.getElementById(id), this);

      // 編譯完成後,將dom返回到app中
      document.getElementById(id).appendChild(dom); 
    }

    function compile (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');
          }
        };

        new Watcher(vm, node, name, 'input');
      }
      // 節點類型爲text
      if (node.nodeType === 3) {
        if (reg.test(node.nodeValue)) {
          var name = RegExp.$1; // 獲取匹配到的字符串
          name = name.trim();
          node.nodeValue = vm[name];
        }
      }
    }

第二部完成效果

訂閱/發佈模式(subscribe&publish)

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

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

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

var pub = {
        publish: function() {
            def.notify():
        }
    }
    // 三個訂閱者subscribers
    var sub1 = {updata: function () {console.log(1)} }
    var sub2 = {updata: function () {console.log(2)} }
    var sub3 = {updata: function () {console.log(3)} }

    // 一個主題對象
    funciton Dep () {
        this.subs = [sub1, sub2, sub3];
    }
    Dep.prototype.notify = function () {
        this.subs.forEach(function (sub) {
            sub.update();
        })
    }
    // 發佈者發佈消息,主題對象執行notify方法,而後會觸發訂閱者實現更函數
    var dep = new Dep();
    pub.publish();  // 1, 2, 3

set在這裏的做用是:做爲發佈者發出通知,而文本節點在這裏是訂閱者,收到消息以後執行相應的更新操做。

雙向綁定的實現

每當 new 一個 Vue,主要作了兩件事:
1.是監聽數據:observe(data),
2.第二個是編譯 HTML:nodeToFragement(id)。

在監聽數據的過程當中,會爲 data 中的每個屬性生成一個主題對象 dep。

在編譯 HTML 的過程當中,會爲每一個與數據綁定相關的節點生成一個訂閱者 watcher,watcher 會將本身添加到相應屬性的 dep 中。

如今效果:修改輸入框內容 => 在事件回調函數中修改屬性值 => 觸發屬性的 set 方法。

下一步實現:發出通知 dep.notify() => 觸發訂閱者的 update 方法 => 更新視圖。

關鍵邏輯:如何將 watcher 添加到關聯屬性的 dep 中。

在編譯 HTML 過程當中,爲每一個與 data 關聯的節點生成一個 Watcher。
Watcher函數實現思路:

function Watcher (vm, node, name, nodeType) {
      Dep.target = this;
      this.name = name;
      this.node = node;
      this.vm = vm;
      this.update();
      Dep.target = null;
    }

    Watcher.prototype = {
      update: function () {
        this.get();
        this.node.nodeValue = this.value;
      },
      // 獲取data中的屬性值
      get: function () {
        this.value = this.vm[this.name]; // 觸發相應屬性的get
      }
    }

首先,將本身賦給了一個全局變量 Dep.target;

其次,執行了 update 方法,進而執行了 get 方法,get 的方法讀取了 vm 的訪問器屬性,從而觸發了訪問器屬性的 get 方法,get 方法中將該 watcher 添加到了對應訪問器屬性的 dep 中;

再次,獲取屬性的值,而後更新視圖。

最後,將 Dep.target 設爲空。由於它是全局變量,也是 watcher 與 dep 關聯的惟一橋樑,任什麼時候刻都必須保證 Dep.target 只有一個值。

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

最終效果:
文本內容會隨輸入框內容同步變化,在控制器中修改 vm.text 的值,會同步反映到文本內容中。

感悟

1.異步更新帶來的數據響應式誤解

<div id="app">
        <h2>{{dataObj.text}}</h2>
</div>



new Vue({
            el: '#app',
            data: {
                dataObj: {}
            },
            ready: function () {
                var self = this;

                /**
                 * 異步請求模擬
                 */
                setTimeout(function () {
                    self.dataObj = {}; 
                    self.dataObj['text'] = 'new text';
                }, 3000);
            }
        })

上面的代碼很是簡單,咱們都知道vue中在data裏面聲明的數據才具備響應式的特性,因此咱們一開始在data中聲明瞭一個dataObj空對象,而後在異步請求中執行了兩行代碼,以下:

self.dataObj = {}; 
self.dataObj['text'] = 'new text';

模板更新了,應該具備響應式特性,若是這麼想那麼你就已經走入了誤區,一開始咱們並無在data中聲明.text屬性,因此該屬性是不具備響應式的特性的。

但模板切切實實已經更新了,這又是怎麼回事呢?

那是由於vue的dom更新是異步的,即當setter操做發生後,指令並不會立馬更新,指令的更新操做會有一個延遲,當指令更新真正執行的時候,此時.text屬性已經賦值,因此指令更新模板時獲得的是新值。

具體流程以下所示:

self.dataObj = {};發生setter操做
vue監測到setter操做,通知相關指令執行更新操做
self.dataObj['text'] = 'new text';賦值語句
指令更新開始執行

因此真正的觸發更新操做是self.dataObj = {};這一句引發的,因此單看上述例子,具備響應式特性的數據只有dataObj這一層,它的子屬性是不具有的。

2.Vue 不容許在已經建立的實例上動態添加新的根級響應式屬性(root-level reactive property)。然而它可使用Vue.set(object, key, value) 方法將響應屬性添加到嵌套的對象上或者使用$set

相關文章
相關標籤/搜索