我也來實現一把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下的全部節點,解析指令和取值操做等,爲每一個節點綁定更新函數(爲何在compile這裏綁定呢?由於這裏恰好是遍歷的節點☺),綁定事件和method的關係,同時也添加訂閱者,當接受到視圖更新的訂閱消息後,調用更新函數,實現視圖更新。同時在添加訂閱者的時候,初始化渲染視圖。java

四、Watcher

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

MVVM的實現

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

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),因此用當即執行函數解決循環項獲取不對的問題。github

MVVM.prototype._observer = function(data) {
  var self = this;
  for(var key in this.$data) {
    if (this.$data.hasOwnProperty(key)) {
      // 初始化屬性對應的訂閱者容器(數組)
      this._binding[key] = {
        _directives: [],
        _texts: []
      };

      if(typeof this.$data[key] === "object") {
        return this._observer(this.$data[key]);
      }
      var val = data[key];
      // 當即執行函數獲取正確的循環項
      (function(value, key) {
        Object.defineProperty(self.$data, key, {
          enumerable: true,
          configurable: true,
          get: function() {
            return value;
          },
          set(newval) {
            if(newval === value) {
              return;
            }
            value = newval;
            // 監聽到數據改變後取出全部該屬性對應的訂閱者,通知view更新-屬性
            if(self._binding[key]._directives) {
              self._binding[key]._directives.forEach(function(watcher) {
                watcher.update();
              }, self);
            }
            // 監聽到數據改變後取出全部該屬性對應的訂閱者,通知view更新-文本
            if(self._binding[key]._texts) {
              self._binding[key]._texts.forEach(function(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;
  // 更新函數,但observer中model的數據改變的時候,經過Watcher的update調用更新函數,從而更新dom
  var updater = null;
  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) {
            updater = 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;
            }
            self._binding[attr]._texts.push(new Watcher(self, attr, updater));
          })(attr);
        }
      }

      // 解析vue指令
      var attributes = node.getAttributeNames();
      for(j = 0; j < attributes.length; j++) {
        // vue指令
        var attribute = attributes[j];
        // DOM attribute
        var domAttr = null;
        // 綁定的data屬性
        var vmDataAttr = node.getAttribute(attribute);
       
        if(/v-bind:([^=]+)/.test(attribute)) {
          // 解析v-bind
          domAttr = RegExp.$1;
          // 更新函數
          updater = function(val) {
            node[domAttr] = val;
          }
          // data屬性綁定多個watcher
          self._binding[vmDataAttr]._directives.push(
            new Watcher(self, vmDataAttr, updater)
          )
        } else if(attribute === "v-model" && (node.tagName = 'INPUT' || node.tagName == 'TEXTAREA')) {
          // 解析v-model
          // 更新函數
          updater = function(val) {
            node.value = val;
          }
          // data屬性綁定多個watcher
          self._binding[vmDataAttr]._directives.push(
            new Watcher(self, vmDataAttr, updater)
          )
          // 監聽input/textarea的數據變化,同步到model去,實現雙向綁定
          node.addEventListener("input", function(evt) {
            var $el = evt.currentTarget;
            self.$data[vmDataAttr] = $el.value;
          });
        } else if(/v-on:([^=]+)/.test(attribute)) {
          // 解析v-on
          var event = RegExp.$1;
          var method = vmDataAttr;
          node.addEventListener(event, function(evt) {
            self.$methods[method] && self.$methods[method].call(self, evt);
          });
        }
      }
    })(node);
  }

}
複製代碼

Watcher的實現

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

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>
複製代碼
相關文章
相關標籤/搜索