1. 程式人生 > 程式設計 >理解Proxy及使用Proxy實現vue資料雙向繫結操作

理解Proxy及使用Proxy實現vue資料雙向繫結操作

1.什麼是Proxy?它的作用是?

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

如果對vue2.xx瞭解或看過原始碼的人都知道,vue2.xx中使用 Object.defineProperty()方法對該物件通過 遞迴+遍歷的方式來實現對資料的監控的,具體瞭解

Object.defineProperty可以看我上一篇文章(https://www.jb51.net/article/191097.htm). 但是通過上一篇Object.defineProperty文章 我們也知道,當我們使用陣列的方法或改變陣列的下標是不能重新觸發 Object.defineProperty中的set()方法的,因此就做不到實時響應了。所以使用 Object.defineProperty 存在如下缺點:

1. 監聽陣列的方法不能觸發Object.defineProperty方法中的set操作(如果要監聽的到話,需要重新編寫陣列的方法)。

2. 必須遍歷每個物件的每個屬性,如果物件巢狀很深的話,需要使用遞迴呼叫。

因此vue3.xx中之後就改用Proxy來更好的解決如上面的問題。在學習使用Proxy實現資料雙向繫結之前,我們還是一步步來,先學習了Proxy基本知識點。

Proxy基本語法

const obj = new Proxy(target,handler);

引數說明如下:

target: 被代理物件。

handler: 是一個物件,聲明瞭代理target的一些操作。

obj: 是被代理完成之後返回的物件。

但是當外界每次對obj進行操作時,就會執行handler物件上的一些方法。handler中常用的物件方法如下:

1. get(target,propKey,receiver)

2. set(target,value,receiver)

3. has(target,propKey)

4. construct(target,args):

5. apply(target,object,args)

如上是Proxy中handler 物件的方法,其實它和Reflect裡面的方法類似的,想要了解Reflect看這篇文章

如下程式碼演示:

const target = {
 name: 'kongzhi'
};

const handler = {
 get: function(target,key) {
 console.log(`${key} 被讀取`);
 return target[key];
 },set: function(target,key,value) {
 console.log(`${key} 被設定為 ${value}`);
 target[key] = value;
 }
};

const testObj = new Proxy(target,handler);

/*
 獲取testObj中name屬性值
 會自動執行 get函式後 列印資訊:name 被讀取 及輸出名字 kongzhi
*/
console.log(testObj.name);

/*
 改變target中的name屬性值
 列印資訊如下: name 被設定為 111 
*/
testObj.name = 111;

console.log(target.name); // 輸出 111

如上程式碼所示:也就是說 target是被代理的物件,handler是代理target的,那麼handler上面有set和get方法,當每次列印target中的name屬性值的時候會自動執行handler中get函式方法,當每次設定 target.name 屬性值的時候,會自動呼叫 handler中的set方法,因此target物件對應的屬性值會發生改變,同時改變後的 testObj物件也會發生改變。同理改變返回後 testObj物件中的屬性也會改變原物件target的屬性的,因為物件是引用型別的,是同一個引用的。如果這樣還是不好理解的話,可以簡單的看如下程式碼應該可以理解了:

const target = {
 name: 'kongzhi'
};

const testA = target;
testA.name = 'xxx';
console.log(testA.name); // 列印 xxx
console.log(target.name); // 列印 xxx

2.get(target,receiver)

該方法的含義是:用於攔截某個屬性的讀取操作。它有三個引數,如下解析:

target: 目標物件。

propKey: 目標物件的屬性。

receiver: (可選),該引數為上下文this物件

如下程式碼演示:

const obj = {
 name: 'kongzhi'
};

const handler = {
 get: function(target,propKey) {
 // 使用 Reflect來判斷該目標物件是否有該屬性
 if (Reflect.has(target,propKey)) {
  // 使用Reflect 來讀取該物件的屬性
  return Reflect.get(target,propKey);
 } else {
  throw new ReferenceError('該目標物件沒有該屬性');
 }
 }
};

const testObj = new Proxy(obj,handler);
/* 
 Proxy中讀取某個物件的屬性值的話,
 就會使用get方法進行攔截,然後返回該值。
 */
console.log(testObj.name); // kongzhi

/*
 如果物件沒有該屬性的話,就會進入else語句,就會報錯:
 Uncaught ReferenceError: 該目標物件沒有該屬性
*/
// console.log(testObj.name2);

/*
 其實Proxy中攔截的操作是在原型上的,因此我們也可以使用 Object.create(obj)
 來實現物件的繼承的。
 如下程式碼演示:
*/
const testObj2 = Object.create(testObj);
console.log(testObj2.name);

// 看看他們的原型是否相等 
console.log(testObj2.__proto__ === testObj.__proto__); // 返回true

如果沒有這個攔截的話,如果某個物件沒有該屬性的話,會輸出 undefined.

3.set(target,receiver)

該方法是用來攔截某個屬性的賦值操作,它可以接受四個引數,引數解析分別如下:

target: 目標物件。

propKey: 目標物件的屬性名

value: 屬性值

receiver(可選): 一般情況下是Proxy實列

如下程式碼演示:

const obj = {
 'name': 'kongzhi'
};

const handler = {
 set: function(obj,prop,value) {
 return Reflect.set(obj,value);
 }
};

const proxy = new Proxy(obj,handler);
proxy.name = '我是空智';
console.log(proxy.name); // 輸出: 我是空智
console.log(obj); // 輸出: {name: '我是空智'}

當然如果設定該物件的屬性是不可寫的,那麼set方法就不起作用了,如下程式碼演示:

const obj = {
 'name': 'kongzhi'
};

Object.defineProperty(obj,'name',{
 writable: false
});

const handler = {
 set: function(obj,receiver) {
 Reflect.set(obj,handler);
proxy.name = '我是空智';
console.log(proxy.name); // 列印的是 kongzhi

注意:proxy對陣列也是可以監聽的;如下程式碼演示,陣列中的 push方法監聽:

const obj = [{
 'name': 'kongzhi'
}];

const handler = {
 set: function(obj,handler);
proxy.push({'name': 'kongzhi222'});
proxy.forEach(function(item) {
 console.log(item.name); // 打印出 kongzhi kongzhi222
});

4.has(target,propKey)

該方法是判斷某個目標物件是否有該屬性名。接收二個引數,分別為目標物件和屬性名。返回的是一個布林型。

如下程式碼演示:

const obj = {
 'name': 'kongzhi'
};

const handler = {
 has: function(target,key) {
 if (Reflect.has(target,key)) {
  return true;
 } else {
  return false;
 }
 }
};

const proxy = new Proxy(obj,handler);
console.log(Reflect.has(obj,'name')); // true
console.log(Reflect.has(obj,'age')); // false

5.construct(target,args,newTarget):

該方法是用來攔截new命令的,它接收三個引數,分別為 目標物件,建構函式的引數物件及創造實列的物件。

第三個引數是可選的。它的作用是攔截物件屬性。

如下程式碼演示:

function A(name) {
 this.name = name;
}

const handler = {
 construct: function(target,newTarget) {
 /*
  輸出: function A(name) {
    this.name = name;
   }
 */
 console.log(target); 
 // 輸出: ['kongzhi',{age: 30}]
 console.log(args); 
 return args
 }
};

const Test = new Proxy(A,handler);
const obj = new Test('kongzhi',{age: 30});
console.log(obj); // 輸出: ['kongzhi',{age: 30}]

6.apply(target,args)

該方法是攔截函式的呼叫的。該方法接收三個引數,分別是目標物件。目標物件上下文this物件 和 目標物件的陣列;它和 Reflect.apply引數是一樣的,瞭解 Reflect.apply(https://www.jb51.net/article/191099.htm).

使用demo如下演示:

function testA(p1,p2) {
 return p1 + p2;
}
const handler = {
 apply: function(target,ctx,args) {
 /*
  這裡的 ...arguments 其實就是上面的三個引數 target,args 對應的值。
  分別為:
  target: function testA(p1,p2) {
  return p1 + p2;
  }
  ctx: undefined
  args: [1,2]
  使用 Reflect.apply(...arguments) 呼叫testA函式,因此返回 (1+2) * 2 = 6
 */
 console.log(...arguments);
 return Reflect.apply(...arguments) * 2;
 }
}
const proxy = new Proxy(testA,handler);
console.log(proxy(1,2)); // 6

// 也可以如下呼叫
console.log(proxy.apply(null,[1,3])); // 8
// 我們也可以使用 Reflect.apply 呼叫
console.log(Reflect.apply(proxy,null,[3,5])); // 16

7.使用Proxy實現簡單的vue雙向繫結

vue3.x使用了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,value) {
    const rets = Reflect.set(target,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',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,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>

如上程式碼我們來分析下原理如下:

首先他是使用ES6編寫的語法來實現的。首先我們想實現類似vue那要的初始化程式碼,如下這樣設想:

new Vue({
 el: '#app',data: {
 count: 0
 },methods: {
 add() {
  this.count++;
 },reduce() {
  this.count--;
 }
 }
});

因此使用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,value) {
  const rets = Reflect.set(target,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,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,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,key));
  node.addEventListener('input',() => {
   data[key] = node.value;
  });
  }
  if (node.hasAttribute('v-bind')) {
  const key = node.getAttribute('v-bind');
  this._pushWatcher(new Watcher(node,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方法,如下程式碼:

nodes.map(node => {
 if (node.children && node.children.length) {
 this._compile(node.children);
 }
});

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,key));
 node.addEventListener('input',() => {
  data[key] = node.value;
 });
 }
});

如上程式碼,如果有 v-model,就獲取v-model該屬性值,如程式碼:

const key = node.getAttribute('v-model');

然後把該指令通知訂閱者 Watcher; 如下程式碼:

this._pushWatcher(new Watcher(node,key));

就會呼叫 Watcher類的constructor的方法,如下程式碼:

class Watcher {
 constructor(node,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,value);

然後遍歷 儲存到 this._binding 物件中對應的鍵;如下程式碼:

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

如上,我們在input輸入框輸入1的時候,列印item值如下所示:

理解Proxy及使用Proxy實現vue資料雙向繫結操作

然後執行 item.update()方法,update方法如下:

class Watcher {
 update() {
 this.node[this.attr] = this.data[this.key];
 }
}

就會更新值到視圖裡面去,比如input或textarea,那麼 attr = 'value',node 是該元素的節點,key 就是 v-model中的屬性值,因此 this.node['value'] = this.data[key];

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

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

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

比如頁面中html程式碼如下:

<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>

實列化程式碼如下:

new Vue({
 el: '#app',reduce() {
  this.count--;
 }
 }
});

因此上面的 node 是 <input type="text" v-model='count' /> input中的node節點了,因此 node.value = this.data['count']; 因此 input框的值就更新了,同時 <div v-bind="count"></div> 該節點通過 node.innerHTML = this.data['count'] 這樣的話,值也得到了更新了。

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.methods[methodName].bind(data)中對應vue實列中對應的函式的。因此也會執行函式的,其中data就是this.data,監聽該物件的值發生改變的話,同樣會呼叫 Proxy中的set函式,最後也是一樣執行函式去更新檢視的。如上就是使用proxy實現資料雙向繫結的基本原理的。

以上這篇理解Proxy及使用Proxy實現vue資料雙向繫結操作就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。