Ray-D-Song's Blog

用 Proxy 和 WeakMap 实现一个类似 @vueuse 的 util 方法

2024-11-18 4min

在使用 Vueuse 时,我发现一个很好用的方法util

该方法的作用是监听某个响应式变量,阻塞当前方法直到该变量符合要求。

调用形式如下:

ts
1// 阻塞当前方法直到 ref.value === true 或 1000ms 后 2await until(ref).toBe(true, { timeout: 1000 })

这个方法可以解决许多工程难题,比如双 token 方案中,用户一次触发了多个请求,但是在第一个请求时 token 就过期了,此时应该发送刷新 token 的请求,并阻塞其他的请求,直到 token 刷新成功。

调用方式类似于:

ts
1const status = reactive('idle') 2 3async function fetcher() { 4 // 阻塞其他请求的执行,直到 status 变为 idle 5 await util(status, 'idle') 6 7 // ... 8 9 if (res.status === 401) { 10 // 401 未授权时阻塞其他请求的执行 11 status.value = 'blocked' 12 await refreshToken() 13 // 刷新成功后修改状态 14 status.value = 'idele' 15 } 16}

Vue 的 util 依赖于 Vue 的响应式系统,我希望实现一个框架无关的 util 方案,实现一个简单的值监听效果。
因为 util 是一个阻塞操作,所以应该返回一个 Promise。

ts
1function util(reactive: unknown, targetValue: unknown) { 2 return new Promise(resolve => { 3 // 直到 reactive 的值和 targetValue 相等时 resolve 4 }) 5}

问题来了,JavaScript 不包含直到这样的语义或 API,但我们可以监听 reactive 值的变化,将 resolve 作为回调传递,每次变化时对比新值和 targetValue 是否相等,相等的话就执行回调。

为了实现这样一个监听器,我们可以参考 Vue,Vue 的响应式库 @vue/reactivity 的核心是基于 Proxy 的写入和读取代理。
首先编写一个 reactive 包装函数,接收初始值,返回一个被代理的对象:

ts
1function reactive<T>(value: T): Reactive<T> { 2 const proxyed = new Proxy({ value }, { 3 set(target, prop, newValue) { 4 target[prop as keyof typeof target] = newValue 5 return true 6 } 7 }) 8 return proxyed 9}

这样在修改proxyed.value的时候就会触发 set 方法。 接下来我们需要一个存储桶来存储所有的回调。
使用 WeakMap 来新建存储桶:

ts
1const effectMap = new WeakMap<object, Array<() => void>>()

在 util 函数中将回调函数写入存储桶:

ts
1function util<T>(reactive: Reactive<T>, targetValue: unknown) { 2 return new Promise(resolve => { 3 // 如果初始值就相同,直接 resolve 4 if (reactive.value === targetValue) { 5 resolve(true) 6 return; 7 } 8 // 获取已有的 callback 9 const cbs = effectMap.get(reactive) || [] 10 // 写入新的 callback 11 cbs.push(() => { 12 // 当值等于目标值时 resolve 13 if (reactive.value === targetValue) { 14 resolve(true) 15 } 16 }) 17 }) 18}

接下来就只需要在每次 set 操作时获取 effectMap 中的回调并执行即可:

ts
1set(target, prop, newValue) { 2 target[prop as keyof typeof target] = newValue 3 const cbs = effectMap.get(raw) 4 if (cbs) cbs.forEach(cb => cb()) 5 return true 6}

完整的代码可以参考:https://github/ray-d-song/EchoRSS/web/src/lib/util.ts