你了解vue3.0響應(yīng)式數(shù)據(jù)怎么實(shí)現(xiàn)嗎
從 Proxy 說起
什么是Proxy
proxy翻譯過來的意思就是”代理“,ES6對(duì)Proxy的定位就是target對(duì)象(原對(duì)象)的基礎(chǔ)上通過handler增加一層”攔截“,返回一個(gè)新的代理對(duì)象,之后所有在Proxy中被攔截的屬性,都可以定制化一些新的流程在上面,先看一個(gè)最簡(jiǎn)單的例子
const target = {}; // 要被代理的原對(duì)象 // 用于描述代理過程的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就是一個(gè)被新的代理對(duì)象 const obj = new Proxy(target, handler); obj.a = 1 // setting a! console.log(obj.a) // getting a!
上面的例子中我們?cè)趖arget對(duì)象上架設(shè)了一層handler,其中攔截了針對(duì)target的get和set,然后我們就可以在get和set中間做一些額外的操作了
注意1:對(duì)Proxy對(duì)象的賦值操作也會(huì)影響到原對(duì)象target,同時(shí)對(duì)target的操作也會(huì)影響Proxy,不過直接操作原對(duì)象的話不會(huì)觸發(fā)攔截的內(nèi)容~
obj.a = 1; // setting a! console.log(target.a) // 1 不會(huì)打印 "getting a!"
注意2:如果handler中沒有任何攔截上的處理,那么對(duì)代理對(duì)象的操作會(huì)直接通向原對(duì)象
const target = {}; const handler = {}; const obj = new Proxy(target, handler); obj.a = 1; console.log(target.a) // 1
既然proxy也是一個(gè)對(duì)象,那么它就可以做為原型對(duì)象,所以我們把obj的原型指向到proxy上后,發(fā)現(xiàn)對(duì)obj的操作會(huì)找到原型上的代理對(duì)象,如果obj自己有a屬性,則不會(huì)觸發(fā)proxy上的get,這個(gè)應(yīng)該很好理解
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實(shí)現(xiàn)了對(duì)哪些屬性的攔截?
通過上面的例子了解了Proxy的原理后,我們來看下ES6目前實(shí)現(xiàn)了哪些屬性的攔截,以及他們分別可以做什么? 下面是 Proxy 支持的攔截操作一覽,一共 13 種
- get(target, propKey, receiver):攔截對(duì)象屬性的讀取,比如proxy.foo和proxy['foo'];
- set(target, propKey, value, receiver):攔截對(duì)象屬性的設(shè)置,比如proxy.foo = v或proxy['foo'] = v,返回一個(gè)布爾值;
- has(target, propKey):攔截propKey in proxy的操作,返回一個(gè)布爾值。
- deleteProperty(target, propKey):攔截delete proxy[propKey]的操作,返回一個(gè)布爾值;
- ownKeys(target):攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循環(huán),返回一個(gè)數(shù)組。該方法返回目標(biāo)對(duì)象所有自身的屬性的屬性名,而Object.keys()的返回結(jié)果僅包括目標(biāo)對(duì)象自身的可遍歷屬性;
- getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對(duì)象;
- defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個(gè)布爾值;
- preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個(gè)布爾值;
- getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個(gè)對(duì)象;
- isExtensible(target):攔截Object.isExtensible(proxy),返回一個(gè)布爾值;
- setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個(gè)布爾值。如果目標(biāo)對(duì)象是函數(shù),那么還有兩種額外操作可以攔截;
- apply(target, object, args):攔截 Proxy 實(shí)例作為函數(shù)調(diào)用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…);
- construct(target, args):攔截 Proxy 實(shí)例作為構(gòu)造函數(shù)調(diào)用的操作,比如new proxy(…args);
以上是目前es6支持的proxy,具體的用法不做贅述,有興趣的可以到阮一峰老師的es6入門去研究每種的具體用法,其實(shí)思想都是一樣的,只是每種對(duì)應(yīng)了一些不同的功能~
實(shí)際場(chǎng)景中 Proxy 可以做什么?
實(shí)現(xiàn)私有變量
js的語法中沒有private這個(gè)關(guān)鍵字來修飾私有變量,所以基本上所有的class的屬性都是可以被訪問的,但是在有些場(chǎng)景下我們需要使用到私有變量,現(xiàn)在業(yè)界的一些做法都是使用”_變量名“來”約定“這是一個(gè)私有變量,但是如果哪天被別人從外部改掉的話,我們還是沒有辦法阻止的,然而,當(dāng)Proxy出現(xiàn)后,我們可以用代理來處理這種場(chǎng)景,看代碼:
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方法,我們可以實(shí)現(xiàn)把任何一個(gè)對(duì)象都過濾一次,然后返回新的代理對(duì)象,被處理的對(duì)象會(huì)把所有_開頭的變量給攔截掉,更進(jìn)一步,如果有用過mobx的同學(xué)會(huì)發(fā)現(xiàn)mobx里面的store中的對(duì)象都是類似于這樣的
有handler 和 target,說明mobx本身也是用了代理模式,同時(shí)加上Decorator函數(shù),在這里就相當(dāng)于把proxyObj使用裝飾器的方式來實(shí)現(xiàn),Proxy + Decorator 就是mobx的核心原理啦~
vue響應(yīng)式數(shù)據(jù)實(shí)現(xiàn)
VUE的雙向綁定涉及到模板編譯,響應(yīng)式數(shù)據(jù),訂閱者模式等等,有興趣的可以看這里 ,因?yàn)檫@篇文章的主題是proxy,因此我們著重介紹一下數(shù)據(jù)響應(yīng)式的過程。
2.x版本
在當(dāng)前的vue2.x的版本中,在data中聲名一個(gè)obj后,vue會(huì)利用Object.defineProperty來遞歸的給data中的數(shù)據(jù)加上get和set,然后每次set的時(shí)候,加入額外的邏輯。來觸發(fā)對(duì)應(yīng)模板視圖的更新,看下偽代碼:
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() // 通知相關(guān)的模板進(jìn)行編譯 value = newValue; }, enumerable : true, configurable : true }) }) }
這個(gè)方法可以給data上面的所有屬性都加上get和set,當(dāng)然這只是偽代碼,實(shí)際場(chǎng)景下我們還需要考慮如果某個(gè)屬性還是對(duì)象我們應(yīng)該遞歸下去,來試試:
const data = { name: 'nanjing', age: 19 } defineReactiveData(data) data.name // getting name 'nanjing' data.name = 'beijing'; // setting name
可以看到當(dāng)我們get和set觸發(fā)的時(shí)候,已經(jīng)能夠同時(shí)觸發(fā)我們想要調(diào)用的函數(shù)拉,Vue雙向綁定過程中,當(dāng)改變this上的data的時(shí)候去更新模板的核心原理就是這個(gè)方法,通過它我們就能在data的某個(gè)屬性被set的時(shí)候,去觸發(fā)對(duì)應(yīng)模板的更新。
現(xiàn)在我們?cè)趤碓囋囅旅娴拇a:
const data = { userIds: ['01','02','03','04','05'] } defineReactiveData(data); data.userIds // getting userIds ["01", "02", "03", "04", "05"] // get 過程是沒有問題的,現(xiàn)在我們嘗試給數(shù)組中push一個(gè)數(shù)據(jù) data.userIds.push('06') // getting userIds
what ? setting沒有被觸發(fā),反而因?yàn)槿×艘淮蝩serIds所以觸發(fā)了一次getting~,
不僅如此,很多數(shù)組的方法都不會(huì)觸發(fā)setting,比如:push,pop,shift,unshift,splice,sort,reverse這些方法都會(huì)改變數(shù)組,但是不會(huì)觸發(fā)set,所以Vue為了解決這個(gè)問題,重新包裝了這些函數(shù),同時(shí)當(dāng)這些方法被調(diào)用的時(shí)候,手動(dòng)去觸發(fā)notify();看下源碼:
// 獲得數(shù)組原型 const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) // 重寫以下函數(shù) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', ] methodsToPatch.forEach(function(method) { // 緩存原生函數(shù) const original = arrayProto[method] // 重寫函數(shù) def(arrayMethods, method, function mutator(...args) { // 先調(diào)用原生函數(shù)獲得結(jié)果 const result = original.apply(this, args) const ob = this.__ob__ let inserted // 調(diào)用以下幾個(gè)函數(shù)時(shí),監(jiān)聽新數(shù)據(jù) switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // 手動(dòng)派發(fā)更新 ob.dep.notify() return result }) })
上面是官方的源碼,我們可以實(shí)現(xiàn)一下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
通過這種方式,我們可以監(jiān)聽到這些的變化,但是vue官方文檔中有這么一個(gè)注意事項(xiàng)
由于 JavaScript 的限制,Vue 不能檢測(cè)以下變動(dòng)的數(shù)組:
- 當(dāng)你利用索引直接設(shè)置一個(gè)項(xiàng)時(shí),例如:vm.items[indexOfItem] = newValue
- 當(dāng)你修改數(shù)組的長(zhǎng)度時(shí),例如:vm.items.length = newLength
這個(gè)最根本的原因是因?yàn)檫@2種情況下,受制于js本身無法實(shí)現(xiàn)監(jiān)聽,所以官方建議用他們自己提供的內(nèi)置api來實(shí)現(xiàn),我們也可以理解到這里既不是defineProperty可以處理的,也不是包一層函數(shù)就能解決的,這就是2.x版本現(xiàn)在的一個(gè)問。 回到這篇文章的主題,vue官方會(huì)在3.x的版本中使用proxy來代替defineProperty處理響應(yīng)式數(shù)據(jù)的過程,我們先來模擬一下實(shí)現(xiàn),看看能否解決當(dāng)前遇到的這些問題;
3.x版本
我們先來通過proxy實(shí)現(xiàn)對(duì)data對(duì)象的get和set的劫持,并返回一個(gè)代理的對(duì)象,注意,我們只關(guān)注proxy本身,所有的實(shí)現(xiàn)都是偽代碼,有興趣的同學(xué)可以自行完善
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,遞歸設(shè)置代理 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
看起來我們的代理已經(jīng)起作用啦,之后只要在setting的時(shí)候加上notify()去通知模板進(jìn)行編譯就可以了,然后我們來嘗試設(shè)置一個(gè)數(shù)組看看;
vm.userIds = [1,2,3] // setting userIds vm.userIds.push(1); // getting userIds 因?yàn)槲覀儠?huì)先訪問一次userids // getting push 調(diào)用了push方法,所以會(huì)訪問一次push屬性 // getting length 數(shù)組push的時(shí)候 length會(huì)變,所以需要先訪問原來的length // setting 3 通過下標(biāo)設(shè)置的,所以set當(dāng)前的index是3 // setting length 改變了數(shù)組的長(zhǎng)度,所以會(huì)set length // 4 返回新的數(shù)組的長(zhǎng)度
回顧2.x遇到的第一個(gè)問題,需要重新包裝Array.prototype上的一些方法,使用了proxy后不需要了,解決了~,繼續(xù)看下一個(gè)問題
vm.userIds.length = 2 // getting userIds 先訪問 // setting length 在設(shè)置 vm.userIds[1] = '123' // getting userIds 先訪問 // setting 1 設(shè)置index=1的item // "123"
從上面的例子中我們可以看到,不管是直接改變數(shù)組的length還是通過某一個(gè)下標(biāo)改變數(shù)組的內(nèi)容,proxy都能攔截到這次變化,這比defineProperty方便太多了,2.x版本中的第二個(gè)問題,在proxy中根本不會(huì)出現(xiàn)了。
總結(jié)1
通過上面的例子和代碼,我們看到Vue的響應(yīng)模式如果使用proxy會(huì)比現(xiàn)在的實(shí)現(xiàn)方式要簡(jiǎn)化和優(yōu)化很多,很快在即將來臨的3.0版本中,大家就可以體驗(yàn)到了。不過因?yàn)閜roxy本身是有兼容性的,比如ie瀏覽器,所以在低版本的場(chǎng)景下,vue會(huì)回退到現(xiàn)在的實(shí)現(xiàn)方式。
總結(jié)2
回歸到proxy本身,設(shè)計(jì)模式中有一種典型的代理模式,proxy就是js的一種實(shí)現(xiàn),它的好處在于,我可以在不污染本身對(duì)象的條件下,生成一個(gè)新的代理對(duì)象,所有的一些針對(duì)性邏輯放到代理對(duì)象上去實(shí)現(xiàn),這樣我可以由A對(duì)象,衍生出B,C,D…每個(gè)的處理過程都不一樣,從而簡(jiǎn)化代碼的復(fù)雜性,提升一定的可讀性,比如用proxy實(shí)現(xiàn)數(shù)據(jù)庫(kù)的ORM就是一種很好的應(yīng)用,其實(shí)代碼很簡(jiǎn)單,關(guān)鍵是要理解背后的思想,同時(shí)能夠舉一反三~
擴(kuò)展: 1.Proxy.revocable()
這個(gè)方法可以返回一個(gè)可取消的代理對(duì)象
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
一旦代理被取消了,就不能再?gòu)拇韺?duì)象訪問了
打印proxy 可以看到IsRevoked變?yōu)閠rue了
2.代理對(duì)象的this問題
因?yàn)閚ew Proxy出來的是一個(gè)新的對(duì)象,所以在如果你在target中有使用this,被代理后的this將指向新的代理對(duì)象,而不是原來的對(duì)象,這個(gè)時(shí)候,如果有些函數(shù)是原對(duì)象獨(dú)有的,就會(huì)出現(xiàn)this指向?qū)е碌膯栴},這種場(chǎng)景下,建議使用bind來強(qiáng)制綁定this
看代碼:
const target = new Date(); const handler = {}; const proxy = new Proxy(target, handler); proxy.getDate(); // Uncaught TypeError: this is not a Date object.
因?yàn)榇砗蟮膶?duì)象并不是一個(gè)Date類型的,不具有g(shù)etDate方法的,所以我們需要在get的時(shí)候,綁定一下this的指向
const target = new Date(); const handler = { get: function(target, key){ if(typeof target[key] === 'function'){ return target[key].bind(target) // 強(qiáng)制綁定 this到原對(duì)象 } return Reflect.get(target, key) } }; const proxy = new Proxy(target, handler); proxy.getDate(); // 6
這樣就可以正常使用this啦,當(dāng)然具體的使用還要看具體的場(chǎng)景,靈活運(yùn)用吧!
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
VSCode Vue開發(fā)推薦插件和VSCode快捷鍵(小結(jié))
這篇文章主要介紹了VSCode Vue開發(fā)推薦插件和VSCode快捷鍵(小結(jié)),文中通過圖文表格介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08Vue-Cli配置代理轉(zhuǎn)發(fā)解決跨域問題的方法
本文主要介紹了Vue-Cli配置代理轉(zhuǎn)發(fā)解決跨域問題的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06vue項(xiàng)目查看vue版本及cli版本的實(shí)現(xiàn)方式
這篇文章主要介紹了vue項(xiàng)目查看vue版本及cli版本的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-10-10解決ElementUI中tooltip出現(xiàn)無法顯示的問題
這篇文章主要介紹了解決ElementUI中tooltip出現(xiàn)無法顯示的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03element-ui使用el-date-picker日期組件常見場(chǎng)景分析
最近一直在使用 element-ui中的日期組件,所以想對(duì)日期組件常用的做一個(gè)簡(jiǎn)單的總結(jié),對(duì)element-ui el-date-picker日期組件使用場(chǎng)景分析感興趣的朋友一起看看吧2024-05-05