原創不易,但願能關注下咱們,再順手點個贊~~ |
本文首發於政採雲前端團隊博客: 茶餘飯後聊聊 Vue3.0 響應式數據那些事兒javascript
"別再更新了,實在是學不動了"這句話道出了多少前端開發者的心聲,"不幸"的是 Vue 的做者在國慶區間發佈了 Vue3.0 的 pre-Aplha 版本,這意味着 Vue3.0 快要和咱們見面了。既來之則安之,扶我起來我要開始講了。Vue3.0 爲了達到更快、更小、更易於維護、更貼近原生、對開發者更友好的目的,在不少方面進行了重構:html
今天咱就聊聊重構後的響應式數據。前端
重構後的 Vue3.0 和以前在寫法上有很大的差異,早前在網絡上對於 Vue3.0 這種激進式的重構方式發起了一場討論,見仁見智。很少說先看看 Vue3.0 在寫法上激進到什麼程度。vue
<!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 的響應式狀態管理插件java
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 暴露的這幾個和響應式數據相關的方法:react
reactive(value)express
建立可觀察的變量,參數能夠是 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 方法,開發者能夠選擇性的建立可觀察對象,達到減小依賴項的保存,下降了運行內存的使用。
前面提到 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在前端開發者眼裏和魔鬼沒什麼區別。在 Vue3.0 以前,響應式數據的實現是依賴 ES5 的 Object.defineProperty,所以只要支持 ES5 的瀏覽器都支持 Vue ,也就是說 Vue2.x 能支持到 IE9 。Vue3.0 依賴的是 Proxy 和 Reflect 這一對出生新時代的 CP,且沒法被轉譯成 ES5 ,或者經過 Polyfill 提供兼容,這就尷尬了。開發者技術前線獲悉的信息,官方在發佈最終版本以前會作到兼容 IE11 ,至於更低版本的 IE 那就只有送上一曲涼涼了。
其實也不用太糾結 IE 的問題,由於連微軟本身都已經放棄治療 IE 擁抱 Chromium 了,咱們又何須糾結呢?
在使用開源框架時不要忘了,咱們之因此能免費試用他,靠的維護者投入的大量精力。但願咱們多去發現它帶來的優勢和做者想經過它傳遞的編程思想。最後期待 Vue3.0 正式版本的早日到來。
招人,前端,隸屬政採雲前端大團隊(ZooTeam),50 餘個小夥伴正等你加入一塊兒浪~ 若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變「 5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手參與一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給ZooTeam@cai-inc.com