首頁>技術>

"別再更新了,實在是學不動了"這句話道出了多少前端開發者的心聲,"不幸"的是 Vue 的作者在國慶區間釋出了 Vue3.0 的 pre-Aplha 版本,這意味著 Vue3.0 快要和我們見面了。既來之則安之,扶我起來我要開始講了。Vue3.0 為了達到更快、更小、更易於維護、更貼近原生、對開發者更友好的目的,在很多方面進行了重構:

使用 Typescript放棄 class 採用 function-based API重構 complier重構 virtual DOM新的響應式機制

今天咱就聊聊重構後的響應式資料。

嚐鮮

重構後的 Vue3.0 和之前在寫法上有很大的差別,早前在網路上對於 Vue3.0 這種激進式的重構方式發起了一場討論,見仁見智。不多說先看看 Vue3.0 在寫法上激進到什麼程度。

<!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>

確實寫法上和 Vue2.x 差別有點大,還整出了個 setup。不過我的第一感覺倒不是寫法上的差異,畢竟寫過 React,這種寫法也沒啥特別的。關鍵是這種響應式資料的寫法好像在哪裡見過有沒有?寫過 React 專案的人可能一眼就能看出來,沒錯就是它 mobx,一種 React 的響應式狀態管理外掛

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 暴露的這幾個和響應式資料相關的方法:

reactive(value)建立可觀察的變數,引數可以是 JS 原始型別、引用、純物件、類例項、陣列、集合(Map|Set)。effect(fn)effect 意思是副作用,此方法預設會先執行一次。如果 fn 中有依賴的可觀察屬性變化時,會再次觸發此回撥函式computed(()=>expression)建立一個計算值,computed 實現也是基於 effect 來實現的,特點是 computed 中的函式不會立即執行,多次取值是有快取機制的,expression 不應該有任何副作用,而僅僅是返回一個值。當這個 expression 依賴的可觀察屬性變化時,這個表示式會重新計算。

和 mobx 有異曲同工之妙。

Vue3.0 把建立響應式物件從元件例項初始化中抽離了出來,通過暴露 API 的方式將響應式物件建立的權利交給開發者,開發者可以自由的決定何時何地建立響應式物件,就衝這點 Vue3.0 我先粉了。

重構後的響應式機制帶來了哪些改變?

每一個大版本的釋出都意味著新功能、新特性的出現,那麼重構後的響應式資料部分相比 3.0 之前的版本有了哪些方面的改變呢?下面聽我娓娓道來:

對陣列的全面監聽

Vue2.x 中被大家吐槽的最多的一點就是針對陣列只實現了 push,pop,shift,unshift,splice,sort,reverse' 這七個方法的監聽,以前通過陣列下標改變值的時候,是不能觸發檢視更新的。這裡插一個題外話,很多人認為 Vue2.x 中陣列不能實現全方位監聽是 Object.defineProperty 不能監聽陣列下標的改變,這可就冤枉人家了,人家也能偵聽陣列下標變化的好嗎,不信你看

const arr = ["2019","雲","棲","音","樂","節"];arr.forEach((val,index)=>{ Object.defineProperty(arr,index,{ set(newVal){ console.log("賦值"); }, get(){ console.log("取值"); return val; } })})let index = arr[1];//取值arr[0] = "2050";//賦值

沒毛病,一切變化都在人家的掌握中。上面這段程式碼,有沒有人沒看懂,我假裝你們都不懂,貼張圖

從陣列的資料結構來看,陣列也是一個 Key-Value 的鍵值對集合,只是 Key 是數字罷了,自然也可以通過Object.defineProperty 來實現陣列的下標訪問和賦值攔截了。其實 Vue2.x 沒有實現陣列的全方位監聽主要有兩方面原因:

陣列和普通物件相比,JS 陣列太"多變"了。比如:arr.length=0,可以瞬間清空一個數組;arr[100]=1又可以瞬間將一個數組的長度變為 100(其他位置用空元素填充),等等騷操作。對於一個普通物件,我們一般只會改變 Key 對應的 Value 值,而不會連key都改變了,而陣列就不一樣了 Key 和 Value 都經常增加或減少,因此每次變化後我們都需要重新將整個陣列的所有 key 遞迴的使用 Object.defineProperty 加上 setter 和 getter,同時我們還要窮舉每一種陣列變化的可能,這樣勢必就會帶來效能開銷問題,有的人會覺得這點效能開銷算個 x 呀,但是效能問題都是由小變大的,如果陣列中存的資料量大而且操作頻繁時,這就會是一個大問題。React16.x 在就因為在優化 textNode 的時候,移除了無意義的 span 標籤,效能據說都提升了多少個百分點,所以效能問題不可小看。陣列在應用中經常會被操作,但是通常 push,pop,shift,unshift,splice,sort,reverse 這 7 種操作就能達到目的。因此,出於效能方面的考慮 Vue2.x 做出了一定的取捨。

那麼 Vue3.0 怎麼又走回頭路去實現了陣列的全面監聽了呢?答案就是 Proxy 和 Reflet 這一對原生 CP 的出現,Vue3.0 使用 Proxy 作為響應式資料實現的核心,用 Proxy 返回一個代理物件,通過代理物件來收集依賴和觸發更新。大概的原理像這段程式碼一樣:

const arr = ["2019","雲","棲","音","樂","節"];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 一樣一樣的,又顯得清新脫俗有沒有?而且 Proxy 只要是物件都能代理,後面還會提到。當然 Vue3.0 是雖然有了新歡,但也沒忘記舊愛,對於在之前版本中陣列的幾種方法的監聽還是照樣支援的。

