前言:
在平常的開發中,經常會在vue中用到對象數組,如渲染一個小區的列表,數據結構可能如下所示:
CommunityList: [ { _id: '', community_code: '', community_name: '', ... }, ... ]
當需要實現選擇某幾個小區加入收藏列表的時候,頁面上往往需要增加el-checkbox來勾選,在數據結構上需要增加一個類似relation屬性來標志該小區是否被選擇。
所以在從接口拿到數據的時候需要在數據上增加relation屬性,處理方式可能如下所示:
/** result是從接口獲取的數據 **/ if (result && result.retCode === 0) { this.communityList = result.list.map(item => { item.relation = false; return item; }) }
當你碰巧在處理數據之前先將result.list賦值給了另一個實例屬性,如下所示:
/** result是從接口獲取的數據 **/ if (result && result.retCode === 0) { this.communityListCache = result.list; // 新增的代碼 this.communityList = result.list.map(item => { item.relation = false; return item; }) }
那么此時el-checkbox的表現可能會不太正常,如下所示:
點擊時狀態不會立馬改變,而是等鼠標移開了才改變
下面兩個圖均是在el-checkbox為灰色狀態下點擊的結果,一個為true,另一個卻是false
分析:
經過打斷點分析發現某些屬性並不是像其它屬性一樣的表現
如這里的relation屬性(el-checkbox綁定的就是該屬性),不像下面的_id屬性所表現的一致,_id屬性所表現的就是正常的被vue監聽的狀態,很明顯relation並沒有被vue監聽,問題就出現在新增的代碼那里。
通過分析vue的源代碼可知,實例屬性被賦值的時候會使用Object.defineProperties方法將其轉化為可被監聽的屬性,如果賦值了一個數組,也會將數組轉化為可被監聽的對象,且會深入內部監聽如下:
截取自vue源代碼中的部分片段:
export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that have this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) // 當為數組時,轉化數組 } else { this.walk(value) } } ... /** * Observe a list of Array items. */ observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) // 逐級轉化 } } }
又因為js中引用類型只是指向實際數據的類似指針的結構,此時this.communityListCache和result.list在內存中實際就是指向同一個地址,當vue轉化this.communityListCache的時候實際就是轉化了result.list
this.communityListCache = result.list; // 新增的代碼
vue會在result.list中新增__ob__這個屬性來標志已經被轉化過了,如下圖所示:
由於relation是在之后才新增的屬性,而在vue轉化的時候,如果對象有__ob__屬性,則表示已經被轉化過了,不會再次去處理,如下:
截取自vue源代碼中的部分片段:
/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */ export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // 判斷是否本來就存在__ob__屬性,如果存在就直接賦值,不做處理 ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob }
這樣后面新增的relation屬性實際就是沒有被vue所監聽的屬性,自然就不會響應變化。
解決辦法:
1、在完全處理好原始數據后再賦值給其它的實例屬性,如下:
/** result是從接口獲取的數據 **/ if (result && result.retCode === 0) { // this.communityCache = result.list; // 原始代碼 this.communityList = result.list.map(item => { item.relation = false; return item; }); this.communityCache = result.list; // 新增的代碼 }
2、由於接口返回的數據一般沒有包涵特殊類型,所以在賦值之前進行簡單的深拷貝即可,這樣的話,這兩個變量就不是指向同一個對象,也就不會相互影響:
/** result是從接口獲取的數據 **/ if (result && result.retCode === 0) { // this.communityCache = result.list; // 原始代碼 this.communityCache = JSON.parse(JSON.stringify(result.list)); // 新增代碼 this.communityList = result.list.map(item => { item.relation = false; return item; }); this.communityCache = result.list; // 新增的代碼 }
注:個人總結,如有不對,還望指正,謝謝!