c++ 回调函数_Node.js从零开始——异步编程与回调

f625e5cee19420af981eee9535d15150.png

其实计算机在设计上就是异步的,异步意味着事情可以独立于主程序流而发生。

在当前的用户计算机中,每个程序都运行于特定的时间段,然后停止执行,以让另一个程序继续执行, 这件事运行得如此之快,以至于无法察觉,这让我们以为计算机可以同时运行许多程序,但这是一种错觉(在多处理器计算机上除外,那是因为不同的计算核心 CPU,可以在物理上同时进行计算,从而多任务并行处理,不过这个也取决于操作系统对多核心的支持)。

程序在内部会使用中断,一种被发送到处理器以获取系统关注的信号;这里不会深入探讨这个问题,只要记住,程序是异步的且会暂停执行直到需要关注,这使得计算机可以同时执行其他操作,当程序正在等待来自网络的响应时,则它无法在请求完成之前停止处理。

通常,编程语言是同步的,有些会在语言或库中提供管理异步性的方法,默认情况下,CJavaC#PHPGoRubySwift Python 都是同步的, 其中一些语言(譬如我有少许了解的 JavaC #Python)通过使用线程(衍生新的进程)来处理异步操作。

1 JavaScript 的异步性

JavaScript 默认情况下是同步的,并且是单线程的,这意味着代码无法创建新的线程并且不能并行运行。

代码行是依次执行的,例如:

const 

但是 JavaScript 诞生于浏览器内部,一开始的主要工作是响应用户的操作,例如 onClickonMouseOveronChangeonSubmit 等,使用同步的编程模型该如何做到这一点?

答案就在于它的环境: 浏览器通过提供一组可以处理这种功能的 API 来提供了一种实现方式。

而在 Node.js 这里,引入了非阻塞的 I/O 环境,以将该概念扩展到文件访问、网络调用等。

2 回调

我们不知道用户何时单击按钮,因此,为点击事件定义了一个事件处理程序,该事件处理程序会接受一个函数,该函数会在该事件被触发时被调用:

document

这就是所谓的回调。

回调是一个简单的函数,会作为值被传给另一个函数,并且仅在事件发生时才被执行; 之所以这样做,是因为 JavaScript 具有顶级的函数,这些函数可以被分配给变量并传给其他函数(称为高阶函数)。

通常会将所有的客户端代码封装在 window 对象的 load 事件监听器中,其仅在页面准备就绪时才会运行回调函数:

window

回调无处不在,不仅在 DOM 事件中,一个常见的示例是使用定时器:

setTimeout

XHR 请求也接受回调,在此示例中,会将一个函数分配给一个属性,该属性会在发生特定事件(在该示例中,是请求状态的改变)时被调用:

const 

3 处理回调中的错误

如何处理回调的错误?

一种非常常见的策略是使用 Node.js 所采用的方式:任何回调函数中的第一个参数为错误对象(即错误优先的回调)。

如果没有错误,则该对象为 null;如果有错误,则它会包含对该错误的描述以及其他信息。

fs

当然也有一种是 jQurey 常用的,将错误对象放在最后,比如 jQuery AJAX

91d7dec58e1e8d63b92514bb5687f95d.png

当然这里因为 jQuery AJAX 的定义,我们是把处理方法都统一写在了参数选项里面,所以稍稍有点繁琐;如果用 $.get() 这个语法糖就相对好理解一点:

09e6b1754ff4037fef337b2a2b0c40cb.png

当然这种方式,已经不算是回调了,更像是 Promise;具体想了解,可以考虑看看 jQuery,毕竟它依旧是世界上最流行的 JS 库。

4 回调的问题

回调适用于简单的场景!

但是,每个回调都可以添加嵌套的层级,并且当有很多回调时,代码就会很快变得非常复杂,难以辨认(也就是所谓的回调地狱):

window

这只是一个简单的 4 个层级的代码,但还有更多层级的嵌套,这很不好;该如何解决?

5 回调的替代方法

ES 6 开始,JavaScript 引入了一些特性,可以帮助处理异步代码而不涉及使用回调:PromiseES 6)和 Async/AwaitES 2017),都是非常好的替代方式,这样回调可以修改成更为直观的链式操作,譬如上面的例子:

$

再比如 Promise 的例子(当然是很简单的例子):

const 

