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實現數據雙向綁定的基本原理。