茶余飯后聊聊Vue3.0響應(yīng)式數(shù)據(jù)那些事兒
"別再更新了,實(shí)在是學(xué)不動(dòng)了"這句話(huà)道出了多少前端開(kāi)發(fā)者的心聲,"不幸"的是 Vue 的作者在國(guó)慶區(qū)間發(fā)布了 Vue3.0 的 pre-Aplha 版本,這意味著 Vue3.0 快要和我們見(jiàn)面了。既來(lái)之則安之,扶我起來(lái)我要開(kāi)始講了。Vue3.0 為了達(dá)到更快、更小、更易于維護(hù)、更貼近原生、對(duì)開(kāi)發(fā)者更友好的目的,在很多方面進(jìn)行了重構(gòu):
- 使用 Typescript
- 放棄 class 采用 function-based API
- 重構(gòu) complier
- 重構(gòu) virtual DOM
- 新的響應(yīng)式機(jī)制
今天咱就聊聊重構(gòu)后的響應(yīng)式數(shù)據(jù)。
嘗鮮
重構(gòu)后的 Vue3.0 和之前在寫(xiě)法上有很大的差別,早前在網(wǎng)絡(luò)上對(duì)于 Vue3.0 這種激進(jìn)式的重構(gòu)方式發(fā)起了一場(chǎng)討論,見(jiàn)仁見(jiàn)智。不多說(shuō)先看看 Vue3.0 在寫(xiě)法上激進(jìn)到什么程度。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="../packages/vue/dist/vue.global.js"></script> </head> <body> <div id="app"></div> <script> const { reactive, computed, effect, createApp } = Vue const App = { template: ` <div id="box"> <button @click="add">{{ state.count }}</button> </div> `, setup() { const state = reactive({ count: 0 }) function add() { state.count++ } effect(() => { console.log('count改變', state.count); }) return { state, add } } } createApp().mount(App, '#app') </script> </body> </html>
確實(shí)寫(xiě)法上和 Vue2.x 差別有點(diǎn)大,還整出了個(gè) setup。不過(guò)我的第一感覺(jué)倒不是寫(xiě)法上的差異,畢竟寫(xiě)過(guò) React,這種寫(xiě)法也沒(méi)啥特別的。關(guān)鍵是這種響應(yīng)式數(shù)據(jù)的寫(xiě)法好像在哪里見(jiàn)過(guò)有沒(méi)有?寫(xiě)過(guò) React 項(xiàng)目的人可能一眼就能看出來(lái),沒(méi)錯(cuò)就是它 mobx,一種 React 的響應(yīng)式狀態(tài)管理插件
import {observable,computed,autorun} from "mobx" var numbers = observable([1,2,3]); var sum = computed(() => numbers.reduce((a, b) => a + b, 0)); var disposer = autorun(() => console.log(sum.get())); // 輸出 '6' numbers.push(4); // 輸出 '10' numbers.push(5);
再看看 Vue3.0 暴露的這幾個(gè)和響應(yīng)式數(shù)據(jù)相關(guān)的方法:
reactive(value)
創(chuàng)建可觀察的變量,參數(shù)可以是 JS 原始類(lèi)型、引用、純對(duì)象、類(lèi)實(shí)例、數(shù)組、集合(Map|Set)。
effect(fn)
effect 意思是副作用,此方法默認(rèn)會(huì)先執(zhí)行一次。如果 fn 中有依賴(lài)的可觀察屬性變化時(shí),會(huì)再次觸發(fā)此回調(diào)函數(shù)
computed(()=>expression)
創(chuàng)建一個(gè)計(jì)算值,computed 實(shí)現(xiàn)也是基于 effect 來(lái)實(shí)現(xiàn)的,特點(diǎn)是 computed 中的函數(shù)不會(huì)立即執(zhí)行,多次取值是有緩存機(jī)制的,expression 不應(yīng)該有任何副作用,而僅僅是返回一個(gè)值。當(dāng)這個(gè) expression 依賴(lài)的可觀察屬性變化時(shí),這個(gè)表達(dá)式會(huì)重新計(jì)算。
和 mobx 有異曲同工之妙。
Vue3.0 把創(chuàng)建響應(yīng)式對(duì)象從組件實(shí)例初始化中抽離了出來(lái),通過(guò)暴露 API 的方式將響應(yīng)式對(duì)象創(chuàng)建的權(quán)利交給開(kāi)發(fā)者,開(kāi)發(fā)者可以自由的決定何時(shí)何地創(chuàng)建響應(yīng)式對(duì)象,就沖這點(diǎn) Vue3.0 我先粉了。
重構(gòu)后的響應(yīng)式機(jī)制帶來(lái)了哪些改變?
每一個(gè)大版本的發(fā)布都意味著新功能、新特性的出現(xiàn),那么重構(gòu)后的響應(yīng)式數(shù)據(jù)部分相比 3.0 之前的版本有了哪些方面的改變呢?下面聽(tīng)我娓娓道來(lái):
對(duì)數(shù)組的全面監(jiān)聽(tīng)
Vue2.x 中被大家吐槽的最多的一點(diǎn)就是針對(duì)數(shù)組只實(shí)現(xiàn)了 push,pop,shift,unshift,splice,sort,reverse' 這七個(gè)方法的監(jiān)聽(tīng),以前通過(guò)數(shù)組下標(biāo)改變值的時(shí)候,是不能觸發(fā)視圖更新的。這里插一個(gè)題外話(huà),很多人認(rèn)為 Vue2.x 中數(shù)組不能實(shí)現(xiàn)全方位監(jiān)聽(tīng)是 Object.defineProperty 不能監(jiān)聽(tīng)數(shù)組下標(biāo)的改變,這可就冤枉人家了,人家也能偵聽(tīng)數(shù)組下標(biāo)變化的好嗎,不信你看
const arr = ["2019","云","棲","音","樂(lè)","節(jié)"]; arr.forEach((val,index)=>{ Object.defineProperty(arr,index,{ set(newVal){ console.log("賦值"); }, get(){ console.log("取值"); return val; } }) }) let index = arr[1]; //取值 arr[0] = "2050"; //賦值
沒(méi)毛病,一切變化都在人家的掌握中。上面這段代碼,有沒(méi)有人沒(méi)看懂,我假裝你們都不懂,貼張圖
從數(shù)組的數(shù)據(jù)結(jié)構(gòu)來(lái)看,數(shù)組也是一個(gè) Key-Value 的鍵值對(duì)集合,只是 Key 是數(shù)字罷了,自然也可以通過(guò)Object.defineProperty 來(lái)實(shí)現(xiàn)數(shù)組的下標(biāo)訪(fǎng)問(wèn)和賦值攔截了。其實(shí) Vue2.x 沒(méi)有實(shí)現(xiàn)數(shù)組的全方位監(jiān)聽(tīng)主要有兩方面原因:
- 數(shù)組和普通對(duì)象相比,JS 數(shù)組太"多變"了。比如:arr.length=0,可以瞬間清空一個(gè)數(shù)組;arr[100]=1又可以瞬間將一個(gè)數(shù)組的長(zhǎng)度變?yōu)?100(其他位置用空元素填充),等等騷操作。對(duì)于一個(gè)普通對(duì)象,我們一般只會(huì)改變 Key 對(duì)應(yīng)的 Value 值,而不會(huì)連key都改變了,而數(shù)組就不一樣了 Key 和 Value 都經(jīng)常增加或減少,因此每次變化后我們都需要重新將整個(gè)數(shù)組的所有 key 遞歸的使用 Object.defineProperty 加上 setter 和 getter,同時(shí)我們還要窮舉每一種數(shù)組變化的可能,這樣勢(shì)必就會(huì)帶來(lái)性能開(kāi)銷(xiāo)問(wèn)題,有的人會(huì)覺(jué)得這點(diǎn)性能開(kāi)銷(xiāo)算個(gè) x 呀,但是性能問(wèn)題都是由小變大的,如果數(shù)組中存的數(shù)據(jù)量大而且操作頻繁時(shí),這就會(huì)是一個(gè)大問(wèn)題。React16.x 在就因?yàn)樵趦?yōu)化 textNode 的時(shí)候,移除了無(wú)意義的 span 標(biāo)簽,性能據(jù)說(shuō)都提升了多少個(gè)百分點(diǎn),所以性能問(wèn)題不可小看。
- 數(shù)組在應(yīng)用中經(jīng)常會(huì)被操作,但是通常 push,pop,shift,unshift,splice,sort,reverse 這 7 種操作就能達(dá)到目的。因此,出于性能方面的考慮 Vue2.x 做出了一定的取舍。
那么 Vue3.0 怎么又走回頭路去實(shí)現(xiàn)了數(shù)組的全面監(jiān)聽(tīng)了呢?答案就是 Proxy 和 Reflet 這一對(duì)原生 CP 的出現(xiàn),Vue3.0 使用 Proxy 作為響應(yīng)式數(shù)據(jù)實(shí)現(xiàn)的核心,用 Proxy 返回一個(gè)代理對(duì)象,通過(guò)代理對(duì)象來(lái)收集依賴(lài)和觸發(fā)更新。大概的原理像這段代碼一樣:
const arr = ["2019","云","棲","音","樂(lè)","節(jié)"]; let ProxyArray = new Proxy(arr,{ get:function(target, name, value, receiver) { console.log("取值") return Reflect.get(target,name); }, set: function(target, name, value, receiver) { console.log("賦值") Reflect.set(target,name, value, receiver);; } }) const index = ProxyArray[0]; //取值 ProxyArray[0]="2050" //賦值
效果和 Object.defineProperty 一樣一樣的,又顯得清新脫俗有沒(méi)有?而且 Proxy 只要是對(duì)象都能代理,后面還會(huì)提到。當(dāng)然 Vue3.0 是雖然有了新歡,但也沒(méi)忘記舊愛(ài),對(duì)于在之前版本中數(shù)組的幾種方法的監(jiān)聽(tīng)還是照樣支持的。
惰性監(jiān)聽(tīng)
什么是"惰性監(jiān)聽(tīng)"?
簡(jiǎn)單講就是"偷懶",開(kāi)發(fā)者可以選擇性的生成可觀察對(duì)象。在平時(shí)的開(kāi)發(fā)中常有這樣的場(chǎng)景,一些頁(yè)面上的數(shù)據(jù)在頁(yè)面的整個(gè)生命周期中是不會(huì)變化的,這時(shí)這部分?jǐn)?shù)據(jù)不需要具備響應(yīng)式能力,這在 Vue3.0 以前是沒(méi)有選擇余地的,所有在模板中使用到的數(shù)據(jù)都需要在 data 中定義,組件實(shí)例在初始化的時(shí)候會(huì)將 data 整個(gè)對(duì)象變?yōu)榭捎^察對(duì)象。
惰性監(jiān)聽(tīng)有什么好處?
提高了組件實(shí)例初始化速度
Vue3.0 以前組件實(shí)例在初始化的時(shí)候會(huì)將 data 整個(gè)對(duì)象變?yōu)榭捎^察對(duì)象,通過(guò)遞歸的方式給每個(gè) Key 使用Object.defineProperty 加上 getter 和 settter,如果是數(shù)組就重寫(xiě)代理數(shù)組對(duì)象的七個(gè)方法。而在 Vue3.0 中,將可響應(yīng)式對(duì)象創(chuàng)建的權(quán)利交給了開(kāi)發(fā)者,開(kāi)發(fā)者可以通過(guò)暴露的 reactive, compted, effect 方法自定義自己需要響應(yīng)式能力的數(shù)據(jù),實(shí)例在初始化時(shí)不需要再去遞歸 data 對(duì)象了,從而降低了組件實(shí)例化的時(shí)間。
降低了運(yùn)行內(nèi)存的使用
Vue3.0 以前生成響應(yīng)式對(duì)象會(huì)對(duì)對(duì)象進(jìn)行深度遍歷,同時(shí)為每個(gè) Key 生成一個(gè) def 對(duì)象用來(lái)保存 Key 的所有依賴(lài)項(xiàng),當(dāng) Key 對(duì)應(yīng)的 Value 變化的時(shí)候通知依賴(lài)項(xiàng)進(jìn)行 update。但如果這些依賴(lài)項(xiàng)在頁(yè)面整個(gè)生命周期內(nèi)不需要更新的時(shí)候,這時(shí) def 對(duì)象收集的依賴(lài)項(xiàng)不僅沒(méi)用而且還會(huì)占用內(nèi)存,如果可以在初始化 data 的時(shí)候忽略掉這些不會(huì)變化的值就好了。Vue3.0 通過(guò)暴露的 reactive 方法,開(kāi)發(fā)者可以選擇性的創(chuàng)建可觀察對(duì)象,達(dá)到減少依賴(lài)項(xiàng)的保存,降低了運(yùn)行內(nèi)存的使用。
Map、Set、WeakSet、WeakMap的監(jiān)聽(tīng)
前面提到 Proxy 可以代理所有的對(duì)象,立馬聯(lián)想到了 ES6 里面新增的集合 Map、Set, 聚合類(lèi)型的支持得益于 Proxy 和 Reflect。講真的這之前還真不知道 Proxy 這么剛啥都能代理,二話(huà)不說(shuō)直接動(dòng)手用 Proxy 代理了一個(gè) map 試試水
let map = new Map([["name","zhengcaiyun"]]) let mapProxy = new Proxy(map, { get(target, key, receiver) { console.log("取值:",key) return Reflect.get(target, key, receiver) } }) mapProxy.get("name")
Uncaught TypeError: Method Map.prototype.get called on incompatible receiver [object Object]
一盆涼水潑來(lái),報(bào)錯(cuò)了。原來(lái) Map、Set 對(duì)象賦值、取值和他們內(nèi)部的 this 指向有關(guān)系,但這里的 this 指向的是其實(shí)是 Proxy 對(duì)象,所以得這樣干
let map = new Map([['name','wangyangyang']]) let mapProxy = new Proxy(map, { get(target, key, receiver) { var value = Reflect.get(...arguments) console.log("取值:",...arguments) return typeof value == 'function' ? value.bind(target) : value } }) mapProxy.get("name")
當(dāng)獲取的是一個(gè)函數(shù)的時(shí)候,通過(guò)作用域綁定的方式將原對(duì)象綁定到 Map、Set 對(duì)象上就好了。
Vue3.0 是如何實(shí)現(xiàn)集合類(lèi)型數(shù)據(jù)監(jiān)聽(tīng)的?
眼尖的同學(xué)看完上面這段代碼會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題,集合是沒(méi)有 set 方法,集合賦值用的是 add 操作,那咋辦呢?來(lái)看看那么 Vue3.0 是怎么處理的,上一段簡(jiǎn)化后的源碼
function reactive(target: object) { return createReactiveObject( target, rawToReactive, reactiveToRaw, mutableHandlers, mutableCollectionHandlers ) } function createReactiveObject( target: any, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) { //collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet]) const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers //生成代理對(duì)象 observed = new Proxy(target, handlers) toProxy.set(target, observed) toRaw.set(observed, target) if (!targetMap.has(target)) { targetMap.set(target, new Map()) } return observed }
根據(jù) target 類(lèi)型適配不同的 handler,如果是集合 (Map、Set)就使用 collectionHandlers,是其他類(lèi)型就使用 baseHandlers。接下來(lái)看看 collectionHandlers
export const mutableCollectionHandlers: ProxyHandler<any> = { get: createInstrumentationGetter(mutableInstrumentations) } export const readonlyCollectionHandlers: ProxyHandler<any> = { get: createInstrumentationGetter(readonlyInstrumentations) }
沒(méi)有意外只有 get,騷就騷在這兒:
// 可變數(shù)據(jù)插樁對(duì)象,以及一系列相應(yīng)的插樁方法 const mutableInstrumentations: any = { get(key: any) { return get(this, key, toReactive) }, get size() { return size(this) }, has, add, set, delete: deleteEntry, clear, forEach: createForEach(false) } // 迭代器相關(guān)的方法 const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator] iteratorMethods.forEach(method => { mutableInstrumentations[method] = createIterableMethod(method, false) readonlyInstrumentations[method] = createIterableMethod(method, true) }) // 創(chuàng)建getter的函數(shù) function createInstrumentationGetter(instrumentations: any) { return function getInstrumented( target: any, key: string | symbol, receiver: any ) { target = hasOwn(instrumentations, key) && key in target ? instrumentations : target return Reflect.get(target, key, receiver) } }
由于 Proxy 的 traps 跟 Map|Set 集合的原生方法不一致,因此無(wú)法通過(guò) Proxy 劫持 set,所以作者在在這里進(jìn)行了"偷梁換柱",這里新創(chuàng)建了一個(gè)和集合對(duì)象具有相同屬性和方法的普通對(duì)象,在集合對(duì)象 get 操作時(shí)將 target 對(duì)象換成新創(chuàng)建的普通對(duì)象。這樣,當(dāng)調(diào)用 get 操作時(shí) Reflect 反射到這個(gè)新對(duì)象上,當(dāng)調(diào)用 set 方法時(shí)就直接調(diào)用新對(duì)象上可以觸發(fā)響應(yīng)的方法,是不是很巧妙?所以多看源碼好處多多,可以多學(xué)學(xué)人家的騷操作。
IE 怎么辦?
這是個(gè)實(shí)在不想提但又繞不開(kāi)的話(huà)題,IE 在前端開(kāi)發(fā)者眼里和魔鬼沒(méi)什么區(qū)別。在 Vue3.0 之前,響應(yīng)式數(shù)據(jù)的實(shí)現(xiàn)是依賴(lài) ES5 的 Object.defineProperty,因此只要支持 ES5 的瀏覽器都支持 Vue,也就是說(shuō) Vue2.x 能支持到 IE9。Vue3.0 依賴(lài)的是 Proxy 和 Reflect 這一對(duì)出生新時(shí)代的 CP,且無(wú)法被轉(zhuǎn)譯成 ES5,或者通過(guò) Polyfill 提供兼容,這就尷尬了。開(kāi)發(fā)者技術(shù)前線(xiàn)獲悉的信息,官方在發(fā)布最終版本之前會(huì)做到兼容 IE11,至于更低版本的 IE 那就只有送上一曲涼涼了。
其實(shí)也不用太糾結(jié)IE的問(wèn)題,因?yàn)檫B微軟自己都已經(jīng)放棄治療 IE 擁抱 Chromium 了,我們又何必糾結(jié)呢?
結(jié)語(yǔ)
在使用開(kāi)源框架時(shí)不要忘了,我們之所以能免費(fèi)試用他,靠的維護(hù)者投入的大量精力。希望我們多去發(fā)現(xiàn)它帶來(lái)的優(yōu)點(diǎn)和作者想通過(guò)它傳遞的編程思想。最后期待 Vue3.0 正式版本的早日到來(lái)。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
VUE前端導(dǎo)出文件之file-saver插件安裝使用教程
這篇文章主要給大家介紹了關(guān)于VUE前端導(dǎo)出文件之file-saver插件安裝使用的相關(guān)資料,file-saver是一個(gè)用于保存文件的JavaScript庫(kù),它提供了一種簡(jiǎn)單的方式來(lái)生成和保存文件,支持各種文件類(lèi)型,例如文本文件、圖片、PDF等,需要的朋友可以參考下2024-05-05使用vue-antDesign menu頁(yè)面方式(添加面包屑跳轉(zhuǎn))
這篇文章主要介紹了使用vue-antDesign menu頁(yè)面方式(添加面包屑跳轉(zhuǎn)),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01vue實(shí)現(xiàn)輸入框的模糊查詢(xún)的示例代碼(節(jié)流函數(shù)的應(yīng)用場(chǎng)景)
這篇文章主要介紹了vue實(shí)現(xiàn)輸入框的模糊查詢(xún)的示例代碼(節(jié)流函數(shù)的應(yīng)用場(chǎng)景),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09詳解給Vue2路由導(dǎo)航鉤子和axios攔截器做個(gè)封裝
本篇文章主要介紹了詳解給Vue2路由導(dǎo)航鉤子和axios攔截器做個(gè)封裝,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-04-04Vue 構(gòu)造選項(xiàng) - 進(jìn)階使用說(shuō)明
這篇文章主要介紹了Vue 構(gòu)造選項(xiàng) - 進(jìn)階使用說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08vue2項(xiàng)目中全局封裝axios問(wèn)題
這篇文章主要介紹了vue2項(xiàng)目中全局封裝axios問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10vue?鼠標(biāo)移入移出(hover)切換顯示圖片問(wèn)題
這篇文章主要介紹了vue?鼠標(biāo)移入移出(hover)切換顯示圖片問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10