回调地狱
Node
中有大量的异步 IO 操作,被封装成基于回调的函数,遇到复杂的业务逻辑很容易形成多级缩进的代码,在左侧形成一个由空格(或 Tab)组成的三角形,代码变得非常难读,被称为回调地狱。
1 | const fs = require('fs'); |
下面我们来看一下,怎么解决回调地狱问题。
解决方案
- 函数拆解并使用第三方异步库
- Promise
- 生成器函数
- 终极大招:async/await
函数拆解并使用第三方异步库
1 | const fs = require('fs'); |
使用第三方库
- 多个逻辑单元被分成独立的函数。
- 每个函数有了有意义的名称,更加易读。
- 依赖第三方异步类库解决回调地狱问题。
Promise
Promise 简介
Promise 成为 JavaScript API 的基石
从 Node 6.X 开始内置 Promise. JavaScript 相关生态中更多的 API 都开始基于 Promise 实现。比如下面的两段代码。
Battery API,提供了有关系统充电级别的信息并提供了通过电池等级或者充电状态的改变提醒用户的事件。 这个可以在设备电量低的时候调整应用的资源使用状态,或者在电池用尽前保存应用中的修改以防数据丢失。
1 | //获取设备电池相关数据 |
Fetch API 提供了一个 JavaScript 接口,用于访问和操纵 HTTP 管道的部分,例如请求和响应。它还提供了一个全局 fetch()方法,该方法提供了一种简单,合乎逻辑的方式来跨网络异步获取资源
1 | fetch('flowers.jpg') |
Promise 类方法简介
1 | Promise.resolve(1); |
1 | Promise.reject(1); |
Promise.all
:将多个 Promise 实例,包装成一个新的 Promise 实例。有一个 Promise 对象状态改变成rejected
,新的 Promise 实例的状态就改变成rejected
,否则等所有状态改变成fulfilled
,新的 Promise 实例的状态就改变成fulfilled
。
Promise.race
:将多个 Promise 实例,包装成一个新的 Promise 实例。有一个 Promise 对象状态改变,新的 Promise 实例的状态就改变,新的 Promise 实例的状态就是第一个改变状态的 Promise 实例的状态。
Promise 实例方法
Promise.prototype.then
Promise.prototype.catch
使用 util.promisify 转成基于 Promise 的函数
1 | const { promisify } = require('util'); |
自定义基于 Promise 的函数
使用fn[util.promisify.custom]
来定义基于 Promise 的接口。
1 | const util = require('util'); |
使用 Promise 解决回调问题
1 | //使用`util.promisify`转成基于Promise的接口 |
通过生成器函数
生成器函数是一个状态机,封装了多个内部状态。还是一个遍历器生成函数,返回遍历器对象,可以依次遍历生成器函数内部的每一个状态。
生成器函数执行器
co是一个基于生成器函数的流程控制工作,可用于 Node.js 和浏览器。它可以通过 Promise 让你的非阻塞代码以一种漂亮的方式呈现。
1 | let print = val => console.log(val); |
1 | var fn = co.wrap(function*(val) { |
使用生成器函数解决回调地狱问题
1 | //使用`util.promisify`转成基于Promise的接口 |
终极大招:async/await
生成器函数和 async 函数比较
使用生成器函数
1 | const { promisify } = require('util'); |
使用async
函数
1 | var fn = async function() { |
async
函数和生成器函数非常相似,基本上就是*
换成async
,yield
换成了await
。
async 函数的优点
- 内置了执行器
生成器函数的执行必须靠执行器,所以才有了 co 模块,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。 - 更好的语义
async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。 - 更广的适用性。
co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。 - 返回值是 Promise。
async 函数的返回值是 Promise 对象,这比生成器函数的返回值是 Iterator 对象方便多了。你可以用 then 方法指定下一步的操作。
进一步说,async 函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖。
使用 async 函数解决回调地狱问题
1 | const { promisify } = require('util'); |
总结
- 回调地狱问题可以通过第三方异步类库,Promise,生成器函数和 async 函数等方式解决。
- Promise 是 ECMAScript 中异步 API 的基石,需要重点掌握。
- async 函数解决异步问题更加优雅,推荐在 Node 中使用。
参考资料
- Node.js 8: util.promisify()
- ECMAScript 6 入门 阮一峰
- co 生成器函数执行器