我也來實現一把MVVM

轉自我也來實現一把MVVM

原理

你們都知道,vue是個MVVM框架,可以實現view和model的雙向綁定,不像backbone那樣,model改變須要手動去通知view更新,而vue實現的原理就是經過Object.defineProperty實現數據挾持,定義setter,而後數據改變的時候通知視圖更新。
下面是網上vue的實現原理圖: javascript

image

一、MVVM

入口文件,在這裏對vue當中的el、methods、data進行初始化,調用observer遍歷data的數據並進行挾持,調用compile遍歷el下的全部節點,解析之類和取值操做({{}})。遍歷data的數據,經過Object.defineProperty的getter和setter實現對data的代理。html

二、Observer

遍歷data,經過Object.defineProperty設置getter和setter,在setter知道數據發生了改變,而後通知Wacher去更新view。vue

三、Compile

遍歷$el下的全部節點,解析指令和取值操做等,爲每一個節點綁定更新函數(在這裏),綁定事件和method的關係,同時也添加訂閱者,當接受到視圖更新的訂閱消息後,調用更新函數,實現視圖更新。同時在添加訂閱者的時候,初始化渲染視圖。java

四、Watcher

Watcher做爲訂閱者,充當Observer和Compile的中間橋樑,包含update方法,update方法調用Compile中綁定的事件更新函數,實現對視圖的初始化和更新操做。node

MVVM的實現

MVVM完成初始化操做,而且調用observer和compile。對data進行代理,如此即可以經過this.attribute來代理this.data.attribute。由於一個屬性可能對應多個指令,因此須要一個_binding屬性來存放屬性對應的全部訂閱者,這樣屬性一改變,就能夠取出全部的訂閱者去更新視圖。git

github