再加上使用 Async/Await 的例子:

83340d053d8e3ff9216bca3ff18933aa.png

这样语义就清晰了很多,自然也会对开发减少 bug 起到很大的作用;这里要明确一点,回调和 Promise 对电脑来说是没什么大区别的,它所负责的就是执行,Promise 对象的结构是为了让编程人员减少语义混乱的;如果完全用计算机生成语句,几万行的 if...else 也是可能的吧(哈哈,说的就是《了不起修仙模拟器》那个传闻中的笑话)。

6 Promise 简介

Promise 通常被定义为最终会变为可用值的代理,它是一种处理异步代码(而不会陷入回调地狱)的方式。

多年来,promise 已成为语言的一部分(在 ES 6 中进行了标准化和引入),并且最近变得更加集成,在 ES 2017 中具有了 asyncawait

异步函数在底层使用了 promise,因此了解 promise 的工作方式是了解 async await 的基础。

6.1 Promise 如何运作

promise 被调用后,它会以处理中状态开始; 这意味着调用的函数会继续执行,而 promise 仍处于处理中直到解决为止,从而为调用的函数提供所请求的任何数据。

被创建的 promise 最终会以被解决状态或被拒绝状态结束,并在完成时调用相应的回调函数(传给 then catch)。

6.2 哪些 JS API 使用了 promise?

除了自己的代码和库代码,标准的现代 Web API 也使用了 promise,例如:

  • Battery API
  • Fetch API
  • Service Worker

在现代 JavaScript 中,不太可能没有使用 promise,因此我们需要深入研究它们。

6.3 创建 promise

Promise API 公开了一个 Promise 构造函数,可以使用 new Promise() 对其进行初始化:

const 

这里 promise 检查了 done 全局常量,如果为真,则 promise 进入被解决状态(因为调用了 resolve 回调);否则,则执行 reject 回调(将 promise 置于被拒绝状态);如果在执行路径中从未调用过这些函数之一,则 promise 会保持处理中状态。

使用 resolve reject,可以向调用者传达最终的 promise 状态以及该如何处理;在上面的例子中,只返回了一个字符串,但是它可以是一个对象,也可以为 null;由于已经创建了 promise,因此它已经开始执行。

一个更常见的示例是一种被称为 Promisifying 的技术,这项技术能够使用经典的 JavaScript 函数来接受回调并使其返回 promise(就是 return new Promise()):

const 
在最新版本的 Node.js 中,无需为大多数 API 进行手动地转换;如果需要 promisifying 的函数具有正确的签名,则 ef="http://nodejs.cn/api/util.html#util_util_promisify_original">util 模块中有一个 promisifying 函数可以完成此操作。

6.4 使用 promise

const 

运行 checkIfItsDone() 会指定当 isItDoneYet promise 被解决(在 then 调用中)或被拒绝(在 catch 调用中)时执行的函数。

6.5 链式 promise

Promise 可以返回到另一个 promise,从而创建一个 promise 链。

链式 promise 的一个很好的示例是 Fetch API,可以用于获取资源,且当资源被获取时将 promise 链式排队进行执行。

Fetch API 是基于 promise 的机制,调用 fetch() 相当于使用 new Promise() 来定义 promsie

下面是个例子:

9701f65674421ca2d2bbfa445ca40f8a.png

在此示例中,调用 fetch() 从域根目录中的 stock.json 文件中获取了 stock 列表,并创建一个 promise 链。

运行 fetch() 会返回一个响应,该响应具有许多属性,在属性中引用了:

  • status,表示 HTTP 状态码的数值
  • statusText,状态消息,如果请求成功,则为 OK

response 还有一个 json() 方法,该方法会返回一个 promise,该 promise 解决时会传入已处理并转换为 JSON 的响应体的内容。

因此,考虑到这些前提,发生的过程是:链中的第一个 promise 是我们定义的函数,即 status(),它会检查响应的状态,如果不是成功响应(介于 200 299 之间),则它会拒绝 promise

此操作会导致 promise 链跳过列出的所有被链的 promise,且会直接跳到底部的 catch() 语句(记录请求失败的文本和错误消息)。

如果成功,则会调用定义的 json() 函数;由于上一个 promise 成功后返回了 response 对象,因此将其作为第二个 promise 的输入。

