使用Proxy實現vue數據雙向綁定


  Proxy可以理解成,在目標對象之前架設一層 "攔截",當外界對該對象訪問的時候,都必須經過這層攔截,而Proxy就充當了這種機制,類似於代理的含義,它可以對外界訪問對象之前進行過濾和改寫該對象。

  如果對vue2.xx了解或看過源碼的人都知道,vue2.xx中使用 Object.defineProperty()方法對該對象通過 遞歸+遍歷 的方式來實現對數據的監控的,但是當我們使用數組的方法或改變數組的下標是不能重新觸發 Object.defineProperty中的set()方法的,因此就做不到實時響應了。所以使用 Object.defineProperty 存在如下缺點:

  1. 監聽數組的方法不能觸發Object.defineProperty方法中的set操作(如果要監聽的到話,需要重新編寫數組的方法)。
  2. 必須遍歷每個對象的每個屬性,如果對象嵌套很深的話,需要使用遞歸調用。

  因此vue3.xx中之后就改用Proxy來更好的解決如上面的問題。我們來簡單的學習下使用Proxy來實現一個簡單的vue雙向綁定。

  我們都知道實現數據雙向綁定,需要實現如下幾點:

  1. 需要實現一個數據監聽器 Observer, 能夠對所有數據進行監聽,如果有數據變動的話,拿到最新的值並通知訂閱者Watcher.
  2. 需要實現一個指令解析器Compile,它能夠對每個元素的指令進行掃描和解析,根據指令模板替換數據,以及綁定相對應的函數。
  3. 需要實現一個Watcher, 它是鏈接Observer和Compile的橋梁,它能夠訂閱並收到每個屬性變動的通知,然后會執行指令綁定的相對應的回調函數,從而更新視圖。

  大概的雙向綁定的原理為:

  下面是一個簡單的demo,我們可以參考下,理解下原理:

<!DOCTYPE html>
 <html>
    <head>
      <meta charset="utf-8">
      <title>標題</title>
    </head>
    <body>
      <div id="app">
        <input type="text" v-model='count' />
        <input type="button" value="增加" @click="add" />
        <input type="button" value="減少" @click="reduce" />
        <div v-bind="count"></div>
      </div>
      <script type="text/javascript">   
        class Vue { constructor(options) { this.$el = document.querySelector(options.el); this.$methods = options.methods; this._binding = {}; this._observer(options.data); this._compile(this.$el); } _pushWatcher(watcher) { if (!this._binding[watcher.key]) { this._binding[watcher.key] = []; } this._binding[watcher.key].push(watcher); } /* observer的作用是能夠對所有的數據進行監聽操作,通過使用Proxy對象 中的set方法來監聽,如有發生變動就會拿到最新值通知訂閱者。 */ _observer(datas) { const me = this; const handler = { set(target, key, value) { const rets = Reflect.set(target, key, value); me._binding[key].map(item => { item.update(); }); return rets; } }; this.$data = new Proxy(datas, handler); } /* 指令解析器,對每個元素節點的指令進行掃描和解析,根據指令模板替換數據,以及綁定相對應的更新函數 */ _compile(root) { const nodes = Array.prototype.slice.call(root.children); const data = this.$data; nodes.map(node => { if (node.children && node.children.length) { this._compile(node.children); } const $input = node.tagName.toLocaleUpperCase() === "INPUT"; const $textarea = node.tagName.toLocaleUpperCase() === "TEXTAREA"; const $vmodel = node.hasAttribute('v-model'); // 如果是input框 或 textarea 的話,並且帶有 v-model 屬性的
              if (($vmodel && $input) || ($vmodel && $textarea)) { const key = node.getAttribute('v-model'); this._pushWatcher(new Watcher(node, 'value', data, key)); node.addEventListener('input', () => { data[key] = node.value; }); } if (node.hasAttribute('v-bind')) { const key = node.getAttribute('v-bind'); this._pushWatcher(new Watcher(node, 'innerHTML', data, key)); } if (node.hasAttribute('@click')) { const methodName = node.getAttribute('@click'); const method = this.$methods[methodName].bind(data); node.addEventListener('click', method); } }); } } /* watcher的作用是 鏈接Observer 和 Compile的橋梁,能夠訂閱並收到每個屬性變動的通知, 執行指令綁定的響應的回調函數,從而更新視圖。 */
        class Watcher { constructor(node, attr, data, key) { this.node = node; this.attr = attr; this.data = data; this.key = key; } update() { this.node[this.attr] = this.data[this.key]; } } </script>
      <script type="text/javascript">
        new Vue({ el: '#app', data: { count: 0 }, methods: { add() { this.count++; }, reduce() { this.count--; } } }); </script>
    </body>