function MVVM(options) { // 初始化 this.$data = options.data; this.$methods = options.methods; this.$el = options.el; // 保存data的每一個屬性對應的全部watcher this._binding = {}; // 調用observer和compile this._observer(options.data); this._compile(); // this.xxx 代理this.$data.xxx this.proxyAttribute(); } 複製代碼複製代碼複製代碼

Observer的實現

Observer遍歷$data,經過Object.defineProperty的setter的挾持數據改變,監聽到數據改變後取出全部該屬性對應的訂閱者,而後通知更新函數更新視圖。
注意:這裏有循環,且閉包(getter和setter)裏面須要依賴循環項(value和key),因此用當即執行函數解決循環項獲取不對的問題。數組

bash

MVVM.prototype._observer = function(data) { var self = this; for(var key in this.$data) { if (this.$data.hasOwnProperty(key)) { // 初始化屬性對應的訂閱者容器(數組) this._binding[key] = { _directives: [], _texts: [] };
  <span class="hljs-keyword">if</span>(typeof this.<span class="hljs-variable">$data</span>[key] === <span class="hljs-string">"object"</span>) {
    <span class="hljs-built_in">return</span> this._observer(this.<span class="hljs-variable">$data</span>[key]);
  }
  var val = data[key];
  // 當即執行函數獲取正確的循環項
  (<span class="hljs-keyword">function</span>(value, key) {
    Object.defineProperty(self.<span class="hljs-variable">$data</span>, key, {
      enumerable: <span class="hljs-literal">true</span>,
      configurable: <span class="hljs-literal">true</span>,
      get: <span class="hljs-function"><span class="hljs-title">function</span></span>() {
        <span class="hljs-built_in">return</span> value;
      },
      <span class="hljs-built_in">set</span>(newval) {
        <span class="hljs-keyword">if</span>(newval === value) {
          <span class="hljs-built_in">return</span>;
        }
        value = newval;
        // 監聽到數據改變後取出全部該屬性對應的訂閱者,通知view更新-屬性
        <span class="hljs-keyword">if</span>(self._binding[key]._directives) {
          self._binding[key]._directives.forEach(<span class="hljs-keyword">function</span>(watcher) {
            watcher.update();
          }, self);
        }
        // 監聽到數據改變後取出全部該屬性對應的訂閱者,通知view更新-文本
        <span class="hljs-keyword">if</span>(self._binding[key]._texts) {
          self._binding[key]._texts.forEach(<span class="hljs-keyword">function</span>(watcher) {
            watcher.update();
          }, self);
        }
      }
    });
  })(val, key);
}
複製代碼
複製代碼<span class="hljs-keyword">if</span>(typeof this.<span class="hljs-variable">$data</span>[key] === <span class="hljs-string">"object"</span>) { <span class="hljs-built_in">return</span> this._observer(this.<span class="hljs-variable">$data</span>[key]); } var val = data[key]; // 當即執行函數獲取正確的循環項 (<span class="hljs-keyword">function</span>(value, key) { Object.defineProperty(self.<span class="hljs-variable">$data</span>, key, { enumerable: <span class="hljs-literal">true</span>, configurable: <span class="hljs-literal">true</span>, get: <span class="hljs-function"><span class="hljs-title">function</span></span>() { <span class="hljs-built_in">return</span> value; }, <span class="hljs-built_in">set</span>(newval) { <span class="hljs-keyword">if</span>(newval === value) { <span class="hljs-built_in">return</span>; } value = newval; // 監聽到數據改變後取出全部該屬性對應的訂閱者,通知view更新-屬性 <span class="hljs-keyword">if</span>(self._binding[key]._directives) { self._binding[key]._directives.forEach(<span class="hljs-keyword">function</span>(watcher) { watcher.update(); }, self); } // 監聽到數據改變後取出全部該屬性對應的訂閱者,通知view更新-文本 <span class="hljs-keyword">if</span>(self._binding[key]._texts) { self._binding[key]._texts.forEach(<span class="hljs-keyword">function</span>(watcher) { watcher.update(); }, self); } } }); })(val, key); } 複製代碼} } 複製代碼複製代碼

Compile的實現

Compile遍歷全部的節點,解析指令,爲每一個節點綁定更新函數,且添加訂閱者,當訂閱者通知view更新的時候,調用更新函數,實現對視圖的更新。
這裏一樣須要使用當即執行函數來解決閉包依賴的循環項問題。
還有一點須要解決的是,若是節點的innerText依賴多個屬性的話,如何作到只替換改變屬性對應的文本問題。
好比{{message}}:{{name}}已經被編譯解析成「歡迎: 鳴人」,若是message改變爲「你好」,怎麼讓使得「歡迎:鳴人」改成「你好:鳴人」。閉包

MVVM.prototype._compile = function() { var dom = document.querySelector(this.$el); var children = dom.children; var self = this; var i = 0, j = 0; for(; i < children.length; i++) { var node = children[i]; (function(node) { // 解析{{}}裏面的內容 // 保存指令原始內容,否則數據更新時沒法完成替換 var text = node.innerText; var matches = text.match(/{{([^{}]+)}}/g); if(matches && matches.length > 0) { // 保存和node綁定的全部屬性 node.bindingAttributes = []; for(j = 0; j < matches.length; j++) { // data某個屬性 var attr = matches[j].match(/{{([^{}]+)}}/)[1]; // 將和該node綁定的data屬性保存起來 node.bindingAttributes.push(attr); (function(attr) { self._binding[attr]._texts.push(new Watcher(self, attr, function() { // 改變的屬性值對應的文本進行替換 var innerText = text.replace(new RegExp("{{" + attr + "}}", "g"), self.$data[attr]); // 若是該node綁定多個屬性 eg:<div>{{title}}{{description}}</div> for(var k = 0; k < node.bindingAttributes.length; k++) { if(node.bindingAttributes[k] !== attr) { // 恢復原來沒改變的屬性對應的文本 innerText = innerText.replace("{{" + node.bindingAttributes[k] + "}}", self.$data[node.bindingAttributes[k]]); } } node.innerText = innerText; })); })(attr); } }
  // 解析vue指令
  var attributes = node.getAttributeNames();
  <span class="hljs-keyword">for</span>(j = 0; j &lt; attributes.length; j++) {
    // vue指令
    var attribute = attributes[j];
    // DOM attribute
    var domAttr = null;
    // 綁定的data屬性
    var vmDataAttr = node.getAttribute(attribute);
    // 更新函數,但observer中model的數據改變的時候,經過Watcher的update調用更新函數,從而更新dom
    var updater = null;
   
    <span class="hljs-keyword">if</span>(/v-bind:([^=]+)/.test(attribute)) {
      // 解析v-bind
      domAttr = RegExp.<span class="hljs-variable">$1</span>;
      // 更新函數
      updater = <span class="hljs-keyword">function</span>(val) {
        node[domAttr] = val;
      }
      // data屬性綁定多個watcher
      self._binding[vmDataAttr]._directives.push(
        new Watcher(self, vmDataAttr, updater)
      )
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(attribute === <span class="hljs-string">"v-model"</span> &amp;&amp; (node.tagName = <span class="hljs-string">'INPUT'</span> || node.tagName == <span class="hljs-string">'TEXTAREA'</span>)) {
      // 解析v-model
      // 更新函數
      updater = <span class="hljs-keyword">function</span>(val) {
        node.value = val;
      }
      // data屬性綁定多個watcher
      self._binding[vmDataAttr]._directives.push(
        new Watcher(self, vmDataAttr, updater)
      )
      // 監聽input/textarea的數據變化,同步到model去,實現雙向綁定
      node.addEventListener(<span class="hljs-string">"input"</span>, <span class="hljs-keyword">function</span>(evt) {
        var <span class="hljs-variable">$el</span> = evt.currentTarget;
        self.<span class="hljs-variable">$data</span>[vmDataAttr] = <span class="hljs-variable">$el</span>.value;
      });
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(/v-on:([^=]+)/.test(attribute)) {
      // 解析v-on
      var event = RegExp.<span class="hljs-variable">$1</span>;
      var method = vmDataAttr;
      node.addEventListener(event, <span class="hljs-keyword">function</span>(evt) {
        self.<span class="hljs-variable">$methods</span>[method] &amp;&amp; self.<span class="hljs-variable">$methods</span>[method].call(self, evt);
      });
    }
  }
})(node);
複製代碼

}

複製代碼// 解析vue指令 var attributes = node.getAttributeNames(); <span class="hljs-keyword">for</span>(j = 0; j &lt; attributes.length; j++) { // vue指令 var attribute = attributes[j]; // DOM attribute var domAttr = null; // 綁定的data屬性 var vmDataAttr = node.getAttribute(attribute); // 更新函數,但observer中model的數據改變的時候,經過Watcher的update調用更新函數,從而更新dom var updater = null; <span class="hljs-keyword">if</span>(/v-bind:([^=]+)/.test(attribute)) { // 解析v-bind domAttr = RegExp.<span class="hljs-variable">$1</span>; // 更新函數 updater = <span class="hljs-keyword">function</span>(val) { node[domAttr] = val; } // data屬性綁定多個watcher self._binding[vmDataAttr]._directives.push( new Watcher(self, vmDataAttr, updater) ) } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(attribute === <span class="hljs-string">"v-model"</span> &amp;&amp; (node.tagName = <span class="hljs-string">'INPUT'</span> || node.tagName == <span class="hljs-string">'TEXTAREA'</span>)) { // 解析v-model // 更新函數 updater = <span class="hljs-keyword">function</span>(val) { node.value = val; } // data屬性綁定多個watcher self._binding[vmDataAttr]._directives.push( new Watcher(self, vmDataAttr, updater) ) // 監聽input/textarea的數據變化,同步到model去,實現雙向綁定 node.addEventListener(<span class="hljs-string">"input"</span>, <span class="hljs-keyword">function</span>(evt) { var <span class="hljs-variable">$el</span> = evt.currentTarget; self.<span class="hljs-variable">$data</span>[vmDataAttr] = <span class="hljs-variable">$el</span>.value; }); } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(/v-on:([^=]+)/.test(attribute)) { // 解析v-on var event = RegExp.<span class="hljs-variable">$1</span>; var method = vmDataAttr; node.addEventListener(event, <span class="hljs-keyword">function</span>(evt) { self.<span class="hljs-variable">$methods</span>[method] &amp;&amp; self.<span class="hljs-variable">$methods</span>[method].call(self, evt); }); } } })(node); 複製代碼} 複製代碼複製代碼

Wathcer的實現

Watcher充當訂閱者的角色,架起了Observer和Compile的橋樑,Observer監聽到數據變化後,通知Wathcer更新視圖(調用Wathcer的update方法),Watcher再告訴Compile去調用更新函數,實現dom的更新。同時頁面的初始化渲染也交給了Watcher(固然也能夠放到Compile進行)。

function Watcher(vm, attr, cb) { this.vm = vm; // viewmodel this.attr = attr; // data的屬性,一個watcher訂閱一個data屬性 this.cb = cb; // 更新函數,在compile那邊定義 // 初始化渲染視圖 this.update(); } 複製代碼Watcher.prototype.update = function() { // 通知comile中的更新函數更新dom this.cb(this.vm.$data[this.attr]); } 複製代碼複製代碼

所有代碼

git地址:github.com/VikiLee/MVV…

鳴謝:juejin.im/post/5acc17…

使用例子

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="view"> <div v-bind:id="id"> {{message}}:{{name}} </div> <input type="text" v-model="name"/> <button v-on:click="handleClick">獲取輸入值</button> </div> </body> <script src="js/MVVM.js" type="text/javascript"></script> <script> var vue = new MVVM({ el: "#view", data: { message: "歡迎光臨", name: "鳴人", id: "id" }, methods: { handleClick: function() { alert(this.message + ":" + this.name + ", 點擊肯定路飛會出來"); this.name = '路飛'; } } }) 複製代碼setTimeout(function() { vue.message = "你好"; }, 1000); </script> </html> 複製代碼複製代碼
相關文章
相關標籤/搜索