在这个例子中,返回处理后的 JSON 数据,因此第三个 promise 直接接收 JSON

.

只需将其记录到控制台即可。

6.6 处理错误

在上一节的示例中,有个 catch 被附加到了 promise 链上。

promise 链中的任何内容失败并引发错误或拒绝 promise 时,则控制权会转到链中最近的 catch() 语句。

new 

6.7 级联错误

如果在 catch() 内部引发错误,则可以附加第二个 catch() 来处理,依此类推:

new 

6.8 编排 promise

(1)Promise.all()

如果需要同步不同的 promise,则 Promise.all() 可以帮助定义 promise 列表,并在所有 promise 都被解决后执行一些操作。

示例:

const 

ES 6 解构赋值语法也可以执行:

Promise

当然,不限于使用 fetch,任何 promise 都可以以这种方式使用。

(2)Promise.race()

当传给其的首个 promise 被解决时,则 Promise.race() 开始运行,并且只运行一次附加的回调(传入第一个被解决的 promise 的结果)。

示例:

const 

6.9 常见的错误

(1)Uncaught TypeError: undefined is not a promise

如果在控制台中收到 Uncaught TypeError: undefined is not a promise 错误,则请确保使用 new Promise() 而不是 Promise()

(2)UnhandledPromiseRejectionWarning

这意味着调用的 promise 被拒绝,但是没有用于处理错误的 catch; 在 then 之后添加 catch 则可以正确地处理。

7 async/await

JavaScript 在很短的时间内从回调发展到了 promiseES 6 或者 ES 2015),且自 ES 2017 以来,异步的 JavaScript 使用 async/await 语法甚至更加简单。

异步函数是 promise 和生成器的组合,基本上,它们是 promise 的更高级别的抽象; 而 async/await 建立在 promise 之上。

7.1 为什么引入 async/await

它们减少了 promises 自身的语法复杂性,且减少了 promise 链的“不破坏链条”的限制。

ES 6 中引入 Promise 时,本来旨在解决异步代码的问题,并且确实做到了,但是很明显,promise 不可能成为最终的解决方案:Promise 反而引入了语法复杂性。

故而在 ES 2017 当中,async/await 出现了,它们可以向开发人员提供更容易理解和更简洁的语法,它们使代码看起来像是同步的,但实际上是异步的并且在后台无阻塞。

7.2 工作原理

异步函数会返回 promise,例如以下示例:

const 

当要调用此函数时,则在前面加上 await,然后调用的代码就会停止直到 promise 被解决或被拒绝; 注意:客户端函数必须被定义为 async

这是一个示例:

const 

7.3 简单的示例

这是一个 async/await 的简单示例,用于异步地运行函数:

f6fa4d8721e5ce6832e6469592365ca6.png

可以看到,“完成”放在了最后打印在控制台。

在任何函数之前加上 async 关键字意味着该函数会返回 promise;即使没有显式地这样做,它也会在内部返回 promise

这就是为什么下面代码有效的原因:

c77073b697563757d3988072337667db.png

上面的代码,其实就和以下代码一样:

const 

7.4 代码更容易阅读

如在上面的示例中所见,代码看起来非常简单(相对于使用普通的 promise、链式和回调函数的代码)。

不过由于这是一个非常简单的示例,其主要的好处要在代码更复杂时才会看到。

例如,这是使用 promise 获取并解析 JSON 资源的方法:

const 

这是使用 await/async 提供的相同功能:

const 

看起来后者更像是同步的代码,仅仅是多增加了 await 命令,这样对编程人员来说更容易理解,也更不容易出错。

7.5 多个异步函数串联

异步函数可以很容易地链接起来,并且语法比普通的 promise 更具可读性:

41bbc4a9e8a7bd7ba5f92a892fab0cf4.png

当然英文语法上这句话有问题,哈哈。

7.6 更容易调试

调试 promise 很难,因为调试器不会跳过异步的代码。

Async/await 使调试更容易,因为对于编译器而言,它就像同步代码一样。


上面就是异步的原理,其中有部分和以前的 JavaScript从零开始——异步操作(1)这个部分有重合,不过有鉴于为了说清楚 Node.js 的异步原理,这些部分是必不可少的,同样也是一个复习过程吧,哈哈;接下来就是 Node.js 的组件了,比如事件处理、文件处理、HTTP 服务等等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值