vue雙向綁定的原理及實現雙向綁定MVVM源碼分析

vue雙向綁定的原理及實現雙向綁定MVVM源碼分析html

    雙向數據綁定的原理是:能夠將對象的屬性綁定到UI,具體的說,咱們有一個對象,該對象有一個name屬性,當咱們給這個對象name屬性賦新值的時候,新值在UI上也會獲得更新。一樣的道理,當咱們有一個輸入框或者textarea的時候,咱們輸入一個新值的時候,也會在該對象的name屬性獲得更新。vue

實現數據綁定的作法有以下幾種:
1. 發佈者-訂閱模式(http://www.cnblogs.com/tugenhua0707/p/7471381.html)
2. 髒值檢查(angular.js)
3. 數據劫持(vue.js)node

髒值檢查:是經過髒值檢測的方式比對數據是否有變動,來決定是否更新視圖,最簡單的方式就是經過 setInterval()定時輪詢檢測數據的變更。git

數據劫持:vue.js 則是採用數據劫持結合發佈者-訂閱者模式,經過Object.defineProperty()來劫持各個屬性的setter,getter。在數據變更時發佈消息給訂閱者,觸發響應的監聽回調。github

下面是一個經過 Object.defineProperty 來實現一個簡單的數據雙向綁定。經過該方法來監聽屬性的變化。
實現的效果簡單以下:頁面上有一個input輸入框和div顯示框,當在input輸入框輸入值的時候,div也會顯示對應的值,當我打開控制檯改變 obj.name="輸入任意值"的時候,按回車鍵運行下,input輸入框的值也會跟着變,能夠簡單的理解爲 模型-> 視圖的 改變,以及 視圖 -> 模型的改變。以下代碼:web

<!DOCTYPE html>
 <html>
  <head>
    <meta charset="utf-8">
    <meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" name="viewport">
    <meta content="yes" name="apple-mobile-web-app-capable">
    <meta content="black" name="apple-mobile-web-app-status-bar-style">
    <meta content="telephone=no" name="format-detection">
    <meta content="email=no" name="format-detection">
    <title>標題</title>
    <link rel="shortcut icon" href="/favicon.ico">
  </head>
  <body>
    <h3>使用Object.defineProperty實現簡單的雙向數據綁定</h3>
    <input type="text" id="input" />
    <div id="div"></div>
    <script>
        var obj = {};
        var inputVal = document.getElementById("input");
        var div = document.getElementById("div");

        Object.defineProperty(obj, "name", {
          set: function(newVal) {
            inputVal.value = newVal;
            div.innerHTML = newVal;
          }
        });
        inputVal.addEventListener('input', function(e){
          obj.name = e.target.value;
        });
    </script>
  </body>
</html>

查看效果數組

vue是經過數據劫持的方式來作數據綁定的,最核心的方法是經過 Object.defineProperty()方法來實現對屬性的劫持,達到能監聽到數據的變更。要實現數據的雙向綁定,須要實現以下幾點
1. 須要實現一個數據監聽器Observer,可以對數據對象的全部屬性進行監聽,若有變更拿到最新值並通知訂閱者。
2. 須要實現一個指令解析器Compile, 對每一個元素節點的指令進行掃描和解析,根據指令模板替換數據,以及綁定相應的更新函數。
3. 須要實現一個Watcher,做爲連接Observer和Compile的橋樑,可以訂閱並收到每一個屬性變更的通知,執行指令綁定的相應的回調函數,從而更新視圖。app

一: 實現Observer
咱們可使用Object.defineProperty()來監聽屬性的變更,咱們須要對屬性對象進行遞歸遍歷,包括子屬性對象的屬性。再加上getter和setter方法,咱們能夠和上面的demo同樣,監聽input值的變化,即 視圖 -> 模型。且當咱們給某個對象的屬性賦值的話,會自動調用setter方法,來動態修改數據,即 模型 -> 視圖。
引入咱們可使用 object.defineProperty來實現監聽屬性的變更,以下代碼:dom

<!DOCTYPE html>
 <html>
  <head>
    <meta charset="utf-8">
    <meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" name="viewport">
    <meta content="yes" name="apple-mobile-web-app-capable">
    <meta content="black" name="apple-mobile-web-app-status-bar-style">
    <meta content="telephone=no" name="format-detection">
    <meta content="email=no" name="format-detection">
    <title>標題</title>
    <link rel="shortcut icon" href="/favicon.ico">
  </head>
  <body>
    <script>
      function Observer(data) {
        this.data = data;
        this.init();
      }
      Observer.prototype = {
        init: function() {
          var self = this;
          var data = self.data;
          // 遍歷data對象
          Object.keys(data).forEach(function(key) {
            self.defineReactive(data, key, data[key]);
          });
        },
        defineReactive: function(data, key, value) {

          // 遞歸遍歷子對象
          var childObj = observer(value);

          // 對對象的屬性使用Object.defineProperty進行監聽
          Object.defineProperty(data, key, {
            enumerable: true,  // 可枚舉
            configurable: false, // 不能刪除目標屬性或不能修改目標屬性
            get: function() {
              return value;
            },
            set: function(newVal) {
              if (newVal === value) {
                return;
              }
              console.log('已經監聽到值的變化了', value, '==>', newVal);
              value = newVal;
            }
          });
        }
      }

      function observer(value) {
        if (!value || typeof value !== 'object') {
          return;
        }
        return new Observer(value);
      }

      // 測試demo
      var data = {name: 'kongzhi'};
      observer(data);
      data.name = 'kongzhi2';  // 控制檯打印出  已經監聽到值的變化了,kongzhi ==> kongzhi2
    </script>
  </body>
</html>

如上代碼咱們能夠監聽每一個屬性對象數據的變化了,那麼監聽到屬性值變化後咱們須要把消息通知到訂閱者,所以咱們須要實現一個消息訂閱器,該訂閱器的做用是收集全部的訂閱者,當有屬性值發生改變的時候,就把該消息通知給全部訂閱者。所以咱們能夠實現以下代碼:mvvm

// 對全部的屬性數據進行監聽
function Observer(data) {
  this.data = data;
  this.init();
}

Observer.prototype = {
  init: function() {
    var self = this;
    var data = self.data;
    // 遍歷data對象
    Object.keys(data).forEach(function(key) {
      self.defineReactive(data, key, data[key]);
    });
  },
  defineReactive: function(data, key, value) {
    var dep = new Dep();
    // 遞歸遍歷子對象
    var childObj = observer(value);

    // 對對象的屬性使用Object.defineProperty進行監聽
    Object.defineProperty(data, key, {
      enumerable: true,  // 可枚舉
      configurable: false, // 不能刪除目標屬性或不能修改目標屬性
      get: function() {
        if (Dep.target) {
          dep.depend();
        }
        return value;
      },
      set: function(newVal) {
        if (newVal === value) {
          return;
        }
        value = newVal;
        // 若是新值是對象的話,遞歸該對象 進行監聽
        childObj = observer(newVal);
        // 通知訂閱者 
        dep.notify();
      }
    });
  }
}

function observer(value) {
  if (!value || typeof value !== 'object') {
    return;
  }
  return new Observer(value);
}

function Dep() {
  this.subs = [];
}

Dep.prototype = {
  addSub: function(sub) {
    this.subs.push(sub);
  },
  depend: function() {
    Dep.target.addDep(this);
  },
  removeSub: function(sub) {
    var index = this.subs.indexOf(sub);
    if (index != -1) {
      this.subs.splice(index, 1);
    }
  },
  notify: function() {
    // 遍歷全部的訂閱者 通知全部的訂閱者
    this.subs.forEach(function(sub) {
      sub.update();
    })
  }
};

Dep.target = null;

從上面的分析獲得,須要實現一個Watcher,做爲連接Observer和Compile的橋樑,可以訂閱並收到每一個屬性變更的通知,因此訂閱者就是Watcher,var dep = new Dep();是在 defineReactive方法內部定義的,是想經過dep添加訂閱者,經過Dep定義一個全局的target屬性,暫存watcher,添加完後會移除,所以最後一句代碼設置 Dep.target = null;
執行getter方法會返回值,執行setter方法會判斷新舊值是否相等,若是不相等的話,再次遞歸遍歷設置的對象的全部子對象,而後會通知全部的訂閱者。

二: 實現 Compile

compile作的事情是解析模板指令,將模板中的變量替換成數據,而後初始化渲染頁面視圖,並將每一個指令對應的節點綁定更新函數。

以下圖所示:

// 具體看以下代碼,代碼中有對應的註釋

/*
 * @param {el} 元素節點容器 如:el: '#mvvm-app'
 * @param {vm} Object 對象傳遞數據 
*/
function Compile(el, vm) {
  this.$vm = vm;
  // 判斷是元素節點 仍是 選擇器
  this.$el = this.isElementNode(el) ? el : document.querySelector(el);
  if (this.$el) {
    /*
     由於遍歷解析過程有屢次操做dom節點,爲了提升性能和效率,會先將根節點el轉換成文檔碎片fragment進行解析編譯操做,
     解析完成後,再將fragment添加回原來真實的dom節點中。有關 DocumentFragment 請看 http://www.cnblogs.com/tugenhua0707/p/7465915.html
     */
    this.$fragment = this.node2Fragment(this.$el);
    this.init();
    this.$el.appendChild(this.$fragment);
  }
}

Compile.prototype = {
  node2Fragment: function(el) {
    var fragment = document.createDocumentFragment(),
      child;
    // 將原生節點拷貝到fragment中
    while (child = el.firstChild) {
      fragment.appendChild(child);
    }
    return fragment;
  },
  init: function() {
    // 進行解析編譯操做
    this.compileElement(this.$fragment);
  },
  compileElement: function(el) {
    var childNodes = el.childNodes;
    // 遍歷全部的子節點 判斷子節點是 元素節點仍是文本節點 分別進行編譯解析操做
    [].slice.call(childNodes).forEach(function(node) {

      // 獲取節點的文本內容和它的全部後代
      var text = node.textContent;

      // 正則匹配 {{xx}} 這樣的xx文本值
      var reg = /\{\{(.*)\}\}/;
      // 判斷是不是元素節點,而後進行編譯
      if (this.isElementNode(node)) {
        this.compile(node);

      } else if(this.isTextNode(node) && reg.test(text)) {
        // 判斷node是不是文本節點 且 符合正則匹配的 {{xx}} 那麼就會進行編譯解析
        this.compileText(node, RegExp.$1);
      }

      // 若是該節點還有子節點的話,那麼遞歸進行判斷編譯
      if (node.childNodes && node.childNodes.length) {
        this.compileElement(node);
      }
    });
  },
  // 元素節點編譯
  compile: function(node) {
    // 獲取節點的全部屬性
    var nodeAttrs = node.attributes;
    var self = this;
    /*
     遍歷該節點的全部屬性,判斷該屬性是事件指令仍是普通指令
     */
    [].slice.call(nodeAttrs).forEach(function(attr) {
      // 獲取屬性名
      var attrName = attr.name;
      // 先判斷屬性名是不是 以 v- 開頭的
      if (self.isDirective(attrName)) {
        var attrValue = attr.value;
        // 獲取 v-xx 中從xx開始的全部字符串
        var dir = attrName.substring(2);
        // 判斷是不是事件指令
        if (self.isEventDirective(dir)) {
          compileUtil.eventHandler(node, self.$vm, attrValue, dir);
        } else {
          // 普通指令
          compileUtil[dir] && compileUtil[dir](node, self.$vm, attrValue);
        }
        // 循環完成一次後 刪除該屬性
        node.removeAttribute(attrName);
      }
    });
  },
  // 編譯文本
  compileText: function(node, exp) {
    compileUtil.text(node, this.$vm, exp);
  },
  // 是不是v- 開始的指令
  isDirective: function(attrName) {
    return attrName.indexOf('v-') === 0;
  },
  /*
   * 是不是事件指令 事件指令以 v-on開頭的
   */
   isEventDirective: function(dir) {
     return dir.indexOf('on') === 0;
   },
   // 是不是元素節點
   isElementNode: function(node) {
     return node.nodeType === 1;
   },
   // 是不是文本節點
   isTextNode: function(node) {
     return node.nodeType === 3;
   }
};

// 指令處理
var compileUtil = {
  text: function(node, vm, exp) {
    this.bind(node, vm, exp, 'text');
  },
  html: function(node, vm, exp) {
    this.bind(node, vm, exp, 'html');
  },
  /*
   * 普通指令 v-model 開頭的,調用model方法
   * @param {node} 容器節點
   * @param {vm} 數據對象
   * @param {exp} 普通指令的值 好比 v-model="xx" 那麼exp就等於xx
   */
  model: function(node, vm, exp) {
    this.bind(node, vm, exp, 'model');
    var self = this;
    var val = this.getVMVal(vm, exp);
    // 監聽input的事件
    node.addEventListener('input', function(e) {
      var newValue = e.target.value;
      // 比較新舊值是否相同
      if (val === newValue) {
        return;
      }
      // 從新設置新值
      self.setVMVal(vm, exp, newValue);
      val = newValue;
    });
  },
  /*
   *  返回 v-mdoel = "xx" 中的xx的值, 好比data對象會定義以下:
    data: {
      "xx" : "111"
    }
    * @param {vm} 數據對象
    * @param {exp} 普通指令的值 好比 v-model="xx" 那麼exp就等於xx
   */
  getVMVal: function(vm, exp) {
    var val = vm;
    exp = exp.split('.');
    exp.forEach(function(k) {
      val = val[k];
    });
    return val;
  },
  /*
    設置普通指令的值
    @param {vm} 數據對象
    @param {exp} 普通指令的值 好比 v-model="xx" 那麼exp就等於xx
    @param {value} 新值
   */
  setVMVal: function(vm, exp, value) {
    var val = vm;
    exp = exp.split('.');
    exp.forEach(function(key, index) {
      // 若是不是最後一個元素的話,更新值
      /*
       數據對象 data 以下數據
       data: {
        child: {
          someStr: 'World !'
        }
      },
       若是 v-model="child.someStr" 那麼 exp = ["child", "someStr"], 遍歷該數組,
       val = val["child"]; val 先儲存該對象,而後再繼續遍歷 someStr,會執行else語句,所以val['someStr'] = value, 就會更新到對象的值了。
       */
      if (i < exp.length - 1) {
        val = val[key];
      } else {
        val[key] = value;
      }
    });
  },
  class: function(node, vm, exp) {
    this.bind(node, vm, exp, 'class');
  },
  /* 
   事件處理
   @param {node} 元素節點
   @param {vm} 數據對象
   @param {attrValue} attrValue 屬性值
   @param {dir} 事件指令的值 好比 v-on:click="xx" 那麼dir就等 on:click
   */
  eventHandler: function(node, vm, attrValue, dir) {
    // 獲取事件類型 好比dir=on:click 所以 eventType="click" 
    var eventType = dir.split(':')[1];
    /*
     * 獲取事件的函數 好比 v-on:click="clickBtn" 那麼就會對應vm裏面的clickBtn 函數。
     * 好比 methods: {
        clickBtn: function(e) {
          console.log(11)
        }
      }
     */
    var fn = vm.$options.methods && vm.$options.methods[attrValue];
    if (eventType && fn) {
      // 若是有事件類型和函數的話,就綁定該事件
      node.addEventListener(eventType, fn.bind(vm), false);
    }
  },
  /*
   @param {node} 節點
   @param {vm} 數據對象
   @param {exp} 正則匹配的值
   @param {dir} 字符串類型 好比 'text', 'html' 'model'
   */
  bind: function(node, vm, exp, dir) {
    // 獲取updater 對象內的對應的函數
    var updaterFn = updater[dir + 'Updater'];

    // 若有該函數的話就執行該函數 參數node爲節點,第二個參數爲 指令的值。 好比 v-model = 'xx' 那麼返回的就是xx的值
    updaterFn && updaterFn(node, this.getVMVal(vm, exp));

    // 調用訂閱者watcher
    new Watcher(vm, exp, function(newValue, oldValue) {
      updaterFn && updaterFn(node, newValue, oldValue);
    });
  }
};

var updater = {
  textUpdater: function(node, value) {
    node.textContent = typeof value === 'undefined' ? '' : value;
  },
  htmlUpdater: function(node, value) {
    node.innerHTML = typeof value === 'undefined' ? '' : value;
  },
  classUpdater: function(node, newValue, oldValue) {
    var className = node.className;
    className = className.replace(oldValue, '').replace(/\s$/, '');
    var space = className && String(newValue) ? ' ' : '';
    node.className = className + space + newValue;
  },
  modelUpdater: function(node, newValue) {
    node.value = typeof newValue === 'undefined' ? '' : newValue;
  }
};

三: 實現Watcher
Watcher是訂閱者是做爲 Observer和Compile之間通訊的橋樑。
相關代碼以下:

/*
 * @param {vm} 數據對象
 * @param {expOrFn} 屬性值 好比 v-model='xx' v-on:click='yy' v-text="tt" 中的 xx, yy, tt 
 * @param {cb}  回調函數
 */
function Watcher(vm, expOrFn, cb) {
  this.vm = vm;
  this.expOrFn = expOrFn;
  this.cb = cb;
  this.depIds = {};

  // expOrFn 是事件函數的話
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = this.parseGetter(expOrFn);
  }
  // 爲了觸發屬性getter,從而在dep添加本身做爲訂閱者
  this.value = this.get();
}

