12.11 聊聊 Vue3.0 響應式數據

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

  1. 使用 Typescript
  2. 放棄 class 採用 function-based API
  3. 重構 complier
  4. 重構 virtual DOM
  5. 新的響應式機制

今天咱就聊聊重構後的響應式數據。

嚐鮮

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





<title>Document/<title>






確實寫法上和 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 暴露的這幾個和響應式數據相關的方法:

  1. reactive(value)創建可觀察的變量,參數可以是 JS 原始類型、引用、純對象、類實例、數組、集合(Map|Set)。
  2. effect(fn)effect 意思是副作用,此方法默認會先執行一次。如果 fn 中有依賴的可觀察屬性變化時,會再次觸發此回調函數
  3. 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";
//賦值

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

聊聊 Vue3.0 響應式數據

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

  1. 數組和普通對象相比,JS 數組太"多變"了。比如:arr.length=0,可以瞬間清空一個數組;arr[100]=1又可以瞬間將一個數組的長度變為 100(其他位置用空元素填充),等等騷操作。對於一個普通對象,我們一般只會改變 Key 對應的 Value 值,而不會連key都改變了,而數組就不一樣了 Key 和 Value 都經常增加或減少,因此每次變化後我們都需要重新將整個數組的所有 key 遞歸的使用 Object.defineProperty 加上 setter 和 getter,同時我們還要窮舉每一種數組變化的可能,這樣勢必就會帶來性能開銷問題,有的人會覺得這點性能開銷算個 x 呀,但是性能問題都是由小變大的,如果數組中存的數據量大而且操作頻繁時,這就會是一個大問題。React16.x 在就因為在優化 textNode 的時候,移除了無意義的 span 標籤,性能據說都提升了多少個百分點,所以性能問題不可小看。
  2. 數組在應用中經常會被操作,但是通常 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 整個對象變為可觀察對象。

惰性監聽有什麼好處?

  1. 提高了組件實例初始化速度Vue3.0 以前組件實例在初始化的時候會將 data 整個對象變為可觀察對象,通過遞歸的方式給每個 Key 使用Object.defineProperty 加上 getter 和 settter,如果是數組就重寫代理數組對象的七個方法。而在 Vue3.0 中,將可響應式對象創建的權利交給了開發者,開發者可以通過暴露的 reactive, compted, effect 方法自定義自己需要響應式能力的數據,實例在初始化時不需要再去遞歸 data 對象了,從而降低了組件實例化的時間。
  2. 降低了運行內存的使用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,
toRaw: WeakMap,
baseHandlers: ProxyHandler,
collectionHandlers: ProxyHandler
) {
//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
}/<function>

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

export const mutableCollectionHandlers: ProxyHandler = {
get: createInstrumentationGetter(mutableInstrumentations)
}
export const readonlyCollectionHandlers: ProxyHandler = {

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 正式版本的早日到來。


分享到:


相關文章: