Skip to content

异步 Action、竞态与服务端状态边界 ​

异步 action 的难点不是 await,而是“这次响应是否仍然属于当前输入”。搜索框快速输入 v、vu、vue 时,网络返回顺序可以是 vue、v、vu;如果每个响应都直接写入 store,旧结果会覆盖新结果。

用请求身份保护提交 ​

ts
import { reactive } from 'vue'

type SearchState = { status: 'idle' | 'pending' | 'success' | 'empty' | 'error'; items: string[]; error: Error | null }
const state = reactive<SearchState>({ status: 'idle', items: [], error: null })
const fetchResults = (keyword: string) => fetch(`/api/search?q=${encodeURIComponent(keyword)}`).then((response) => {
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
  return response.json() as Promise<string[]>
})

let requestVersion = 0

async function search(keyword: string) {
  const version = ++requestVersion
  state.status = 'pending'
  try {
    const result = await fetchResults(keyword)
    if (version !== requestVersion) return { ignored: true }
    state.items = result
    state.status = result.length ? 'success' : 'empty'
    return { ignored: false }
  } catch (error) {
    if (version !== requestVersion) return { ignored: true }
    state.status = 'error'
    state.error = error instanceof Error ? error : new Error('unknown error')
    throw error
  }
}

版本号只负责“最后一次提交”,不能停止已经发出的请求;搜索、详情切换等场景还应配合 AbortController 释放不再需要的网络和解析工作。取消属于控制流,不应在界面上误报成服务器失败。

不要把缓存问题伪装成 Pinia 问题 ​

Pinia 很适合保存当前用户、编辑草稿和跨页面 UI 状态;带 query key、过期时间、失效、重试、后台重取的服务器状态应交给 TanStack Query 等专门工具。若仍把列表缓存放进 Pinia,至少要显式保存 fetchedAt、请求参数和失效规则,禁止用“刷新页面就好了”作为一致性策略。

练习 ​

实现一个可控 Promise 的搜索测试:先发出 old 再发出 new,让 new 先 resolve,随后让 old resolve;最终列表必须仍是 new。再加入取消路径,说明取消后 loading 如何结束。

下一步 ​

完成练习后打开本章答案,再回到Pinia 课程总览。

用心记录每一步成长