Watcher.prototype = {
  update: function() {
    this.run();    // observer 中的屬性值發生變化 收到通知
  },
  run: function() {
    var value = this.get();
    var oldValue = this.value;
    // 新舊值不相等的話
    if (value !== oldValue) {
      // 把當前的值賦值給 this.value 更新this.value的值
      this.value = value;
      this.cb.call(this.vm, value, oldValue);  // 執行Compile中綁定的回調 更新視圖
    }
  },
  get: function() {
    Dep.target = this; // 將當前訂閱者指向本身
    var value = this.getter.call(this.vm, this.vm); // 觸發getter,添加本身到屬性訂閱器中
    Dep.target = null;  // 添加完成後 清空數據
    return value;
  },
  parseGetter: function(exp) {
    var reg = /[^\w.$]/;
    if (reg.test(exp)) {
      return;
    }
    var exps = exp.split('.');
    return function(obj) {
      for(var i = 0, len = exps.length; i < len; i++) {
        if (!obj) {
          return;
        }
        obj = obj[exps[i]];
      }
      return obj;
    }
  }
}

四: 實現MVVM
MVVM是數據綁定的入口,整合 Observer, Compile, 和 Watcher,經過Observer來監聽model數據變化,經過Compile來解析編譯模板指令,最後使用watcher搭起Observer和Compile之間的通訊橋樑。 達到數據變化 --》視圖更新,視圖變化 --》 數據model更新的 雙向綁定效果。
代碼以下:

