善用 Promise.all

async_function 的出现,让异步代码写起来更方便的同时可能也会让人忽略一些性能问题,比如我今天优化的一个异步日志功能。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async function doLog(work) {
const logs = []
await work((log) => logs.push(log))
console.log(logs)
}

async function work1(log) {
await someAsyncWork()
log(1)
}

async function work2(log) {
await someAsyncWork()
log(2)
}

// log [1, 2]
doLog(async (log) => {
await work1(log)
await work2(log)
})

以上代码可以看出 work2 需要等待 work1 ,且业务上确实需要保证 work1 的日志输出要先于 work2 ,所以看起来这么写是没啥毛病的。

不过对于这个需求来说,仍然是可以优化的,通过重新设计 doLog 函数,让 work1work2 既可以同时执行又可以保障日志的输出顺序。思路是:doLog 内部使用 Promise.all 同时执行多个异步任务,各自任务拥有各自的 log 队列,而这些队列又按照 works 顺序排列。

代码如下:

1
2
3
4
5
6
7
8
9
10
async function doLog(...works) {
const allLogs = works.map(() => [])
await Promise.all(
works.map((work, index) => work((log) => allLogs[index].push(log))),
)
console.log([].concat.apply([], allLogs))
}

// log [1, 2]
doLog(work1, work2)

这里假设 work1 的执行始终需要 2s,work2 的执行始终需要 1s。

那么理论上原版需要耗时 3s,优化版的耗时会始终等于耗时最长的任务也就是 2s,总共节约了 1s。显然 work 队列越大节约的时间还会越多。

Promise 缓存

请求数据是一个常见的需求,一般来说开发者都会设计一个支持数据缓存的请求层。然而更深一步想,除了数据缓存,请求本身是否也可以缓存复用呢?

场景如下:

A,B 两个组件都会请求一个设置了数据缓存的 **url: ‘/xxx’**。

假设 B 的请求刚好发生在 A 请求结束的时候,这时候数据缓存会命中,至始至终只发起了一次请求;那么如果 B 的请求刚好发生在 A 请求的过程中,数据缓存不会被命中,相同的请求会在间隔相当短的时间内发起两次。

所以,上述场景展示了数据缓存的不足。如果你的请求层刚好是一个基于 Promise 的设计,那么稍作改造就可以天然支持数据缓存和请求缓存。

代码如下:

1
2
3
4
const cache = {}
export function get(url) {
return cache[url] || (cache[url] = somePromiseGet(url))
}

如此,上述情况 A 和 B 会复用同一个请求

善用 await 做归一化处理

好像很多人都不知道可以直接 await 一个非 Promise 函数,这在做兼容的时候很方便。

比如,写一个弹窗组件,onOk 支持传入一个普通函数或 Promise 函数。如果是 Promise 函数弹窗会在 resolve 后自动关闭。基于以上特性核心部分只需要:

1
2
await this.onOk()
this.visible = false

而不用

1
2
3
4
5
6
const ret = this.onOk()
if (ret && typeof ret.then === 'function') {
ret.then(() => (this.visible = false))
} else {
this.visible = false
}

 评论