У меня есть хранилище Pinia, которое загружает данные по запросу из базы данных (в частности, Firestore, но я не думаю, что это имеет значение).
Когда данные запрашиваются, они устанавливают значение в реактивную карту ({ id: <id>, loaded: false }
).
Когда запрос к базе данных возвращает результат (через обещание), он устанавливает то же значение карты для данных, загруженных из базы данных.
В то же время вычисленная ссылка возвращается на исходный сайт вызова, возвращая значение базы данных, если данные присутствуют (загружено == true), или идентификатор, если нет.
Это работает, но я получил предупреждение Computed is still dirty after getter evaluation, likely because a computed is mutating its own dependency in its getter
, предположительно потому, что обещание разрешается, пока вычисляется вычисляемое свойство.
Есть ли способ подавить это предупреждение, так как я хочу, чтобы вычисляемое свойство пересчитывалось при изменении данных, и это происходило максимум один раз для каждой запрашиваемой записи в базе данных.
Код моего магазина приведен ниже (машинописный текст)
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);
// Warning caused by this line
loadedPlayers.value.set(playerId, p);
} else {
loadedPlayers.value.delete(playerId);
}
}),
);
}
};
const playerName = (playerId: string) => {
loadPlayer(playerId);
// Warning caused by this computed property
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>
{/* Dynamic table body content here */}
</tbody>
</table>;
}
})
Трудно помочь без реального кода. Также, пожалуйста, погуглите эту ошибку, возможно, она не первая с конкретной проблемой.
@EstusFlask Я не публиковал код, так как это был скорее вопрос об общем решении сообщаемого состояния гонки (изменение, происходящее во время вычислительного расчета), чем конкретно об этой проблеме.
@kissu Это был мой первый подход, но все, что я смог найти, это использование в синхронном коде.
@Alpvax, я вижу. Это очень специфично для вашего случая. Предупреждение не является чем-то распространенным, и реальная проблема заключается в том, что вычисления используются неправильно. Является ли playerName действием? Действие не должно возвращать новую ссылку/вычисление. В большинстве случаев действия вообще не возвращают никакого результата. Я разместил ответ, это еще зависит от предполагаемого использования магазина, как его нужно переписать, это пока не показано
Вычисления используются неправильно, они должны быть определены один раз при инициализации хранилища, их не следует вызывать несколько раз с помощью return computed
.
Параметризованное вычисление можно определить следующим образом:
const getPlayerName = computed(() => (playerId: string) => {...});
Но вычисление необходимо отделить от побочных эффектов, таких как loadPlayer
:
loadPlayer('foo');
watchEffect(() => {
const fooName = getPlayerName.value('foo'),
...
);
Для loadPlayer
также имело бы смысл вернуть обещание для строгого потока управления, это позволило бы действию вернуть обещание результата и избежать возможных состояний гонки, возникающих из-за того, что игрок не загружается в момент обращения к нему.
Спасибо. Я хотел, чтобы getPlayerName возвращал вычисленное свойство либо имени игрока, либо pid, в зависимости от того, был ли загружен игрок (когда он не загружен, имя недоступно). Таким образом, я могу использовать свойство в компонентах для отображения имени, и оно будет автоматически обновляться при возврате запроса к базе данных, без необходимости определять вычисляемое свойство в каждом компоненте (они используются как списки игроков, то есть как списки вычисляемых свойств). Не могли бы вы посоветовать лучший подход для этого, если это неправильный подход?
loadPlayer не нужно возвращать обещание, поскольку меня на самом деле не волнует, загружен ли плеер, я просто хочу инициировать его, если он еще не загружается.
Я ожидаю, что это будет сделано так, как предложено в ответе. Отделите loadPlayer от вычисляемого, при этом getPlayerName будет реактивным. Есть ли случай, когда этот подход вызывает проблемы?
Итак, если у меня есть компонент с (реактивным) списком идентификаторов игроков, и я хочу отображать их как имена, когда они доступны, у меня должна быть реактивная вся функция getName, а не ComputedRef[]
? Как мне гарантировать, что игроки загружаются? Должен ли я иметь в списке отдельный наблюдатель и вызывать store.loadPlayer для каждого?
Моя цель состояла в том, чтобы все это работало как компоновка, где я мог бы просто предоставить сайту вызова единственный метод для получения имени игрока и загрузки в фоновом режиме. Должно ли это быть выделено в отдельный составной объект с наблюдателем внутри него, который сам ссылается на хранилище?
Можете ли вы предоставить упрощенную схему, которая использует эти данные и отражает ваше реальное использование, чтобы ее можно было конкретно рассмотреть? То, как это должно работать, зависит от того, что происходит с getPlayerName. Обычно у вас есть {{ getPlayerName('foo') }} в шаблоне и loadPlayer('foo') в части сценария (вероятно, внутри наблюдателя, если 'foo' является динамическим значением), и это будет работать. Я совершенно уверен, что в целом существует более одного способа добиться этого, но что касается части Pinia, то она должна быть довольно строгой, есть «геттеры» и «действия».
Я добавил в ОП образец упрощенного компонента. Информация об игроках может не поступать из базы данных, поэтому нет гарантии, что игроки с указанными идентификаторами будут загружены. Было бы неплохо иметь возможность запрашивать имена с помощью простой функции, но я не уверен на 100%, как бы работала реактивность, если бы это была просто функция, а не возврат вычисленной ссылки. Я сильно усложняю ситуацию?
Я ожидаю, что это будет watchEffect(() => { props.players.forEach(pid => store.loadPlayer(pid)) })
в настройке компа и {store.getPlayerName(pid)}
в функции рендеринга. Реактивность обеспечивается тем фактом, что доступ к loadedPlayers.value.get()
осуществляется внутри getPlayerName при запуске функции рендеринга. Вычисление само по себе не улучшит реактивность, оно полезно для оптимизации, но не в этом случае. getPlayerName на самом деле может быть обычной функцией, которая возвращает строку, она не имеет особой пользы от вычисления(), это всего лишь соглашение, которое предотвращает ее переназначение в хранилище
Пожалуйста, укажите stackoverflow.com/help/mcve вместо описания. Поскольку проблема вызвана конкретным кодом, ее необходимо решать конкретно.