function MVVM(options) {
  this.$options = options || {};
  var data = this._data = this.$options.data;
  var self = this;

  /*
   數據代理,實現 vm.xxx 
   上面的代碼看出 監聽數據的對象是 options.data, 所以每次更新視圖的時候;如:
   var vm = new MVVM({
     data: {name: 'kongzhi'}
   });
   那麼更新數據就變成 vm._data.name = 'kongzhi2'; 可是咱們想實現這樣更改 vm.name = "kongzhi2";
   所以這邊須要使用屬性代理方式,利用Object.defineProperty()方法來劫持vm實列對象屬性的讀寫權,使讀寫vm實列的屬性轉成vm.name的屬性值。
   */
   Object.keys(data).forEach(function(key) {
     self._proxyData(key);
   });

   this._initComputed();

   // 初始化Observer
   observer(data, this);

   // 初始化 Compile
   this.$compile = new Compile(options.el || document.body, this);
}

MVVM.prototype = {
  $watch: function(key, cb, options) {
    new Watcher(this, key, cb);
  },

  _proxyData: function(key) {
    var self = this;
    Object.defineProperty(self, key, {
      configurable: false,  // 是否能夠刪除或修改目標屬性
      enumerable: true,   // 是否可枚舉
      get: function proxyGetter() {
        return self._data[key];
      },
      set: function proxySetter(newVal) {
        self._data[key] = newVal;
      }
    })
  },

  _initComputed: function() {
    var self = this;
    var computed = this.$options.computed;
    if (typeof computed === 'object') {
      Object.keys(computed).forEach(function(key) {
        Object.defineProperty(self, key, {
          get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
          set: function() {}
        })
      })
    }
  }
}

