最近在編程時(shí),遇到了 vue3 數(shù)組更新卻沒(méi)有響應(yīng)的問(wèn)題,解決后,決定把對(duì)這次的問(wèn)題總結(jié)記錄下來(lái)。
代碼重現(xiàn)
項(xiàng)目的代碼太復(fù)雜了,我做了一個(gè)超精簡(jiǎn)版本的重現(xiàn)代碼:
{{ data.list }}
添加
item-service 代碼:
// 條目列表緩存對(duì)象let _items: string[] | undefined = undefinedtype Listener = (items: string[]) => voidconst listeners: Array = []export async function getItems(): Promise { if (_items) { return _items } // 從服務(wù)器請(qǐng)求數(shù)據(jù) _items = await doAjax() return _items}export async function addItem(item: string) { const items = await getItems() items.push(item) listeners.forEach(listener => listener(items))}export async function onItemsChange(listener: Listener) { listeners.push(listener)}
item-service 中緩存了列表對(duì)象,然后組件中一直使用 item-service 中緩存的這個(gè)列表給 data 賦值 ,問(wèn)題就出在這。
程序調(diào)試
vue3 的響應(yīng)式是通過(guò) Proxy 實(shí)現(xiàn)的,我在 onItemsChange 回調(diào)時(shí)做了調(diào)試,下面是 Proxy handler 的 set 方法的調(diào)試過(guò)程。為了方便查看,調(diào)試中沒(méi)有被執(zhí)行到的代碼都注釋掉了,并且加了一些說(shuō)明。
// 注釋掉的代碼都是調(diào)試過(guò)程中沒(méi)有被執(zhí)行的代碼function set(target, key, value, receiver) { // 代理 handler 的 set 方法參數(shù)說(shuō)明: // target 原始對(duì)象,getItems() 返回的 list // key 屬性名稱,‘list’ // value 要設(shè)置的值,onItemsChange 回調(diào)的 list 對(duì)象 // receiver 最初被調(diào)用的對(duì)象,通常是 proxy 本身,這里就是 data.list, list的代理對(duì)象 let oldValue = target[key]; // 使用 shallowReactive 的情況下 shallow 標(biāo)記是 true ,這里是 false if (!shallow && !isReadonly(value)) { value = toRaw(value); oldValue = toRaw(oldValue); // export const isArray = Array.isArray // !isArray(target) 返回 false if (!isArray(target) && isRef(oldValue) && !isRef(value)) { // oldValue.value = value; // return true; } } const hadKey = isArray(target) && isIntegerKey(key) // isIntegerKey(key) 返回 false,下行不執(zhí)行 // ? Number(key) !Object.is(value, oldValue) // value 和 oldValue 是同一個(gè) list 數(shù)組對(duì)象, hasChanged 返回 false // trigger(target, “set” /* SET */, key, value, oldValue); } } return result;}
可以看到?jīng)]有觸發(fā)任何更新,因此沒(méi)有響應(yīng)。
原因總結(jié)
原因總算確定了,其實(shí)就是因?yàn)楸淮淼臄?shù)組,和要賦值的數(shù)組是同一個(gè)數(shù)據(jù),在 Proxy 的回調(diào)中判定值沒(méi)有改變,沒(méi)有觸發(fā)更新。
// data.list 是 list 的代理對(duì)象,將 data.list 提取原始值(toRaw)就是 listdata.list = list
處理的方法也比較簡(jiǎn)單,將數(shù)組簡(jiǎn)單的克隆下,就沒(méi)有問(wèn)題了。
getItems().then((res) => data.list = […res]).catch(console.error)onItemsChange((list) => data.list = […list])
還可以將 item-service 中返回的列表直接克隆。
我這種情況是由于緩存數(shù)據(jù)共享對(duì)象引起的,對(duì)于需要共享數(shù)據(jù)的項(xiàng)目,還可以使用狀態(tài)管理組件,vuex 或 pinia。
對(duì)象是否也有同樣問(wèn)題?
既然是因?yàn)閺?fù)用數(shù)組,代理回調(diào)因?yàn)橹禌](méi)有改變最終沒(méi)有觸發(fā)更新,那么對(duì)象是否也存在這樣的問(wèn)題呢,我寫了個(gè)簡(jiǎn)單的代碼驗(yàn)證了下:
{{ data.user }}
change age
答案是肯定的,問(wèn)題仍然存在。如果存在這種情況,可以將對(duì)象克隆(使用 Object.assign() 或其它的方法)得到一個(gè)新的對(duì)象,再給響應(yīng)式數(shù)據(jù)賦值來(lái)解決。