您好,登錄后才能下訂單哦!
從 Proxy 說起
什么是Proxy
proxy翻譯過來的意思就是”代理“,ES6對Proxy的定位就是target對象(原對象)的基礎上通過handler增加一層”攔截“,返回一個新的代理對象,之后所有在Proxy中被攔截的屬性,都可以定制化一些新的流程在上面,先看一個最簡單的例子
const target = {}; // 要被代理的原對象 // 用于描述代理過程的handler const handler = { get: function (target, key, receiver) { console.log(`getting ${key}!`); return Reflect.get(target, key, receiver); }, set: function (target, key, value, receiver) { console.log(`setting ${key}!`); return Reflect.set(target, key, value, receiver); } } // obj就是一個被新的代理對象 const obj = new Proxy(target, handler); obj.a = 1 // setting a! console.log(obj.a) // getting a!
上面的例子中我們在target對象上架設了一層handler,其中攔截了針對target的get和set,然后我們就可以在get和set中間做一些額外的操作了
注意1:對Proxy對象的賦值操作也會影響到原對象target,同時對target的操作也會影響Proxy,不過直接操作原對象的話不會觸發攔截的內容~
obj.a = 1; // setting a! console.log(target.a) // 1 不會打印 "getting a!"
注意2:如果handler中沒有任何攔截上的處理,那么對代理對象的操作會直接通向原對象
const target = {}; const handler = {}; const obj = new Proxy(target, handler); obj.a = 1; console.log(target.a) // 1
既然proxy也是一個對象,那么它就可以做為原型對象,所以我們把obj的原型指向到proxy上后,發現對obj的操作會找到原型上的代理對象,如果obj自己有a屬性,則不會觸發proxy上的get,這個應該很好理解
const target = {}; const obj = {}; const handler = { get: function(target, key){ console.log(`get ${key} from ${JSON.stringify(target)}`); return Reflect.get(target, key); } } const proxy = new Proxy(target, handler); Object.setPrototypeOf(obj, proxy); proxy.a = 1; obj.b = 1 console.log(obj.a) // get a from {"a": 1} 1 console.log(obj.b) // 1
ES6的Proxy實現了對哪些屬性的攔截?
通過上面的例子了解了Proxy的原理后,我們來看下ES6目前實現了哪些屬性的攔截,以及他們分別可以做什么? 下面是 Proxy 支持的攔截操作一覽,一共 13 種
以上是目前es6支持的proxy,具體的用法不做贅述,有興趣的可以到阮一峰老師的es6入門去研究每種的具體用法,其實思想都是一樣的,只是每種對應了一些不同的功能~
實際場景中 Proxy 可以做什么?
實現私有變量
js的語法中沒有private這個關鍵字來修飾私有變量,所以基本上所有的class的屬性都是可以被訪問的,但是在有些場景下我們需要使用到私有變量,現在業界的一些做法都是使用”_變量名“來”約定“這是一個私有變量,但是如果哪天被別人從外部改掉的話,我們還是沒有辦法阻止的,然而,當Proxy出現后,我們可以用代理來處理這種場景,看代碼:
const obj = { _name: 'nanjin', age: 19, getName: () => { return this._name; }, setName: (newName) => { this._name = newName; } } const proxyObj = obj => new Proxy(obj, { get: (target, key) => { if(key.startsWith('_')){ throw new Error(`${key} is private key, please use get${key}`) } return Reflect.get(target, key); }, set: (target, key, newVal) => { if(key.startsWith('_')){ throw new Error(`${key} is private key, please use set${key}`) } return Reflect.set(target, key, newVal); } }) const newObj = proxyObj(obj); console.log(newObj._name) // Uncaught Error: _name is private key, please use get_name newObj._name = 'newname'; // Uncaught Error: _name is private key, please use set_name console.log(newObj.age) // 19 console.log(newObj.getName()) // nanjin
可見,通過proxyObj方法,我們可以實現把任何一個對象都過濾一次,然后返回新的代理對象,被處理的對象會把所有_開頭的變量給攔截掉,更進一步,如果有用過mobx的同學會發現mobx里面的store中的對象都是類似于這樣的
有handler 和 target,說明mobx本身也是用了代理模式,同時加上Decorator函數,在這里就相當于把proxyObj使用裝飾器的方式來實現,Proxy + Decorator 就是mobx的核心原理啦~
vue響應式數據實現
VUE的雙向綁定涉及到模板編譯,響應式數據,訂閱者模式等等,有興趣的可以看這里 ,因為這篇文章的主題是proxy,因此我們著重介紹一下數據響應式的過程。
2.x版本
在當前的vue2.x的版本中,在data中聲名一個obj后,vue會利用Object.defineProperty來遞歸的給data中的數據加上get和set,然后每次set的時候,加入額外的邏輯。來觸發對應模板視圖的更新,看下偽代碼:
const defineReactiveData = data => { Object.keys(data).forEach(key => { let value = data[key]; Object.defineProperty(data, key, { get : function(){ console.log(`getting ${key}`) return value; }, set : function(newValue){ console.log(`setting ${key}`) notify() // 通知相關的模板進行編譯 value = newValue; }, enumerable : true, configurable : true }) }) }
這個方法可以給data上面的所有屬性都加上get和set,當然這只是偽代碼,實際場景下我們還需要考慮如果某個屬性還是對象我們應該遞歸下去,來試試:
const data = { name: 'nanjing', age: 19 } defineReactiveData(data) data.name // getting name 'nanjing' data.name = 'beijing'; // setting name
可以看到當我們get和set觸發的時候,已經能夠同時觸發我們想要調用的函數拉,Vue雙向綁定過程中,當改變this上的data的時候去更新模板的核心原理就是這個方法,通過它我們就能在data的某個屬性被set的時候,去觸發對應模板的更新。
現在我們在來試試下面的代碼:
const data = { userIds: ['01','02','03','04','05'] } defineReactiveData(data); data.userIds // getting userIds ["01", "02", "03", "04", "05"] // get 過程是沒有問題的,現在我們嘗試給數組中push一個數據 data.userIds.push('06') // getting userIds
what ? setting沒有被觸發,反而因為取了一次userIds所以觸發了一次getting~,
不僅如此,很多數組的方法都不會觸發setting,比如:push,pop,shift,unshift,splice,sort,reverse這些方法都會改變數組,但是不會觸發set,所以Vue為了解決這個問題,重新包裝了這些函數,同時當這些方法被調用的時候,手動去觸發notify();看下源碼:
// 獲得數組原型 const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) // 重寫以下函數 const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', ] methodsToPatch.forEach(function(method) { // 緩存原生函數 const original = arrayProto[method] // 重寫函數 def(arrayMethods, method, function mutator(...args) { // 先調用原生函數獲得結果 const result = original.apply(this, args) const ob = this.__ob__ let inserted // 調用以下幾個函數時,監聽新數據 switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // 手動派發更新 ob.dep.notify() return result }) })
上面是官方的源碼,我們可以實現一下push的偽代碼,為了省事,直接在prototype上下手了~
const push = Array.prototype.push; Array.prototype.push = function(...args){ console.log('push is happenning'); return push.apply(this, args); } data.userIds.push('123') // push is happenning
通過這種方式,我們可以監聽到這些的變化,但是vue官方文檔中有這么一個注意事項
由于 JavaScript 的限制,Vue 不能檢測以下變動的數組:
這個最根本的原因是因為這2種情況下,受制于js本身無法實現監聽,所以官方建議用他們自己提供的內置api來實現,我們也可以理解到這里既不是defineProperty可以處理的,也不是包一層函數就能解決的,這就是2.x版本現在的一個問。 回到這篇文章的主題,vue官方會在3.x的版本中使用proxy來代替defineProperty處理響應式數據的過程,我們先來模擬一下實現,看看能否解決當前遇到的這些問題;
3.x版本
我們先來通過proxy實現對data對象的get和set的劫持,并返回一個代理的對象,注意,我們只關注proxy本身,所有的實現都是偽代碼,有興趣的同學可以自行完善
const defineReactiveProxyData = data => new Proxy(data, { get: function(data, key){ console.log(`getting ${key}`) return Reflect.get(data, key); }, set: function(data, key, newVal){ console.log(`setting ${key}`); if(typeof newVal === 'object'){ // 如果是object,遞歸設置代理 return Reflect.set(data, key, defineReactiveProxyData(newVal)); } return Reflect.set(data, key, newVal); } }) const data = { name: 'nanjing', age: 19 }; const vm = defineReactiveProxyData(data); vm.name // getting name nanjing vm.age = 20; // setting age 20
看起來我們的代理已經起作用啦,之后只要在setting的時候加上notify()去通知模板進行編譯就可以了,然后我們來嘗試設置一個數組看看;
vm.userIds = [1,2,3] // setting userIds vm.userIds.push(1); // getting userIds 因為我們會先訪問一次userids // getting push 調用了push方法,所以會訪問一次push屬性 // getting length 數組push的時候 length會變,所以需要先訪問原來的length // setting 3 通過下標設置的,所以set當前的index是3 // setting length 改變了數組的長度,所以會set length // 4 返回新的數組的長度
回顧2.x遇到的第一個問題,需要重新包裝Array.prototype上的一些方法,使用了proxy后不需要了,解決了~,繼續看下一個問題
vm.userIds.length = 2 // getting userIds 先訪問 // setting length 在設置 vm.userIds[1] = '123' // getting userIds 先訪問 // setting 1 設置index=1的item // "123"
從上面的例子中我們可以看到,不管是直接改變數組的length還是通過某一個下標改變數組的內容,proxy都能攔截到這次變化,這比defineProperty方便太多了,2.x版本中的第二個問題,在proxy中根本不會出現了。
總結1
通過上面的例子和代碼,我們看到Vue的響應模式如果使用proxy會比現在的實現方式要簡化和優化很多,很快在即將來臨的3.0版本中,大家就可以體驗到了。不過因為proxy本身是有兼容性的,比如ie瀏覽器,所以在低版本的場景下,vue會回退到現在的實現方式。
總結2
回歸到proxy本身,設計模式中有一種典型的代理模式,proxy就是js的一種實現,它的好處在于,我可以在不污染本身對象的條件下,生成一個新的代理對象,所有的一些針對性邏輯放到代理對象上去實現,這樣我可以由A對象,衍生出B,C,D…每個的處理過程都不一樣,從而簡化代碼的復雜性,提升一定的可讀性,比如用proxy實現數據庫的ORM就是一種很好的應用,其實代碼很簡單,關鍵是要理解背后的思想,同時能夠舉一反三~
擴展: 1.Proxy.revocable()
這個方法可以返回一個可取消的代理對象
const obj = {}; const handler = {}; const {proxy, revoke} = Proxy.revocable(obj, handler); proxy.a = 1 proxy.a // 1 revoke(); proxy.a // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
一旦代理被取消了,就不能再從代理對象訪問了
打印proxy 可以看到IsRevoked變為true了
2.代理對象的this問題
因為new Proxy出來的是一個新的對象,所以在如果你在target中有使用this,被代理后的this將指向新的代理對象,而不是原來的對象,這個時候,如果有些函數是原對象獨有的,就會出現this指向導致的問題,這種場景下,建議使用bind來強制綁定this
看代碼:
const target = new Date(); const handler = {}; const proxy = new Proxy(target, handler); proxy.getDate(); // Uncaught TypeError: this is not a Date object.
因為代理后的對象并不是一個Date類型的,不具有getDate方法的,所以我們需要在get的時候,綁定一下this的指向
const target = new Date(); const handler = { get: function(target, key){ if(typeof target[key] === 'function'){ return target[key].bind(target) // 強制綁定 this到原對象 } return Reflect.get(target, key) } }; const proxy = new Proxy(target, handler); proxy.getDate(); // 6
這樣就可以正常使用this啦,當然具體的使用還要看具體的場景,靈活運用吧!
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。