</html>

  首先我們想實現類似vue那要的初始化代碼,如:new Vue()。因此使用ES6 基本語法如下:

class Vue { constructor(options) { this.$el = document.querySelector(options.el); this.$methods = options.methods; this._binding = {}; this._observer(options.data); this._compile(this.$el); } }

  Vue類使用new創建一個實例化的時候,就會執行 constructor方法代碼,因此options是vue傳入的一個對象,它有 el,data, methods等屬性。 如上代碼先執行 this._observer(options.data); 該 observer 函數就是監聽所有數據的變動函數。

1、實現Observer對所有的數據進行監聽。

_observer(datas) { const me = this; const handler = { set(target, key, value) { const rets = Reflect.set(target, key, value); me._binding[key].map(item => { item.update(); }); return rets; } }; this.$data = new Proxy(datas, handler); }

  使用了我們上面介紹的Proxy中的set方法對所有的數據進行監聽,只要我們Vue實列屬性data中有任何數據發生改變的話,都會自動調用Proxy中的set方法,我們上面的代碼使用了 const rets = Reflect.set(target, key, value); return rets; 這樣的代碼,就是對我們的data中的任何數據發生改變后,使用該方法重新設置新值,然后返回給 this.$data保存到這個全局里面。

me._binding[key].map(item => { item.update(); });

  如上this._binding 是一個對象,對象里面保存了所有的指令及對應函數,如果發生改變,拿到最新值通知訂閱者,因此通知Watcher類中的update方法,如下Watcher類代碼如下:

/* watcher的作用是 鏈接Observer 和 Compile的橋梁,能夠訂閱並收到每個屬性變動的通知, 執行指令綁定的響應的回調函數,從而更新視圖。 */
class Watcher { constructor(node, attr, data, key) { this.node = node; this.attr = attr; this.data = data; this.key = key; } update() { this.node[this.attr] = this.data[this.key]; } }

2、實現Compile

class Vue { constructor(options) { this.$el = document.querySelector(options.el); this._compile(this.$el); } }

  以上代碼初始化,_compile 函數的作用就是對頁面中每個元素節點的指令進行解析和掃描的,根據指令模板替換數據,以及綁定相應的更新函數。

_compile(root) { const nodes = Array.prototype.slice.call(root.children); const data = this.$data; nodes.map(node => { if (node.children && node.children.length) { this._compile(node.children); } const $input = node.tagName.toLocaleUpperCase() === "INPUT"; const $textarea = node.tagName.toLocaleUpperCase() === "TEXTAREA"; const $vmodel = node.hasAttribute('v-model'); // 如果是input框 或 textarea 的話,並且帶有 v-model 屬性的
      if (($vmodel && $input) || ($vmodel && $textarea)) { const key = node.getAttribute('v-model'); this._pushWatcher(new Watcher(node, 'value', data, key)); node.addEventListener('input', () => { data[key] = node.value; }); } if (node.hasAttribute('v-bind')) { const key = node.getAttribute('v-bind'); this._pushWatcher(new Watcher(node, 'innerHTML', data, key)); } if (node.hasAttribute('@click')) { const methodName = node.getAttribute('@click'); const method = this.$methods[methodName].bind(data); node.addEventListener('click', method); } }); } }

