这两天调试别人项目中的一段 js 代码,作用是刷新 token ,但是验证下来发现有很小的几率会触发多次刷新 token 的动作(下面代码中的 FIXME 位置),特别是 Promise.all 去发送一批请求的时候,我 google 了一圈,没研究明白,因为复现起来很困难,所以请教大家,代码中读取 isRefreshing 是安全的吗?我让 cursor 和 copilot 解释都是说 js 不启用 worker 是不存在并发问题的,但是从结果来看,确实有不止一个请求进入了刷新 token 的分支,我把这个情况描述完,cursor 让我引入 sync-mutex 加锁,和一开始的解释完全不一样,我在 StackOverflow 和 medium 中也找到几篇类似的文章,都是借助了防抖/记忆函数来解决,实在弄不清楚这块读写 isRefreshing 到底是不是安全的。
还看到了一篇锁的文章,感觉很类似我遇到的这个问题: https://jackpordi.com/posts/locks-in-js-because-why-not
伪代码如下:
let isRefreshing = false // 标记是否正在刷新 token let requests: Array<(token: string, err?: string) => void> = [] // 需要重试的请求列表 client.interceptors.response.use((response: AxiosResponse) => { const { config, status } = response const { code } = response.data if (status >= 500) { return Promise.reject("服务器错误") } else if (code == 10003) { // access token 过期,尝试刷新 token const { refreshToken } = user.useLoginStore.getState() if (refreshToken) { // FIXME: ?? 存在并发读取 isRefreshing 为 false 导致发出多次刷新 token 的请求 if (!isRefreshing) { isRefreshing = true return refreshToken() .then(({ data }) => { const { code } = data if (code === 10000) { user.useLoginStore.setState((state) => { state.token = data.data.token }) config.headers["Authorization"] = data.data.token const retry = client(config) requests.forEach((cb) => cb(data.data.token)) requests = [] return retry } else { return Promise.reject(data.message) } }) .catch((err) => { const msg = isError(err) ? err.message : err requests.forEach((cb) => cb("", msg)) requests = [] publishInvalidTokenEvent(msg) }) .finally(() => { isRefreshing = false }) } else { return new Promise((resolve, reject) => { requests.push((token: string, err?: string) => { if (err) { reject(err) } else { config.headers["Authorization"] = token resolve(client(config)) } }) }) } } else { requests.forEach((cb) => cb("", "登录过期") requests = [] publishInvalidTokenEvent("登录过期") } } else if (code === 10000) { return response.data.data } else if (code == 10006) { // 长 token 失效 requests.forEach((cb) => cb("", "登录过期") requests = [] publishInvalidTokenEvent("登录过期") } else { return Promise.reject(response.data.message || response.data.msg) } }) 