前言
你是否察觉到自己随手写的异步函数,实际却是"同步"的效果!
正文
以一个需求为例:获取给定目录下的全部文件,返回所有文件的路径数组。
第一版
思路很简单:读取目录内容,如果是文件,添加进结果数组,如果还是目录,我们递归执行。
js
import path from 'node:path'
import fs from 'node:fs/promises'
import { existsSync } from 'node:fs'
async function findFiles(root) {
if (!existsSync(root)) return
const rootStat = await fs.stat(root)
if (rootStat.isFile()) return [root]
const result = []
const find = async (dir) => {
const files = await fs.readdir(dir)
for (let file of files) {
file = path.resolve(dir, file)
const stat = await fs.stat(file)
if (stat.isFile()) {
result.push(file)
} else if (stat.isDirectory()) {
await find(file)
}
}
}
await find(root)
return result
}
机智的你是否已经发现了问题?
我们递归查询子目录的过程是不需要等待上一个结果的,但是第 19 行代码,只有查询完一个子目录之后才会查询下一个,显然让并发的异步,变成了顺序的"同步"执行。
那我们去掉 19 行的 await
是不是就可以了,当然不行,这样的话 await find()
在没有完全遍历目录之前就会立刻返回,我们无法拿到正确的结果。
思考一下,怎么修改它呢?......让我们看第二版代码。
第二版
js
import path from 'node:path'
import fs from 'node:fs/promises'
import { existsSync } from 'node:fs'
async function findFiles(root) {
if (!existsSync(root)) return
const rootStat = await fs.stat(root)
if (rootStat.isFile()) return [root]
const result = []
const find = async (dir) => {
const task = (await fs.readdir(dir)).map(async (file) => {
file = path.resolve(dir, file)
const stat = await fs.stat(file)
if (stat.isFile()) {
result.push(file)
} else if (stat.isDirectory()) {
await find(file)
}
})
return Promise.all(task)
}
await find(root)
return result
}
我们把每个子目录内容的查询作为独立的任务,扔给 Promise.all
执行,就是这个简单的改动,性能得到了质的提升,让我们看看测试,究竟能差多少。
对比测试
js
console.time('v1')
const files1 = await findFiles1('D:\\Videos')
console.timeEnd('v1')
console.time('v2')
const files2 = await findFiles2('D:\\Videos')
console.timeEnd('v2')
console.log(files1?.length, files2?.length)
版本二快了三倍不止,如果是并发的接口请求被不小心搞成了顺序执行,差距比这还要夸张。