html代碼初始化以下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MVVM</title>
</head>
<body>

<div id="mvvm-app">
  <input type="text" v-model="someStr">
  <input type="text" v-model="child.someStr">
  <p>{{getHelloWord}}</p>
  <p v-html="child.htmlStr"></p>
  <button v-on:click="clickBtn">change model</button>
</div>
<script src="./observer.js"></script>
<script src="./watcher.js"></script>
<script src="./compile.js"></script>
<script src="./mvvm.js"></script>
<script>
    var vm = new MVVM({
      el: '#mvvm-app',
      data: {
        someStr: 'hello ',
        className: 'btn',
        htmlStr: '<span style="color: #f00;">red</span>',
        child: {
          someStr: 'World !'
        }
      },
      computed: {
        getHelloWord: function() {
          return this.someStr + this.child.someStr;
        }
      },
      methods: {
        clickBtn: function(e) {
          var randomStrArr = ['childOne', 'childTwo', 'childThree'];
          this.child.someStr = randomStrArr[parseInt(Math.random() * 3)];
        }
      }
    });
    vm.$watch('child.someStr', function() {
        console.log(arguments);
    });
</script>
</body>
</html>

代碼分析:

1. 代碼初始化執行

首先html代碼以下:

<div id="mvvm-app">
  <input type="text" v-model="someStr">
  <input type="text" v-model="child.someStr">
  <p>{{getHelloWord}}</p>
  <p v-html="child.htmlStr"></p>
  <button v-on:click="clickBtn">change model</button>
