vue3.0 嚐鮮 -- 摒棄 Object.defineProperty,基於 Proxy 的觀察者機制探索

寫在前面: 11月16日早上,Vue.js的做者尤大大在 Vue Toronto 的主題演講中預演了 Vue.js 3.0的一些新特性,其中一個很重要的改變就是Vue3 將使用 ES6的Proxy 做爲其觀察者機制,取代以前使用的Object.defineProperty。我相信許多同窗深有體會,許多面試中Object.defineProperty是vue這個框架一個出現率很高的考察點,一開始你們對這個屬性還有點陌生,慢慢的隨着使用vue的人愈來愈多,這個屬性常常被你們拿來研究,而就在你們漸漸熟悉了這個屬性之後,vue的做者打算在下個vue版本中用 Proxy替換它,果真一入前端坑就爬不出來了哈哈。雖然vue3正式發佈要等到明年下半年了,但咱們下面能夠來探索下基於 Proxy 的觀察者機制,預測下vue3關於Proxy這部分的代碼(雖然看上去並無什麼用哈哈)。css

一.爲何要取代Object.defineProperty
既然要取代Object.defineProperty,那它確定是有一些明顯的缺點,總結起來大概是下面兩個:html

  1. 在Vue中,Object.defineProperty沒法監控到數組下標的變化,致使直接經過數組的下標給數組設置值,不能實時響應。 爲了解決這個問題,通過vue內部處理後可使用如下幾種方法來監聽數組 (評論區有提到,Object.defineProperty自己是能夠監控到數組下標的變化的,具體可參Vue爲何不能檢測數組變更
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
複製代碼

因爲只針對了以上八種方法進行了hack處理,因此其餘數組的屬性也是檢測不到的,仍是具備必定的侷限性。前端

  1. Object.defineProperty只能劫持對象的屬性,所以咱們須要對每一個對象的每一個屬性進行遍歷。Vue裏,是經過遞歸以及遍歷data 對象來實現對數據的監控的,若是屬性值也是對象那麼須要深度遍歷,顯然若是能劫持一個完整的對象,不論是對操做性仍是性能都會有一個很大的提高。

而要取代它的Proxy有如下兩個優勢;vue

  • 能夠劫持整個對象,並返回一個新對象
  • 有13種劫持操做

看到這可能有同窗要問了,既然Proxy能解決以上兩個問題,並且Proxy屬性在vue2.x以前就有了,爲何vue2.x不使用Proxy呢?一個很重要的緣由就是:git

  • Proxy是es6提供的新特性,兼容性很差,最主要的是這個屬性沒法用polyfill來兼容

經評論提醒,目前Proxy並無有效的兼容方案,將來大概會是3.0和2.0並行,須要支持IE的選擇2.0es6

關於Object.defineProperty來實現觀察者機制,能夠參照剖析Vue原理&實現雙向綁定MVVM這篇文章,下面的內容主要介紹如何基於 Proxy來實現vue觀察者機制。github

二.什麼是Proxy
面試

1.含義:segmentfault

  • Proxy是 ES6 中新增的一個特性,翻譯過來意思是"代理",用在這裏表示由它來「代理」某些操做。 Proxy 讓咱們可以以簡潔易懂的方式控制外部對對象的訪問。其功能很是相似於設計模式中的代理模式。
  • Proxy 能夠理解成,在目標對象以前架設一層「攔截」,外界對該對象的訪問,都必須先經過這層攔截,所以提供了一種機制,能夠對外界的訪問進行過濾和改寫。
  • 使用 Proxy 的核心優勢是能夠交由它來處理一些非核心邏輯(如:讀取或設置對象的某些屬性前記錄日誌;設置對象的某些屬性值前,須要驗證;某些屬性的訪問控制等)。 從而可讓對象只需關注於核心邏輯,達到關注點分離,下降對象複雜度等目的。

2.基本用法:設計模式

let p = new Proxy(target, handler);
複製代碼

參數:

target 是用Proxy包裝的被代理對象(能夠是任何類型的對象,包括原生數組,函數,甚至另外一個代理)。

handler 是一個對象,其聲明瞭代理target 的一些操做,其屬性是當執行一個操做時定義代理的行爲的函數。

p 是代理後的對象。當外界每次對 p 進行操做時,就會執行 handler 對象上的一些方法。Proxy共有13種劫持操做,handler代理的一些經常使用的方法有以下幾個:

get:讀取
set:修改
has:判斷對象是否有該屬性
construct:構造函數
複製代碼

3.示例:
下面就用Proxy來定義一個對象的get和set,做爲一個基礎demo

let obj = {};
 let handler = {
   get(target, property) {
    console.log(`${property} 被讀取`);
    return property in target ? target[property] : 3;
   },
   set(target, property, value) {
    console.log(`${property} 被設置爲 ${value}`);
    target[property] = value;
   }
 }
 
 let p = new Proxy(obj, handler);
 p.name = 'tom' //name 被設置爲 tom
 p.age; //age 被讀取 3
複製代碼

p 讀取屬性的值時,實際上執行的是 handler.get() :在控制檯輸出信息,而且讀取被代理對象 obj 的屬性。

p 設置屬性值時,實際上執行的是 handler.set() :在控制檯輸出信息,而且設置被代理對象 obj 的屬性的值。

以上介紹了Proxy基本用法,實際上這個屬性還有許多內容,具體可參考Proxy文檔

三.基於Proxy來實現雙向綁定

話很少說,接下來咱們就來用Proxy來實現一個經典的雙向綁定todolist,首先簡單的寫一點html結構:

<div id="app">
      <input type="text" id="input" />
      <div>您輸入的是: <span id="title"></span></div>
      <button type="button" name="button" id="btn">添加到todolist</button>
      <ul id="list"></ul>
 </div>
複製代碼

先來一個Proxy,實現輸入框的雙向綁定顯示:

const obj = {};
    const input = document.getElementById("input");
    const title = document.getElementById("title");
    
    const newObj = new Proxy(obj, {
      get: function(target, key, receiver) {
        console.log(`getting ${key}!`);
        return Reflect.get(target, key, receiver);
      },
      set: function(target, key, value, receiver) {
        console.log(target, key, value, receiver);
        if (key === "text") {
          input.value = value;
          title.innerHTML = value;
        }
        return Reflect.set(target, key, value, receiver);
      }
    });

    input.addEventListener("keyup", function(e) {
      newObj.text = e.target.value;
    });
複製代碼

這裏代碼涉及到Reflect屬性,這也是一個es6的新特性,還不太瞭解的同窗能夠參考Reflect文檔. 接下來就是添加todolist列表,先把數組渲染到頁面上去:

// 渲染todolist列表
    const Render = {
      // 初始化
      init: function(arr) {
        const fragment = document.createDocumentFragment();
        for (let i = 0; i < arr.length; i++) {
          const li = document.createElement("li");
          li.textContent = arr[i];
          fragment.appendChild(li);
        }
        list.appendChild(fragment);
      },
      addList: function(val) {
        const li = document.createElement("li");
        li.textContent = val;
        list.appendChild(li);
      }
    };

複製代碼

再來一個Proxy,實現Todolist的添加:

const arr = [];
    // 監聽數組
    const newArr = new Proxy(arr, {
      get: function(target, key, receiver) {
        return Reflect.get(target, key, receiver);
      },
      set: function(target, key, value, receiver) {
        console.log(target, key, value, receiver);
        if (key !== "length") {
          Render.addList(value);
        }
        return Reflect.set(target, key, value, receiver);
      }
    });

    // 初始化
    window.onload = function() {
      Render.init(arr);
    };

    btn.addEventListener("click", function() {
      newArr.push(parseInt(newObj.text));
    });

複製代碼

這樣就用 Proxy實現了一個簡單的雙向綁定Todolist,具體代碼可參考proxy.html

四.基於Proxy來實現vue的觀察者機制
1.Proxy實現observe

observe(data) {
        const that = this;
        let handler = {
         get(target, property) {
            return target[property];
          },
          set(target, key, value) {
            let res = Reflect.set(target, key, value);
            that.subscribe[key].map(item => {
              item.update();
            });
            return res;
          }
        }
        this.$data = new Proxy(data, handler);
      }

複製代碼

這段代碼裏把代理器返回的對象代理到this.$data,即this.$data是代理後的對象,外部每次對this.$data進行操做時,實際上執行的是這段代碼裏handler對象上的方法。

2.compile和watcher

比較熟悉vue的同窗都很清楚,vue2.x在 new Vue() 以後。 Vue 會調用 _init 函數進行初始化,它會初始化生命週期、事件、 props、 methods、 data、 computed 與 watch 等。其中最重要的是經過 Object.defineProperty 設置 setter 與 getter 函數,用來實現「響應式」以及「依賴收集」。相似於下面這個內部流程圖:

而咱們上面已經用Proxy取代了Object.defineProperty這部分觀察者機制,而要實現整個基本mvvm雙向綁定流程,除了observe還須要compile和watche等一系列機制,咱們這裏像模板編譯的工做就不展開描述了,爲了實現基於Proxy的vue添加Totolist,這裏只寫了 compile和watcher來支持observe的工做,具體代碼參考proxyVue,這個代碼至關於一個基於Proxy的一個簡化版vue,主要是實現雙向綁定這個功能,爲了方便這裏把js放到了html頁面中,你們本地運行後能夠發現,如今的效果和第三章的效果達到一致了,等到明年vue3發佈,它源碼裏基於 Proxy實現的的觀察者機制可能和這裏的實現會有不少不一樣,這篇文章主要是對 Proxy這個特性作了一些介紹以及它的一些應用,而做者本人也經過對Proxy 的觀察者機制探索學到了很多東西,因此整合資源,總結出了這篇文章,但願能和你們共勉之,以上,咱們下次有緣再見。

相關文章
相關標籤/搜索