背景
一个列表,其中数据项有属性 favorite 展示是否收藏。
点击打开详情弹框,弹框中有操作进行收藏/取消收藏。然后收藏状态需要同步到列表中。
现状
已经实现的大概逻辑:
父组件:
列表数据 list = ref(xx)
点击列表项获取详情数据、打开详情弹框:
async function handleClickItem(params) {
const detail = await getDetail(params))
detailData.value = {
id: curDetailId,
isFavorite: detail.is_favorite,
}
isDetailVisible.value = true
}
根据子组件更新列表数据:
function handleUpdateFavorite(isFavorite) {
detailData.value.isFavorite = isFavorite
list.value = list.value.map((item) => {
return {
...item,
favorite: item.design_id === curDetailId ? isFavorite : item.favorite,
}
})
}
子组件:
// 子组件收藏状态
const isFavorite = ref(props.xxx.isFavorite)
// 监听父组件传参
watchEffect(() => {
isFavorite.value = props.xxx.isFavorite
})
// 收藏操作
function handleFavorite() {
await updateFavorites(xxx)
isFavorite.value = !isFavorite.value
emits('update-favorite', isFavorite)
}
现象
假设有列表项 A、B、C、D……初始 favorite 属性都是 false。
- 点击 A,在详情弹框中进行收藏,能正常收藏,子组件、父组件状态都能正常改变,favorite 为 true。
- 关掉 A,点击 B,子组件状态正常,父组件列表 list 中的 A 的 favorite 属性变为 false。
- 关掉 B,点击 A,子组件和父组件又都是正确的,也就是 list 中 A 的favorite 为 true。
问题就出在 2,为什么点击 B,会改变 A 的属性呢?list 好像没有进行更新啊。
继续:
- 点击 B,收藏。
- 点击 C,B 的收藏取消了,但是 A 的并没有取消,A 的恢复了收藏状态。
- 点击 C,收藏。
- 点击 D,同5,C 的收藏取消了,但是 B、A 的收藏还在。
排查
一开始怀疑是子组件更新数据的问题,涉及到 watch watchEffect 监听数据。但是排查下来没有发现问题。
上面的现象也是我后来发现总结的。一开始发现 list 更新,就开始排查到底哪里进行了 list 的更新,第二次打开弹框是否进行了 list 更新?打开弹框进行详情数据的请求,detailData 是否会影响 list,似乎也不能发现问题。
问了 ChatGPT,贴了部分代码,完全是模式的回答,对于这种很隐蔽的 bug 完全没有意义。
原因
在请同事帮忙看了之后,很快发现了问题,原来是子组件 emits 传参错误,isFavorite
应为 isFavorite.value
。
这实在是一个低级错误,但是因为收藏功能没问题,一开始就忽略了这里的问题。
但是,我想要知道为什么会导致这个现象?
打开 A,收藏,正确,打开 B,为什么会更新 list?这里并不是哪里主动更新了 list,而是 A 的 favorite 被赋值为一个 ref 对象。对象是引用,也就是子组件的状态 isFavorite
被父组件 list 中 A 的属性 Favorite
引用。当打开 B 的时候,父组件传参更新了子组件的状态 isFavorite
,所以此时 list 中的属性也跟着改变了。
总结
- js 莫名其妙的数据更新问题,往往是引用的问题。此时在控制台打印也要很小心,打印对象并不能反应实时的状态。
- Vue ref 的赋值需要注意 value。
- 但是:
function handleUpdateFavorite(isFavorite) {
console.log('isFavorite: ', isFavorite);
detailData.value.isFavorite = isFavorite
console.log('detailData.value.isFavorite: ', detailData.value.isFavorite);
}
这里的打印是:
isFavorite: {"__v_isShallow":false,"dep":{"cleanup":"Function()=>ref2.dep=void0Function()=>ref2.dep=void0","computed":null},"__v_isRef":true,"_rawValue":true,"_value":true} detailData.value.isFavorite: true
为什么?
因为 Ref Unwrapping,ref 本身作为值赋值给 reactive 对象的属性,会进行自动解包操作。
我之前很清楚 template 中的 unwrapping 以及数组的 unwrapping,却忽略了这里。
- 上面的现象 4-7 怎么解释?
打开 A,收藏,再打开 B,A 的收藏状态被改变,收藏 B,打开 C,B 变为未收藏,而 A 是已收藏。
如果是引用的关系,不应该 A 也会被打开 C 的操作影响吗?变为未收藏吗?
父组件的回调更新:
list.value = list.value.map((item) => {
return {
...item,
favorite: item.design_id === curDetailId ? isFavorite : item.favorite,
}
})
列表只更新了当前项,当打开 C 时,影响了 B 的 favorite 引用,但是 A 的 favorite 属性已经被赋值为 true,不会受到影响。此时 A 的赋值是 item.favorite
,值为 true,而不是 isFavorite
引用。
- 另外这个问题的源头是传参错误,为什么 TS 类型约束不起作用呢?这样修改:
const emits = defineEmits<{
'update-favorite': [isFavorite: boolean]
}>()
这样修改之后,就可以对 emits 的传参进行类型约束。