闭包的妙用,再次以 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 的大致姿势。