闭包的妙用,再次以 memoize 举例

哈德韦

共 3685字,需浏览 8分钟

 ·

2021-11-30 15:04

在《闭包的妙用 —— memoize》一文中,以 memoize 为例,讲述了一个具体的闭包应用场景。最精彩的是,在文章最后,实现了 memoized 以完全避开心智负担。

但是,在实际工作中,经常碰到 async 场景,以及实际工程是用的 TypeScript 语言。那么今天再谈一谈 TypeScript 工程中使用 async memoize 的实例。


重复请求

这是一个很常见的前端问题,每换一个团队,总能遇到相同的问题,解决办法也一样,今天记录下来,希望给到没有经验的前端团队以参考,不要再重蹈覆辙。

这个常见问题就是,前端在做登录流程控制时,一般逻辑是这样:


            先发出一个请求,比如请求个人信息

            由于未登录,或者登录已经过期,服务器端返回了 403

            前端有个响应的通用错误处理,看到 403,就发出登录请求


这个逻辑很正确甚至优雅,但是没有考虑到并发。在并发场景下,多个请求收到 403,就会触发多次登录请求,造成不必要的流量和延时浪费。


以下是一个微信小程序前端的重复请求截图,它从微信获取 code,然后发请求去换取 session(这个例子用了 GraphQL,请求名为 loginWechat)。




先用测试重现这个问题

describe('login', () => {  beforeEach(() => {    jest.clearAllMocks()  })

it('should login wechat once without duplicate requests', async () => { const mockLoginResult = { jwt: '1234', sessionId: '5678' }

client.loginMutate.mockImplementation(async () => mockLoginResult)

// 连接调用两次登录处理 (实际代码就是在 403 后调用 handleLogin ) await Promise.all([auth.handleLogin(), auth.handleLogin()])

expect(storageSync.getStorageSync(StorageSyncKeys.TOKEN)).toStrictEqual(mockLoginResult.jwt)

// 直接运行测试会导致失败,因为这个 loginMutate 会被调用两次(发出两个请求) expect(client.loginMutate).toHaveBeenCalledTimes(1) })})


handleLogin 函数执行了一系列步骤,主要就是发出请求,拿到 token,保存到 storage。



export const handleLogin = () => login() .then(() => { initialUserStatus() }) .catch(() => { resetAuthInfo() messager.error('微信登录授权出错') })

export const login = (): Promise=> { AuthorizeStore.setIsGetNewToken(false) return new Promise(async (resolve, reject) => { try { const { code } = await Taro.login() const loginInfo = await loginWechat(code) AuthorizeStore.setIsGetNewToken(true) setTokenSync(loginInfo?.jwt) resolve(true) } catch (err) { AuthorizeStore.setIsGetNewToken(false) reject(false) } })}


以上是原来的代码,要修复,很简单,只需要把 login 使用 memoize 封装一下就好,但是这里使用了 async,不如把这个记忆函数叫做 memoizeAsync。



- export const login = (): Promise=> {+ export const loginWithNewCode = (): Promise=> { AuthorizeStore.setIsGetNewToken(false) return new Promise(async (resolve, reject) => { try { const { code } = await Taro.login() const loginInfo = await loginWechat(code) AuthorizeStore.setIsGetNewToken(true) setTokenSync(loginInfo?.jwt) resolve(true) } catch (err) { AuthorizeStore.setIsGetNewToken(false) reject(false) } })}

+ export const login = memoizeAsync(loginWithNewCode)


这里引入了 memoizeAsync,实现它之前,写个测试吧!


再用测试文档化新引入的函数的功能



describe('memoize', () => { it('executes only once', async () => { const originalExecute = jest.fn().mockImplementation(async () => { console.log('executing...') }) // Given an async function memoized const memoizedExecute = memoizeAsync(originalExecute)

const sut = { execute: memoizedExecute, }

// When call it multiple times await Promise.all([sut.execute(), sut.execute(), sut.execute()])

// Then the underlying method will be run only once expect(originalExecute).toHaveBeenCalledTimes(1) }) })


memoizeAsync 的实现



export const memoizeAsync =(func: (...args) => Promise) => { const cache = {}

return async (...args) => { const argStr = JSON.stringify(args) const cacheKey = `${func.toString()}${argStr}`

if (cache[cacheKey] !== undefined) { cache[cacheKey] = cache[cacheKey] } else { cache[cacheKey] = func(...args) }

return cache[cacheKey] }}


非常平凡,在返回新的函数前,定义了一个 cache 变量,虽然它对外界不可见,但是每次调用返回的新函数,都会间接引用到它。


再复习一遍:闭包就是一个特殊的高阶函数,在返回的新函数里引用了原函数里的局部变量。


在这个函数实现后,不仅文档测试跑过了,原来重现的问题也修好了。


总结


以 TypeScript 项目为例,再次复习了闭包以及 memoize,并且展示了 TDD 的开发和修 BUG 的大致姿势。


浏览 33
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报