</div>

數據調用以下:

var vm = new MVVM({
  el: '#mvvm-app',
  data: {
    someStr: 'hello ',
    className: 'btn',
    htmlStr: '<span style="color: #f00;">red</span>',
    child: {
      someStr: 'World !'
    }
  },
  computed: {
    getHelloWord: function() {
      return this.someStr + this.child.someStr;
    }
  },
  methods: {
    clickBtn: function(e) {
      var randomStrArr = ['childOne', 'childTwo', 'childThree'];
      this.child.someStr = randomStrArr[parseInt(Math.random() * 3)];
    }
  }
});
vm.$watch('child.someStr', function() {
  console.log(arguments);
});

執行 new MVVM 後 mvvm.js 實列化,mvvm.js 代碼以下:

function MVVM(options) {
  this.$options = options || {};
  var data = this._data = this.$options.data;
  var self = this;
  /*
   數據代理,實現 vm.xxx 
   上面的代碼看出 監聽數據的對象是 options.data, 所以每次更新視圖的時候;如:
   var vm = new MVVM({
     data: {name: 'kongzhi'}
   });
   那麼更新數據就變成 vm._data.name = 'kongzhi2'; 可是咱們想實現這樣更改 vm.name = "kongzhi2";
   所以這邊須要使用屬性代理方式,利用Object.defineProperty()方法來劫持vm實列對象屬性的讀寫權,使讀寫vm實列的屬性轉成vm.name的屬性值。
   */
   Object.keys(data).forEach(function(key) {
     self._proxyData(key);
   });

   this._initComputed();

   // 初始化Observer
   observer(data, this);

   // 初始化 Compile
   this.$compile = new Compile(options.el || document.body, this);
}

