异步、定时器与竞态:练习答案
防抖只提交最后一个 key
ts
vi.useFakeTimers()
const fetcher = vi.fn()
const search = debounce((key: string) => fetcher(key), 250)
search('v'); search('vu'); search('vue')
vi.advanceTimersByTime(249)
expect(fetcher).not.toHaveBeenCalled()
vi.advanceTimersByTime(1)
expect(fetcher).toHaveBeenCalledTimes(1)
expect(fetcher).toHaveBeenCalledWith('vue')
vi.useRealTimers()测试输入是同步的,只有推进到 250ms 才允许一次提交;若实现忘记 clearTimeout,调用次数会是三次。
乱序响应
用可控 Promise 让错误顺序稳定出现:
ts
const pending = new Map<string, (value: string) => void>()
const load = latestOnly((key) => new Promise<string>(resolve => pending.set(key, resolve)))
const first = load('old')
const second = load('new')
pending.get('old')!('OLD')
await expect(first).resolves.toEqual({ kind: 'stale' })
pending.get('new')!('NEW')
await expect(second).resolves.toEqual({ kind: 'success', value: 'NEW' })顺序故意与用户直觉相反。若只测试 new 先完成,错误实现也可能通过。
失败后恢复
ts
const fetcher = vi.fn()
.mockRejectedValueOnce(new Error('offline'))
.mockResolvedValueOnce(['vue'])
const load = latestOnly((key, signal) => fetcher(key, signal))
await expect(load('vue')).resolves.toMatchObject({ kind: 'error' })
await expect(load('vue')).resolves.toEqual({ kind: 'success', value: ['vue'] })
expect(fetcher).toHaveBeenCalledTimes(2)每一次调用都得到新 Promise;复用第一次 rejected Promise 会使“下一次恢复”永远失败。若把 abort 当作错误,需在实现中用 signal.aborted 分支区分取消与真正错误。
可运行核对:执行 vitest run 应一次通过;删掉版本检查、clearTimeout 或第二次调用的 Promise 后,至少一条断言必须失败。