深刻理解Proxy 及 使用Proxy實現vue數據雙向綁定

閱讀目錄javascript

1.什麼是Proxy?它的做用是?html

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

若是對vue2.xx瞭解或看過源碼的人都知道,vue2.xx中使用 Object.defineProperty()方法對該對象經過 遞歸+遍歷的方式來實現對數據的監控的,具體瞭解
 Object.defineProperty能夠看我上一篇文章(http://www.javashuo.com/article/p-qpgkyhbt-hh.html). 可是經過上一篇Object.defineProperty文章 咱們也知道,當咱們使用數組的方法或改變數組的下標是不能從新觸發 Object.defineProperty中的set()方法的,所以就作不到實時響應了。因此使用 Object.defineProperty 存在以下缺點:java

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

所以vue3.xx中以後就改用Proxy來更好的解決如上面的問題。在學習使用Proxy實現數據雙向綁定以前,咱們仍是一步步來,先學習了Proxy基本知識點。git

Proxy基本語法github

const obj = new Proxy(target, handler);數組

參數說明以下:app

target: 被代理對象。
handler: 是一個對象,聲明瞭代理target的一些操做。
obj: 是被代理完成以後返回的對象。函數

可是當外界每次對obj進行操做時,就會執行handler對象上的一些方法。handler中經常使用的對象方法以下:

1. get(target, propKey, receiver)
2. set(target, propKey, 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, propKey, 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, propKey, value, receiver)

該方法是用來攔截某個屬性的賦值操做,它能夠接受四個參數,參數解析分別以下:
target: 目標對象。
propKey: 目標對象的屬性名
value: 屬性值
receiver(可選): 通常狀況下是Proxy實列
以下代碼演示:

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

const handler = {
  set: function(obj, prop, value) {
    return Reflect.set(obj, prop, 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, prop, value, receiver) {
    Reflect.set(obj, prop, value);
  }
};

const proxy = new Proxy(obj, handler);
proxy.name = '我是空智';
console.log(proxy.name); // 打印的是 kongzhi

注意:proxy對數組也是能夠監聽的;以下代碼演示,數組中的 push方法監聽:

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

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

const proxy = new Proxy(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, args, 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, object, args)

該方法是攔截函數的調用的。該方法接收三個參數,分別是目標對象。目標對象上下文this對象 和 目標對象的數組;它和 Reflect.apply參數是同樣的,瞭解 
Reflect.apply(http://www.javashuo.com/article/p-sljulsac-eb.html).

使用demo以下演示:

function testA(p1, p2) {
  return p1 + p2;
}
const handler = {
  apply: function(target, ctx, args) {
    /*
      這裏的 ...arguments 其實就是上面的三個參數 target, ctx, 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, 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>

點擊能夠查看效果

如上代碼咱們來分析下原理以下:

首先他是使用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, 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方法,以下代碼:

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, '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();
});

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

而後執行 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, 'innerHTML', data, 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',
  data: {
    count: 0
  },
  methods: {
    add() {
      this.count++;
    },
    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實現數據雙向綁定的基本原理的。

相關文章
相關標籤/搜索