所以 參數options值爲對象(Object):

this.$options = {
  el: '#mvvm-app',
  data: {
    someStr: 'hello ',
    className: 'btn',
    htmlStr: '<span style="color: #f00;">red</span>',
    child: {
      someStr: 'World !'
    }
  },
  computed: {
    getHelloWord: function() {
      return this.someStr + this.child.someStr;
    }
  },
  methods: {
    clickBtn: function(e) {
      var randomStrArr = ['childOne', 'childTwo', 'childThree'];
      this.child.someStr = randomStrArr[parseInt(Math.random() * 3)];
    }
  }
}
var data = this._data = this.$options.data = {
  someStr: 'hello ',
  className: 'btn',
  htmlStr: '<span style="color: #f00;">red</span>',
  child: {
    someStr: 'World !'
  }
}

而後 遍歷data對象,以下:

Object.keys(data).forEach(function(key) {
  self._proxyData(key);
});

_proxyData 方法代碼以下:

_proxyData: function(key) {
  var self = this;
  Object.defineProperty(self, key, {
    configurable: false,  // 是否能夠刪除或修改目標屬性
    enumerable: true,   // 是否可枚舉
    get: function proxyGetter() {
      return self._data[key];
    },
    set: function proxySetter(newVal) {
      self._data[key] = newVal;
    }
  })
}

使用Object.defineProperty來監聽對象屬性的變化,使vm實列的屬性轉變成vm._data的屬性值。
接着初始化 this._initComputed();方法;代碼以下:

_initComputed: function() {
  var self = this;
  var computed = this.$options.computed;
  if (typeof computed === 'object') {
    Object.keys(computed).forEach(function(key) {
      Object.defineProperty(self, key, {
        get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
        set: function() {}
      })
    })
  }
}

首先判斷 computed 是否爲對象,而後遍歷computed,判斷若是computed[key] 鍵是不是一個函數,若是是一個函數的話,就執行該函數,不然的話,就執行該get調用。因此咱們看到使用vue的
時候會看到 computed方法初始時調用。

而後 初始化Observer
observer(data, this);

所以會實例化 Observer;代碼以下:

function Observer(data) {
  this.data = data;
  this.init();
}

所以 this.data值爲:

this.data = {
  someStr: 'hello ',
  className: 'btn',
  htmlStr: '<span style="color: #f00;">red</span>',
  child: {
    someStr: 'World !'
  }
}

而後調用init方法,以下代碼:

init: function() {
  var self = this;
  var data = self.data;
  // 遍歷data對象
  Object.keys(data).forEach(function(key) {
    self.defineReactive(data, key, data[key]);
  });
},

遍歷data,獲取每個key,也就是 key 能夠爲 someStr, className, htmlStr, child等。
而後執行 self.defineReactive 方法,參數以下三個

data = {
  someStr: 'hello ',
  className: 'btn',
  htmlStr: '<span style="color: #f00;">red</span>',
  child: {
    someStr: 'World !'
  }
}

