基操勿 6 | Node.js 的异步I/O到底有多秀?
嗨,我是勾勾。
前几天发布的和 相关的内容让不少同学都炫技了一把,直呼“好家伙”。今天也给大家伙儿带来点好东西:)。
众所周知,JavaScript 作为网页中的执行脚本,通常是由浏览器来解释执行的。自从Node.js 出现后,解释执行 JavaScript 代码的工具又多了一种。
与浏览器不同的是,JavaScript 可以借助 Node.js 提供的功能模块去完成更多事情。比如创建、修改、删除文件;比如发送接收网络信息等等。因此, JavaScript 版本的服务器程序随之诞生,也就有了后来大量的 JavaScript 版本的实用工具。毫不夸张地讲,正是因为有了 Node.js,才有了今天前端的蓬勃发展。
与浏览器相比,Node.js 让 JS 功能更强大、更丰富。在 Node.js 加持下,JS 和 其它编程语言比较,有着与众不同的特点。
典型的说法是,Node.js具有异步I/O、单线程、事件驱动这些特点。其实这三个特点是一回事儿,不过是从三个角度来看待的。
接下来,我们就以『异步I/O』为切入口,来看看 Node.js 的优秀之处。
比如统计一个目录中文件大小总和,过程中要获取每一个文件的大小信息,这就牵扯到非常典型的磁盘I/O操作。
在不考虑多线程的情况下,有两种操作:
同步I/O操作。步骤是读取完一个文件信息,得到大小后, 再读取下一个文件。(它们的关系是阻塞的)
异步I/O操作。读取第一个文件时,不等信息返回,就发送读取第二个文件信息的指令。整个目录中文件信息的获取时间是非线性的。(或者说,读取文件大小这些小任务之间是『非阻塞』的)
PHP版本的代码如下:
function dirSize($dir) {
$hd = opendir($dir); // 打开目录,获取句柄
$total = 0; // 初始值
while(($f = readdir($hd)) !== false) {
// 跳过.和..
if ($f === '.' || $f === '..') {continue;}
// 获取每文件文件大小, 进行累加
$total += filesize($dir.'/'.$f);
}
return $total;
}
echo dirSize('./code');
一些同学可能没有接触过PHP,下面来一个JS版本方便大家理解。
JS同步版本:
const fs = require("fs")
function dirSizeSync(dir) {
// 1.获取目录中所有文件名
let files = fs.readdirSync(dir)
// 2.求每个文件大小,加到总和上, 最后返回
return files.reduce((totalSize, file) => {
return totalSize + fs.statSync(`${dir}/${file}`).size
}, 0)
}
// 测试. 统计 code 目录
console.log(dirSizeSync("./code"))
JS异步版本:
const fs = require("fs").promises // 采用promise风格API
async function dirSizeAsync(dir) {
// 1.读取目录中所有文件
let files = await fs.readdir(dir)
// 2.异步得到所有文件的相关信息, 返回的是一组promise
let arrPromiseStat = files.map((file) => fs.stat(`${dir}/${file}`))
// 3.等所有promise都完成后,返回一个结果数组
let stats = await Promise.all(arrPromiseStat)
// 4.对结果数组求和,并返回
return stats.reduce((total, stat) => total + stat.size, 0)
}
// 测试. 统计 ./code 目录
dirSizeAsync("./code").then((total) => {
console.log(total)
})
从代码量上看,异步版本更多。
但是,代码要跑一跑的:
// 统计同步版本用时
console.time("同步")
console.log(dirSizeSync("./code"))
console.timeEnd("同步")
// 统计异步版本用时
console.time("异步")
dirSizeAsync("./code").then((totalSize) => {
console.log(totalSize)
console.timeEnd("异步")
})
来看一下结果:
随着业务量增大,这两者的差距会更大。异步I/O的实现依赖于事件驱动,它使得一个线程能够被充分利用。多线程中处理任务经常要考虑复杂的线程同步问题,所以 Node 被有意设计为单线程,进而也减少了多线程间切换的开销。
值得说明的是,这并不是说 Node 就不可以开启多个线程。详细的原因这里不做过多解释。也正是这样一次练习让我们真正对 Node 重视起来,不再把它当成一个玩具。近期 Node 再次发布了新版本 15.0.1,希望它能给我们带来更多的惊喜。