1、拿到根元素的子節點,然后讓子元素變成數組的形式,如代碼const nodes = Array.prototype.slice.call(root.children);

2、保存變動后的 this.$data, const data = this.$data;

3、nodes子節點進行遍歷,如果子節點還有子節點的話,就會遞歸調用 _compile方法

4、對子節點進行判斷,如果子節點是input元素或textarea元素的話,並且有 v-model這樣的指令的話,如下代碼

nodes.map(node => { const $input = node.tagName.toLocaleUpperCase() === "INPUT"; const $textarea = node.tagName.toLocaleUpperCase() === "TEXTAREA"; const $vmodel = node.hasAttribute('v-model'); // 如果是input框 或 textarea 的話,並且帶有 v-model 屬性的
  if (($vmodel && $input) || ($vmodel && $textarea)) { const key = node.getAttribute('v-model'); this._pushWatcher(new Watcher(node, 'value', data, key)); node.addEventListener('input', () => { data[key] = node.value; }); } });

  如上代碼,如果有 v-model,就獲取v-model該屬性值,如代碼: const key = node.getAttribute('v-model');然后把該指令通知訂閱者 Watcher; 如下代碼:this._pushWatcher(new Watcher(node, 'value', data, key));就會調用 Watcher類的constructor的方法,如下代碼:

class Watcher { constructor(node, attr, data, key) { this.node = node; this.attr = attr; this.data = data; this.key = key; } }

  把 node節點,attr屬性,data數據,v-model指令key保存到this對象中了。然后調用 this._pushWatcher(watcher); 這樣方法。_pushWatcher代碼如下:

if (!this._binding[watcher.key]) { this._binding[watcher.key] = []; } this._binding[watcher.key].push(watcher);

  如上代碼,先判斷 this._binding 有沒有 v-model指令中的key, 如果沒有的話,就把該 this._binding[key] = []; 設置成空數組。然后就把它存入 this._binding[key] 數組里面去。

5、對於 input 或 textarea 這樣的 v-model 會綁定相對應的函數,如下代碼:

node.addEventListener('input', () => { data[key] = node.value; });

  當input或textarea有值發生改變的話,那么就把最新的值存入 Vue類中的data對象里面去,因此data中的數據會發生改變,因此會自動觸發執行 _observer 函數中的Proxy中的set方法函數,還是一樣,首先更新最新值,使用代碼:const rets = Reflect.set(target, key, value);然后遍歷 保存到 this._binding 對象中對應的鍵;如下代碼:

me._binding[key].map(item => { console.log(item); item.update(); });

  然后同時代碼中如果有 v-bind這樣的指令的話,也會和上面的邏輯一樣判斷和執行;如下 v-bind指令代碼如下:

if (node.hasAttribute('v-bind')) { const key = node.getAttribute('v-bind'); this._pushWatcher(new Watcher(node, 'innerHTML', data, key)); }

  然后也會更新到視圖里面去,那么 attr = 'innerHTML', node 是該元素的節點,key 也是 v-model中的屬性值了,因此 this.node.innerHTML = thid.data['key'];

6、對於頁面中元素節點帶有 @click這樣的方法,也有判斷,如下代碼:

if (node.hasAttribute('@click')) { const methodName = node.getAttribute('@click'); const method = this.$methods[methodName].bind(data); node.addEventListener('click', method); }

  如上代碼先判斷該node是否有該屬性,然后獲取該屬性的值,比如html頁面中有 @click="add" 和 @click="reduce" 這樣的,當點擊的時候,也會調用 this.data,監聽該對象的值發生改變的話,同樣會調用 Proxy中的set函數,最后也是一樣執行函數去更新視圖的。

  如上就是使用proxy實現數據雙向綁定的基本原理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM