Getter计算后Computed仍脏,异步依赖引发警告的解决办法
解决Pinia中Firestore按需加载数据的Computed警告问题
问题场景
我用Pinia实现了从Firestore按需加载玩家数据的逻辑:
- 查询时先在响应式Map
loadedPlayers中标记对应条目为未加载状态 - Firestore返回数据后,更新Map中的对应条目为加载完成的数据
- 通过
playerName和getPlayer方法返回computed ref,数据加载完成前返回playerId,加载完成后返回真实数据
逻辑能正常运行,但一直收到警告:
Computed is still dirty after getter evaluation, likely because a computed is mutating its own dependency in its getter
推测是Firestore的onSnapshot回调在computed属性求值期间同步修改了loadedPlayers,导致Vue响应式系统检测到computed在求值过程中修改了自身依赖。
原始Store代码
export const usePlayerStore = defineStore("player", () => { const db = getFirestore(); const loadedPlayers = ref(new Map<string, LoadedPlayer>()); const subscriptions = new Map<string, Unsubscribe>(); const loadPlayer = (playerId: string) => { if (!subscriptions.has(playerId)) { subscriptions.set( playerId, onSnapshot(doc(db, "players", playerId), (snapshot) => { if (snapshot.exists()) { const p = new LoadedPlayer(playerId, snapshot.data() as DBPlayer); // 触发警告的代码行 loadedPlayers.value.set(playerId, p); } else { loadedPlayers.value.delete(playerId); } }), ); } }; const playerName = (playerId: string) => { loadPlayer(playerId); // 触发警告的computed return computed(() => { const p = loadedPlayers.value.get(playerId); if (p) { return p.name; } return playerId; }); }; const getPlayer = (playerId: string): Ref<Player> => { loadPlayer(playerId); return computed( () => (loadedPlayers.value.get(playerId) as LoadedPlayer | undefined) ?? { id: playerId, loaded: false, name: playerId, }, ); }; return { loadPlayer, playerName, getPlayer, }; });
组件使用示例(Vue JSX)
export default defineComponent({ props: { players: { type: Array as PropType<string[]>, required: true }, }, setup: (props) => { const playerStore = usePlayerStore(); return () => <table> <thead> <tr> <td> </td> {props.players.map(pid => <td class="playerName" data-player-id={pid}>{playerStore.playerName(pid).value}</td>)} </tr> </thead> <tbody> {/* 动态表格内容 */} </tbody> </table>; } })
解决方案
1. 异步更新响应式数据
将loadedPlayers的更新放到nextTick中,确保修改操作在当前computed求值完成后执行,避免在getter期间修改依赖:
import { nextTick } from 'vue'; // 修改loadPlayer中的onSnapshot回调 onSnapshot(doc(db, "players", playerId), async (snapshot) => { await nextTick(); // 等待当前渲染周期结束后再更新数据 if (snapshot.exists()) { const p = new LoadedPlayer(playerId, snapshot.data() as DBPlayer); loadedPlayers.value.set(playerId, p); } else { loadedPlayers.value.delete(playerId); } }),
2. 缓存Computed实例
避免为同一个playerId重复创建computed实例,减少不必要的计算和订阅触发,同时优化性能:
export const usePlayerStore = defineStore("player", () => { const db = getFirestore(); const loadedPlayers = ref(new Map<string, LoadedPlayer>()); const subscriptions = new Map<string, Unsubscribe>(); // 新增computed缓存 const playerNameComputeds = new Map<string, ComputedRef<string>>(); const playerComputeds = new Map<string, ComputedRef<Player>>(); const loadPlayer = (playerId: string) => { if (!subscriptions.has(playerId)) { subscriptions.set( playerId, onSnapshot(doc(db, "players", playerId), async (snapshot) => { await nextTick(); if (snapshot.exists()) { const p = new LoadedPlayer(playerId, snapshot.data() as DBPlayer); loadedPlayers.value.set(playerId, p); } else { loadedPlayers.value.delete(playerId); } }), ); } }; const playerName = (playerId: string) => { if (!playerNameComputeds.has(playerId)) { loadPlayer(playerId); const computedRef = computed(() => { const p = loadedPlayers.value.get(playerId); return p ? p.name : playerId; }); playerNameComputeds.set(playerId, computedRef); } return playerNameComputeds.get(playerId)!; }; const getPlayer = (playerId: string): Ref<Player> => { if (!playerComputeds.has(playerId)) { loadPlayer(playerId); const computedRef = computed( () => loadedPlayers.value.get(playerId) ?? { id: playerId, loaded: false, name: playerId, }, ); playerComputeds.set(playerId, computedRef); } return playerComputeds.get(playerId)!; }; return { loadPlayer, playerName, getPlayer, }; });
3. 组件内优化(可选)
在组件中提前缓存玩家名称的computed集合,避免每次渲染都调用store方法:
export default defineComponent({ props: { players: { type: Array as PropType<string[]>, required: true }, }, setup: (props) => { const playerStore = usePlayerStore(); // 提前计算所有玩家名称的响应式集合 const playerNames = computed(() => props.players.map(pid => playerStore.playerName(pid).value) ); return () => <table> <thead> <tr> <td> </td> {playerNames.value.map((name, idx) => ( <td class="playerName" data-player-id={props.players[idx]}> {name} </td> ))} </tr> </thead> <tbody> {/* 动态表格内容 */} </tbody> </table>; } })
效果说明
- 异步更新确保computed求值过程中不会修改依赖,彻底消除警告
- 缓存computed实例保证每个玩家条目只触发一次数据加载和computed创建,符合需求
- 组件内优化进一步提升渲染性能,减少响应式依赖的重复解析
内容的提问来源于stack exchange,提问作者Alpvax
相关产品推荐
相关产品推荐

