Node.js教程:新手入门
2,异步工作
### 2.1 异步流控制 ##### 2.1.1 回调处理 > avaScript 控制流就是回调处理。函数式编程风格解决 "回调地狱"。 1. 启动器风格/输入 2. 中间件 3. 终止符 ```javascript //终止符 function final(someInput, callback) { callback(`${someInput} and terminated by executing callback `); } //中间件 function middleware(someInput, callback) { return final(`${someInput} touched by middleware `, callback); } //"启动器风格/输入" 是序列中的第一个函数,接受原始输入(如果有)以进行操作 function initiate() { const someInput = 'hello this is a function '; middleware(someInput, function (result) { console.log(result); // requires callback to `return` result }); } initiate(); ``` ##### 2.1.2 状态管理 函数可能依赖于状态,也可能不依赖于状态。 当函数的输入或其他变量依赖于外部函数时,就会出现状态依赖。 使用全局变量管理状态通常是一种草率的反模式,这使得保证状态变得困难或不可能。 应尽可能避免复杂程序中的全局变量。 **状态管理有两种主要策略:** 1. 将变量直接传递给函数,并且 2. 从缓存、会话、文件、数据库、网络或其他外部源获取变量值。 ##### 2.1.3 控制流 如果对象在内存中可用,则可以进行迭代,并且控制流不会发生变化,但是,如果数据存在于内存之外,迭代将不再有效。为什么会这样? `setTimeout` 指示 CPU 将指令存储在总线上的其他位置,并指示数据被安排在稍后的时间拾取。 在函数再次到达 0 毫秒标记之前经过了数千个 CPU 周期,CPU 从总线获取指令并执行它们。 唯一的问题是歌曲 ('') 在数千个循环之前被返回。 在处理文件系统和网络请求时也会出现同样的情况。 主线程不能被阻塞一段不确定的时间 - 因此,我们使用回调以可控的方式及时安排代码的执行。 你将能够使用以下 3 种模式执行几乎所有操作: 1. **在系列中:** 函数将按照严格的顺序执行,这与 `for` 循环最相似。 2. **全并行:** 当顺序不是问题时,例如通过电子邮件发送 1,000,000 个电子邮件收件人的列表。 3. **有限并行:** 带限制的并行,例如成功向 10E7 用户列表中的 1,000,000 个收件人发送电子邮件。 ### 2.2 阻塞与非阻塞 ##### 2.2.1 阻塞 **阻塞** 是当 Node.js 进程中的其他 JavaScript 的执行必须等到非 JavaScript 操作完成时。 发生这种情况是因为在发生 **阻塞** 操作时事件循环无法继续运行 JavaScript。 在 Node.js 中,由于 CPU 密集而不是等待非 JavaScript 操作(例如 I/O)而表现出较差性能的 JavaScript 通常不称为 **阻塞**。 Node.js 标准库中使用 libuv 的同步方法是最常用的 **阻塞** 操作。 原生模块也可能有 **阻塞** 方法。 Node.js 标准库中所有的 I/O 方法都提供异步版本,**非阻塞**,接受回调函数。 一些方法也有对应的 **阻塞**,其名称以 `Sync` 结尾。 ```javascript const fs = require('fs'); //同步读取文件 const data = fs.readFileSync('/file.md'); // blocks here until file is read //异步读取文件 fs.readFile('/file.md', (err, data) => { if (err) throw err; }); ``` > 请注意,在同步版本中,如果抛出错误,则需要将其捕获,否则进程将崩溃。 在异步版本中,由作者决定是否应如图所示抛出错误。 ##### 2.2.2 并发和吞吐量 Node.js 中的 JavaScript 执行是单线程的,因此并发是指事件循环在完成其他工作后执行 JavaScript 回调函数的能力。 任何预期以并发方式运行的代码都必须允许事件循环在非 JavaScript 操作(如 I/O)发生时继续运行。 例如,让我们考虑这样一种情况:每个对 Web 服务器的请求都需要 50 毫秒才能完成,而这 50 毫秒中有 45 毫秒是可以异步完成的数据库 I/O。 选择 **非阻塞** 异步操作可以释放每个请求 45 毫秒的时间来处理其他请求。 这仅仅是选择使用 **非阻塞** 方法而不是 **阻塞** 方法在容量上的显着差异。 ##### 2.2.3 混合阻塞和非阻塞代码的危险 ```javascript const fs = require('fs'); fs.readFile('/file.md', (err, data) => { if (err) throw err; console.log(data); }); fs.unlinkSync('/file.md'); //fs.unlinkSync() 很可能在 fs.readFile() 之前运行,这将在实际读取之前删除 file.md。 //修改:在 fs.readFile() 的回调中放置了对 fs.unlink() 的 非阻塞 调用,这保证了正确的操作顺序 fs.readFile('/file.md', (readFileErr, data) => { if (readFileErr) throw readFileErr; console.log(data); fs.unlink('/file.md', unlinkErr => { if (unlinkErr) throw unlinkErr; }); }); ``` ### 2.3 JavaScript 异步编程和回调 JavaScript 默认是 **同步的** 并且是单线程的。 这意味着代码无法创建新线程并并行运行。 回调是一个简单的函数,它作为值传递给另一个函数,并且只会在事件发生时执行。 我们可以这样做是因为 JavaScript 具有一流的函数,可以将其分配给变量并传递给其他函数(称为 **高阶函数**) ```javascript //你 为点击事件定义一个事件处理程序。 此事件处理程序接受一个函数,该函数将在事件触发时调用,这就是所谓的回调 document.getElementById('button').addEventListener('click', () => { // item clicked }); //通常将所有客户端代码封装在 window 对象上的 load 事件监听器中,它仅在页面准备就绪时运行回调函数: window.addEventListener('load', () => { // window loaded // do what you want }); ``` - **处理回调中的错误** 你如何处理回调错误? 一种非常常见的策略是使用 Node.js 采用的策略: 任何回调函数中的第一个参数都是错误对象: **错误优先回调** 如果没有错误,对象就是 `null`。 如果有错误,它包含错误的一些描述和其他信息。 ```javascript const fs = require('fs'); fs.readFile('/file.json', (err, data) => { if (err) { // handle error console.log(err); return; } // no errors, process data console.log(data); }); ``` - **回调的问题** 回调非常适合简单的情况!然而,每个回调都会增加一层嵌套,当你有很多回调时,代码很快就会变得复杂起来。从 ES6 开始,JavaScript 引入了几个特性来帮助我们处理不涉及使用回调的异步代码: Promises (ES6) 和 Async/Await (ES2017)。 ### 2.4 process.nextTick() > 每次事件循环完成一次完整的行程,我们称之为**滴答**。 当我们将一个函数传递给 `process.nextTick()` 时,我们指示引擎在当前操作结束时、在下一个事件循环滴答开始之前 调用此函数,这是我们可以告诉 JS 引擎异步处理函数的方式(在当前函数之后),但要尽快,而不是将其排队。 调用 `setTimeout(() => {}, 0)` 将在下一个滴答结束时执行该函数,比使用 `nextTick()` 时要晚得多,后者优先调用并在下一个滴答开始之前执行它。 当你想确保在下一次事件循环迭代中该代码已被执行时,请使用 `nextTick()`。 ```javascript console.log("Hello => number 1"); setImmediate(() => { console.log("Running before the timeout => number 2"); }); setTimeout(() => { console.log("The timeout running last => number 3"); }, 0); process.nextTick(() => { console.log("Running at next tick => number 4"); }); //Hello => number 1 //Running at next tick => number 4 //Running before the timeout => number 2 //The timeout running last => number 3 ``` ### 2.5 JavaScript 定时器 ##### setTimeout() > setTimeout:指定一个稍后执行的回调函数,以及一个表示你希望它稍后运行的值,以毫秒为单位,即延迟执行 ```javascript //指定一个稍后执行的回调函数,以及一个表示你希望它稍后运行的值,以毫秒为单位 setTimeout(() => { // runs after 2 seconds }, 2000); //定义了一个新函数。 你可以在那里调用你想要的任何其他函数,或者你可以传递现有的函数名称和一组参数 const myFunction = (firstParam, secondParam) => { // do something }; // runs after 2 seconds setTimeout(myFunction, 2000, firstParam, secondParam); //回计时器 ID。 这个一般不用,但是你可以把这个 id 存起来,如果你想删除这个定时执行的函数,就把它清空 const id = setTimeout(() => { // should run after 2 seconds }, 2000); clearTimeout(id); //零延迟 //如果将超时延迟指定为 0,则回调函数将尽快执行,但在当前函数执行之后 //这对于避免在密集任务上阻塞 CPU 并在执行繁重计算时通过在调度程序中排队函数来执行其他函数特别有用 setTimeout(() => { console.log('after '); }, 0); ``` ##### setInterval() > `setInterval` 是一个类似于 `setTimeout` 的函数,不同的是: 它不会运行一次回调函数,而是会在你指定的特定时间间隔(以毫秒为单位)永远运行它 ```javascript const id = setInterval(() => { // runs every 2 seconds }, 2000); clearInterval(id); //通常在 setInterval 回调函数中调用 clearInterval,让它自动确定是否应该再次运行或停止 const interval = setInterval(() => { if (App.somethingIWait === 'arrived') { clearInterval(interval); } // otherwise do things }, 100); ``` ##### 递归设置超时 `setInterval` 每 n 毫秒启动一个函数,而不考虑函数何时完成执行。 为避免这种情况,你可以安排一个递归的 setTimeout 在回调函数完成时调用: ```javascript const myFunction = () => { // do something setTimeout(myFunction, 1000); }; setTimeout(myFunction, 1000); ``` ### 2.6 setImmediate() 当你想异步执行一段代码,但尽可能快地执行时,一个选择是使用 Node.js 提供的 `setImmediate()` 函数: ```javascript jscopysetImmediate(() => { // run something }); ``` 作为 setImmediate() 参数传递的任何函数都是在事件循环的下一次迭代中执行的回调。 `setImmediate()` 与 `setTimeout(() => {}, 0)`(传递 0 毫秒超时)以及 `process.nextTick()` 和 `Promise.then()` 有何不同? 在当前操作结束后,传递给 `process.nextTick()` 的函数将在事件循环的当前迭代中执行。 这意味着它将始终在 `setTimeout` 和 `setImmediate` 之前执行。 延迟为 0 毫秒的 `setTimeout()` 回调与 `setImmediate()` 非常相似。 执行顺序将取决于各种因素,但它们都将在事件循环的下一次迭代中运行。 `process.nextTick` 回调添加到 `process.nextTick queue`。 `Promise.then()` 回调添加到 `promises microtask queue`。 `macrotask queue` 添加了 `setTimeout`、`setImmediate` 回调。 事件循环先执行 `process.nextTick queue` 中的任务,然后执行 `promises microtask queue`,再执行 `macrotask queue`。 ### 2.7 Node.js 事件触发器 如何在 Node.js 中使用自定义事件?这个模块特别提供了 `EventEmitter` 类,它来处理我们的事件。 - `emit` 用于触发事件 - `on` 用于添加事件触发时执行的回调函数 - `once()`: 添加一次性监听器 - `removeListener()` / `off()`: 从事件中删除事件监听器 - `removeAllListeners()`: 删除事件的所有监听器 ```typescript const EventEmitter = require('events'); const eventEmitter = new EventEmitter(); //创建一个 start 事件 并触发 eventEmitter.on('start', () => { console.log('started'); }); eventEmitter.emit('start'); //将参数作为附加参数传递给 emit() 来将参数传递给事件处理程序 eventEmitter.on('start', number => { console.log(`started ${number}`); }); eventEmitter.emit('start', 23); //多个参数 eventEmitter.on('start', (start, end) => { console.log(`started from ${start} to ${end}`); }); eventEmitter.emit('start', 1, 100); ```
顶部
收展
底部
[TOC]
目录
1,新手入门
2,异步工作
3,文件操作
4,命令行
5,yarn 依赖管理
相关推荐
Node.js接口
Node.js:ExpressWeb
朴灵《深入浅出 Node.js》