key 分別爲 someStr, className, htmlStr, child,
data[key]值分別爲:

'hello ', 'btn', '<span style="color: #f00;">red</span>', 和 
{
  someStr: 'World !'
}

調用 defineReactive  代碼以下:

defineReactive: function(data, key, value) {
  var dep = new Dep();
  // 遞歸遍歷子對象
  var childObj = observer(value);

  // 對對象的屬性使用Object.defineProperty進行監聽
  Object.defineProperty(data, key, {
    enumerable: true,  // 可枚舉
    configurable: false, // 不能刪除目標屬性或不能修改目標屬性
    get: function() {
      if (Dep.target) {
        dep.depend();
      }
      return value;
    },
    set: function(newVal) {
      if (newVal === value) {
        return;
      }
      value = newVal;
      // 若是新值是對象的話,遞歸該對象 進行監聽
      childObj = observer(newVal);
      // 通知訂閱者 
      dep.notify();
    }
  });
}

同理 使用 Object.defineProperty 監聽對象屬性值的變化。執行完成後,下一步代碼執行以下:
// 初始化 Compile
this.$compile = new Compile(options.el || document.body, this);

options.el參數就是傳進去的Id爲 #mvvm-app, 若是沒有的話,就是 document.body;
compile實列化代碼以下:

/*
 * @param {el} 元素節點容器 如:el: '#mvvm-app'
 * @param {vm} Object 對象傳遞數據 
*/
function Compile(el, vm) {
  this.$vm = vm;
  // 判斷是元素節點 仍是 選擇器
  this.$el = this.isElementNode(el) ? el : document.querySelector(el);
  if (this.$el) {
    /*
     由於遍歷解析過程有屢次操做dom節點,爲了提升性能和效率,會先將根節點el轉換成文檔碎片fragment進行解析編譯操做,
     解析完成後,再將fragment添加回原來真實的dom節點中。有關 DocumentFragment 請看 http://www.cnblogs.com/tugenhua0707/p/7465915.html
     */
    this.$fragment = this.node2Fragment(this.$el);
    this.init();
    this.$el.appendChild(this.$fragment);
  }
}

調用init方法會進行compileElement 執行解析編譯操做,找到id爲#mvvm-app的childNodes, 遍歷子節點首先會判斷是元素節點仍是文本節點,若是是元素節點會調用 該以下方法:
this.compile(node); 進行元素節點編譯和解析操做, 若是是文本節點的話,就會 調用 this.compileText(node, RegExp.$1); 方法編譯文本節點。

理清思路:
視圖 --> 模型的改變:

在輸入框輸入新值的時,會在 compile.js裏面會監聽input事件,以下代碼:

node.addEventListener('input', function(e) {
    var newValue = e.target.value;
    if (val === newValue) {
      return;
    }
    self.setVMVal(vm, attrValue, newValue);
    val = newValue;
 });

首先會獲取當前的值,而後執行 setVMVal該方法,以下代碼:

/*
    設置普通指令的值
    @param {vm} 數據對象
    @param {exp} 普通指令的值 好比 v-model="xx" 那麼exp就等於xx
    @param {value} 新值
   */
  setVMVal: function(vm, exp, value) {
    var val = vm;
    exp = exp.split('.');
    exp.forEach(function(key, index) {
      // 若是不是最後一個元素的話,更新值
      /*
       數據對象 data 以下數據
       data: {
        child: {
          someStr: 'World !'
        }
      },
       若是 v-model="child.someStr" 那麼 exp = ["child", "someStr"], 遍歷該數組,
       val = val["child"]; val 先儲存該對象,而後再繼續遍歷 someStr,會執行else語句,所以val['someStr'] = value, 就會更新到對象的值了。
       */
      if (index < exp.length - 1) {
        val = val[key];
      } else {
        val[key] = value;
      }
    });
  }

執行完setVMVal方法後,會拿到最新值,而後會調用 Object.defineProperty 裏面的setter方法,以下代碼:

set: function(newVal) {
    if (newVal === value) {
      return;
    }
    value = newVal;
    // 發佈消息
    dep.public();
  }

從而發佈消息, 執行方法 dep.public(); 所以會調用Dep裏面的public方法,代碼以下:

public: function() {
    this.subs.forEach(function(sub){
      sub.update();
    });
  }

從而調用 watcher.js 裏面的update方法,代碼以下:

update: function() {
  this.run();
},

接着會調用run方法,代碼以下:

run: function() {
    var value = this.get();
    var oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm, value, oldVal);
    }
 },

先調用get方法拿到當前的值,代碼以下:

get: function() {
    Dep.target = this;
    var value = this.getter.call(this.vm, this.vm);
    Dep.target = null;
    return value;
},

如上代碼,會調用 this.getter方法,this.getter方法初始化代碼在watcher以下:

// expOrFn 是事件函數的話
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = this.parseGetter(expOrFn);
  }

接着調用 parseGetter 方法代碼:

parseGetter: function(exp) {
    if (/[^\w.$]/.test(exp)) return; 
    var exps = exp.split('.');
    return function(obj) {
      for (var i = 0, len = exps.length; i < len; i++) {
        if (!obj) return;
        obj = obj[exps[i]];
      }
      return obj;
    }
}

用完成後,會執行到get方法裏面代碼 var value = this.getter.call(this.vm, this.vm); 所以會繼續調用到 parseGetter 返回函數的代碼,因此最後返回新值。
返回新值後,在執行函數run方法後,判斷新舊值是否相等,不相等的話,就會執行 compile.js裏面bind的方法的回調,以下代碼:

new Watcher(vm, attrValue, function(value, oldValue) {
    updaterFn && updaterFn(node, value, oldValue);
});

compile.js裏面的updater對象以下:

var updater = {
  textUpdater: function(node, value) {
    node.textContent = typeof value === 'undefined' ? '' : value;
  },
  htmlUpdater: function(node, value) {
    node.innerHTML = typeof value === 'undefined' ? '' : value;
  },
  classUpdater: function(node, newValue, oldValue) {
    var className = node.className;
    className = className.replace(oldValue, '').replace(/\s$/, '');
    var space = className && String(newValue) ? ' ' : '';
    node.className = className + space + newValue;
  },
  modelUpdater: function(node, newValue) {
    node.value = typeof newValue === 'undefined' ? '' : newValue;
  }
};

從而更新視圖。

模型 -> 視圖的改變

當頁面初始化時,會判斷是普通指令 v-model, v-text, or v-html,仍是事件指令 v-on:click; compile.js相對應的代碼以下:

// 判斷是不是事件指令
if (self.isEventDirective(dir)) {
  // 事件指令 好比 v-on:click 這樣的
  compileUtil.eventHandler(node, self.$vm, attrValue, dir);
}

因此會對事件進行處理: 以下函數:

// 事件處理
eventHandler: function(node, vm, attrValue, dir) {
  var eventType = dir.split(':')[1],
    fn = vm.$options.methods && vm.$options.methods[attrValue];
    if (eventType && fn) {
      node.addEventListener(eventType, fn.bind(vm), false);
    }
}

所以會對該節點進行綁定 'click'事件,代碼 node.addEventListener(eventType, fn.bind(vm), false); 就是對綁定的事件 進行監聽,demo裏面是使用 v-on:click, 所以eventType就是click事件,所以會對 點擊事件 進行監聽。而後執行相對應的回調函數。該demo中的回調函數爲 clickBtn(函數名)。
clickBtn函數代碼以下:

clickBtn: function(e) {
  var randomStrArr = ['childOne', 'childTwo', 'childThree'];
  this.child.someStr = randomStrArr[parseInt(Math.random() * 3)];
}

所以 this.child.someStr 會從新賦值一個隨機數,也就是說值會獲得更新,所以首先會調用 mvvm.js的_proxyData中的get方法;代碼以下:

_proxyData: function(key, setter, getter) {
  var me = this;
  setter = setter || 
  Object.defineProperty(me, key, {
      configurable: false,
      enumerable: true,
      get: function proxyGetter() {
          return me._data[key];
      },
      set: function proxySetter(newVal) {
          me._data[key] = newVal;
      }
  });
}

頁面初始化的時候,vm.data.child.someStr 會被mvvm.js 裏面的 代理方法轉換成 vm.child.someStr, 所以給 vm.child.someStr 設置新值的時候,會調用 Object.defineProperty方法來監聽屬性值的改變,所以須要返回一個新值,因此先調用mvvm.js中的get方法,以後會調用 Observer.js代碼 的Object.defineProperty(obj, key, {})中的get方法,返回頁面初始化的值,而後會調用Observer.js該對應的set方法,獲取新值,而後判斷新舊值是否相等,若是不相等的話,就會把消息發佈出去該訂閱者,訂閱者會接收該消息。從而和上面 視圖 -》模型 步驟同樣 渲染視圖頁面。
git代碼

相關文章
相關標籤/搜索