vlambda博客
学习文章列表

基操勿 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,希望它能给我们带来更多的惊喜。