Proxy相比於defineProperty的優勢


本文原鏈接:https://www.jianshu.com/p/860418f0785c

https://blog.csdn.net/sinat_17775997/article/details/83989098

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這部分的代碼(雖然看上去並沒有什么用哈哈)。

一.為什么要取代Object.defineProperty

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

  • Object.defineProperty無法監控到數組下標的變化,導致直接通過數組的下標給數組設置值,不能實時響應。 為了解決這個問題,經過vue內部處理后可以使用以下幾種方法來監聽數組
push()
pop()
shift() unshift() splice() sort() reverse() 

由於只針對了以上八種方法進行了hack處理,所以其他數組的屬性也是檢測不到的,還是具有一定的局限性。

  • Object.defineProperty只能劫持對象的屬性,因此我們需要對每個對象的每個屬性進行遍歷。Vue 2.x里,是通過 遞歸 + 遍歷 data 對象來實現對數據的監控的,如果屬性值也是對象那么需要深度遍歷,顯然如果能劫持一個完整的對象是才是更好的選擇。

而要取代它的Proxy有以下兩個優點;

  • 可以劫持整個對象,並返回一個新對象
  • 有13種劫持操作

看到這可能有同學要問了,既然Proxy能解決以上兩個問題,而且Proxy作為es6的新屬性在vue2.x之前就有了,為什么vue2.x不使用Proxy呢?一個很重要的原因就是:

  • Proxy是es6提供的新特性,兼容性不好,最主要的是這個屬性無法用polyfill來兼容

相信尤大大在vue3.0的版本中會有效的提供兼容解決方案。

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

二.什么是Proxy

1.含義:

  • 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 函數,用來實現「響應式」以及「依賴收集」。類似於下面這個內部流程圖:

 
image

而我們上面已經用Proxy取代了Object.defineProperty這部分觀察者機制,而要實現整個基本mvvm雙向綁定流程,除了observe還需要compile和watche等一系列機制,我們這里像模板編譯的工作就不展開描述了,為了實現基於Proxy的vue添加Totolist,這里只寫了
compile和watcher來支持observe的工作,具體代碼參考proxyVue,這個代碼相當於一個基於Proxy的一個簡化版vue,主要是實現雙向綁定這個功能,為了方便這里把js放到了html頁面中


免責聲明!

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



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