用 Proxy 和 WeakMap 实现一个类似 @vueuse 的 util 方法
2024-11-18 • 4min
在使用 Vueuse 时,我发现一个很好用的方法util。
该方法的作用是监听某个响应式变量,阻塞当前方法直到该变量符合要求。
调用形式如下:
ts1// 阻塞当前方法直到 ref.value === true 或 1000ms 后 2await until(ref).toBe(true, { timeout: 1000 })
这个方法可以解决许多工程难题,比如双 token 方案中,用户一次触发了多个请求,但是在第一个请求时 token 就过期了,此时应该发送刷新 token 的请求,并阻塞其他的请求,直到 token 刷新成功。
调用方式类似于:
ts1const 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。
ts1function 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 包装函数,接收初始值,返回一个被代理的对象:
ts1function 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 来新建存储桶:
ts1const effectMap = new WeakMap<object, Array<() => void>>()
在 util 函数中将回调函数写入存储桶:
ts1function 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 中的回调并执行即可:
ts1set(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