JS 在浏览器的网页中执行,浏览器给 JS 提供的能力是操作文字、图片,或实现一些简单效果。术语叫 DOM 操作。
JS 在 Node.js 环境中执行, Node 给 JS 提供了诸如 文件操作, 网络操作 等功能模块。基于这些模块,JS 一下就牛气冲天了。
在 Node.js 的加持下,JS 可以写服务器程序了,可以写大量实用工具了。进而推动了今天前端领域的蓬勃发展。
JS 语法还是那个语法,平台不同,则产生了不同的效果。
『人也一样,平台很重要。』
Node 环境下 JS 到底有啥不同?
典型的说法: 异步I/O、单线程、事件驱动。本次我们就通过『异步I/O』领略 Node.js 的优秀之处。
比如统计一个目录中文件大小总和,过程中要获取每一个文件的大小信息,这就牵扯到非常典型的磁盘I/O操作。
同步I/O操作,是读取完一个文件信息,得到大小后, 再读取下一个文件。(它们的关系是阻塞的)
异步I/O操作,读取第一个文件时,不等信息返回,就发送读取第二个文件信息的指令。整个目录中文件信息的获取时间是非线性的。(或者说, 读取文件大小这些小任务之间是『非阻塞』的)
让我们通过代码体会一下 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风格APIasync 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 重视起来,不再把它当成一个玩具。
尊重技术, 尊重读者, 本篇文章知识点:
1. (node + js) 与 (浏览器 + js) 的不同?
2. 什么是阻塞 ? 什么是非阻塞 ?
3. reduce() 方法怎么用?
4. fs.promises API, Promise.all( ) 的真实应用场景?
5. 尽量结合使用的 async / await ?
6. 如何统计JS代码运行时间? 异步代码的小细节.
7. node 为何单线程? 能不能有多个线程?
8. 异步i/o、事件驱动,、单线程如何统一理解?