惰性監聽

什麼是"惰性監聽"?

簡單講就是"偷懶",開發者可以選擇性的生成可觀察物件。在平時的開發中常有這樣的場景,一些頁面上的資料在頁面的整個生命週期中是不會變化的,這時這部分資料不需要具備響應式能力,這在 Vue3.0 以前是沒有選擇餘地的,所有在模板中使用到的資料都需要在 data 中定義,元件例項在初始化的時候會將 data 整個物件變為可觀察物件。

惰性監聽有什麼好處?

提高了元件例項初始化速度Vue3.0 以前元件例項在初始化的時候會將 data 整個物件變為可觀察物件,通過遞迴的方式給每個 Key 使用Object.defineProperty 加上 getter 和 settter,如果是陣列就重寫代理陣列物件的七個方法。而在 Vue3.0 中,將可響應式物件建立的權利交給了開發者,開發者可以通過暴露的 reactive, compted, effect 方法自定義自己需要響應式能力的資料,例項在初始化時不需要再去遞迴 data 物件了,從而降低了元件例項化的時間。降低了執行記憶體的使用Vue3.0 以前生成響應式物件會對物件進行深度遍歷,同時為每個 Key 生成一個 def 物件用來儲存 Key 的所有依賴項,當 Key 對應的 Value 變化的時候通知依賴項進行 update。但如果這些依賴項在頁面整個生命週期內不需要更新的時候,這時 def 物件收集的依賴項不僅沒用而且還會佔用記憶體,如果可以在初始化 data 的時候忽略掉這些不會變化的值就好了。Vue3.0 通過暴露的 reactive 方法,開發者可以選擇性的建立可觀察物件,達到減少依賴項的儲存,降低了執行記憶體的使用。Map、Set、WeakSet、WeakMap的監聽

前面提到 Proxy 可以代理所有的物件,立馬聯想到了 ES6 裡面新增的集合 Map、Set, 聚合型別的支援得益於 Proxy 和 Reflect。講真的這之前還真不知道 Proxy 這麼剛啥都能代理,二話不說直接動手用 Proxy 代理了一個 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]

一盆涼水潑來,報錯了。原來 Map、Set 物件賦值、取值和他們內部的 this 指向有關係,但這裡的 this 指向的是其實是 Proxy 物件,所以得這樣幹

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")

當獲取的是一個函式的時候,通過作用域繫結的方式將原物件繫結到 Map、Set 物件上就好了。

Vue3.0 是如何實現集合型別資料監聽的?

眼尖的同學看完上面這段程式碼會發現一個問題,集合是沒有 set 方法,集合賦值用的是 add 操作,那咋辦呢?來看看那麼 Vue3.0 是怎麼處理的,上一段簡化後的原始碼

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 //生成代理物件 observed = new Proxy(target, handlers) toProxy.set(target, observed) toRaw.set(observed, target) if (!targetMap.has(target)) { targetMap.set(target, new Map()) } return observed}

根據 target 型別適配不同的 handler,如果是集合 (Map、Set)就使用 collectionHandlers,是其他型別就使用 baseHandlers。接下來看看 collectionHandlers

export const mutableCollectionHandlers: ProxyHandler<any> = { get: createInstrumentationGetter(mutableInstrumentations)}export const readonlyCollectionHandlers: ProxyHandler<any> = { get: createInstrumentationGetter(readonlyInstrumentations)}

沒有意外只有 get,騷就騷在這兒:

// 可變資料插樁物件,以及一系列相應的插樁方法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)}// 迭代器相關的方法const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]iteratorMethods.forEach(method => { mutableInstrumentations[method] = createIterableMethod(method, false) readonlyInstrumentations[method] = createIterableMethod(method, true)})// 建立getter的函式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 集合的原生方法不一致,因此無法通過 Proxy 劫持 set,所以作者在在這裡進行了"偷樑換柱",這裡新建立了一個和集合物件具有相同屬性和方法的普通物件,在集合物件 get 操作時將 target 物件換成新建立的普通物件。這樣,當呼叫 get 操作時 Reflect 反射到這個新物件上,當呼叫 set 方法時就直接呼叫新物件上可以觸發響應的方法,是不是很巧妙?所以多看原始碼好處多多,可以多學學人家的騷操作。

IE 怎麼辦?

這是個實在不想提但又繞不開的話題,IE 在前端開發者眼裡和魔鬼沒什麼區別。在 Vue3.0 之前,響應式資料的實現是依賴 ES5 的 Object.defineProperty,因此只要支援 ES5 的瀏覽器都支援 Vue,也就是說 Vue2.x 能支援到 IE9。Vue3.0 依賴的是 Proxy 和 Reflect 這一對出生新時代的 CP,且無法被轉譯成 ES5,或者通過 Polyfill 提供相容,這就尷尬了。開發者技術前線獲悉的資訊,官方在釋出最終版本之前會做到相容 IE11,至於更低版本的 IE 那就只有送上一曲涼涼了。

其實也不用太糾結IE的問題,因為連微軟自己都已經放棄治療 IE 擁抱 Chromium 了,我們又何必糾結呢?

結語

在使用開源框架時不要忘了,我們之所以能免費試用他,靠的維護者投入的大量精力。希望我們多去發現它帶來的優點和作者想通過它傳遞的程式設計思想。最後期待 Vue3.0 正式版本的早日到來。

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • PHP設計簡訊驗證碼防刷機制,你能想出多少種方案?