如何避免回调地狱
栏目: JavaScript · 发布时间: 6年前
内容简介:目前有几个比较好的解决方法以上代码包括了三个异步操作:我们每增加一个异步请求,就会多添加一层回调函数的嵌套,这样下去,可读性会越来越低,也不易于以后的代码维护。过多的回调也就让我们陷入“回调地狱”。接下来会大概介绍一下规避回调地狱的方法。
目前有几个比较好的解决方法
- 拆解function
- 事件发布/监听模式
- Promise
- generator
- async/await
先来看一点代码
fs.readFile('./sample.txt', 'utf-8', (err, content) => { let keyword = content.substring(0, 5); db.find(`select * from sample where kw = ${keyword}`, (err, res) => { get(`/sampleget?count=${res.length}`, data => { console.log(data); }); }); }); 复制代码
以上代码包括了三个异步操作:
- 文件读取: fs.readFile
- 数据库查询:db.find
- http请求:get
我们每增加一个异步请求,就会多添加一层回调函数的嵌套,这样下去,可读性会越来越低,也不易于以后的代码维护。过多的回调也就让我们陷入“回调地狱”。接下来会大概介绍一下规避回调地狱的方法。
1、拆分function
回调嵌套所带来的一个重要的问题就是代码不易阅读与维护。因为普遍来说,过多的嵌套(缩进)会极大的影响代码的可读性。基于这一点,可以进行一个最简单的优化----将各个步骤拆解为单个function
//HTTP请求 function getData(count) { get(`/sampleget?count=${count}`, data => { console.log(data); }); } //查询数据库 function queryDB(kw) { db.find(`select * from sample where kw = ${kw}`, (err, res) => { getData(res.length); }); } //读取文件 function readFile(filepath) { fs.readFile(filepath, 'utf-8', (err, content) => { let keyword = content.substring(0, 5); queryDB(keyword); }); } //执行函数 readFile('./sample.txt'); 复制代码
通过改写,再加上注释,可以很清晰的知道这段代码要做的事情。该方法非常简单,具有一定的效果,但是缺少通用性。
2、事件发布/监听模式
addEventListener应该不陌生吧,如果你在浏览器中写过监听事件。 借鉴这个思路,我们可以监听某一件事情,当事情发生的时候,进行相应的回调操作;另一方面,当某些操作完成后,通过发布事件触发回调。这样就可以将原本捆绑在一起的代码解耦。
const events = require('events'); const eventEmitter = new events.EventEmitter(); eventEmitter.on('db', (err, kw) => { db.find(`select * from sample where kw = ${kw}`, (err, res) => { eventEmitter('get', res.length); }); }); eventEmitter.on('get', (err, count) => { get(`/sampleget?count=${count}`, data => { console.log(data); }); }); fs.readFile('./sample.txt', 'utf-8', (err, content) => { let keyword = content.substring(0, 5); eventEmitter. emit('db', keyword); }); 复制代码
events 模块是node原生模块,用node实现这种模式只需要一个事件发布/监听的库。
3、Promise
Promise是es6的规范 首先,我们需要将异步方法改写成Promise,对于符合node规范的回调函数(第一个参数必须是Error), 可以使用bluebird的promisify方法。该方法接受一个标准的异步方法并返回一个Promise对象
const bluebird = require('bluebird'); const fs = require("fs"); const readFile = bluebird.promisify(fs.readFile); 复制代码
这样fs.readFile就变成一个Promise对象。 但是可能有些异步无法进行转换,这样我们就需要使用原生Promise改造。 以fs.readFile为例,借助原生Promise来改造该方法:
const readFile = function (filepath) { let resolve, reject; let promise = new Promise((_resolve, _reject) => { resolve = _resolve; reject = _reject; }); let deferred = { resolve, reject, promise }; fs.readFile(filepath, 'utf-8', function (err, ...args) { if (err) { deferred.reject(err); } else { deferred.resolve(...args); } }); return deferred.promise; } 复制代码
我们在方法中创建一个Promise对象,并在异步回调中根据不同的情况使用reject与resolve来改变Promise对象的状态。该方法返回这个Promise对象。其他的一些异步方法可以参照这种方式进行改造。 假设通过改造,readFile、queryDB与getData方法均会返回一个Promise对象。代码就会变成这样:
readFile('./sample.txt').then(content => { let keyword = content.substring(0, 5); return queryDB(keyword); }).then(res => { return getData(res.length); }).then(data => { console.log(data); }).catch(err => { console.warn(err); }); 复制代码
通过then的链式改造。使代码的整洁度在一定的程度上有了一个较大的提高。
4、generator
generator是es6中的一个新的语法。在function关键字后添加*即可将函数变为generator。
const gen = function* () { yield 1; yield 2; return 3; } 复制代码
执行generator将会返回一个遍历器对象,用于遍历generator内部的状态。
let g = gen(); g.next(); // { value: 1, done: false } g.next(); // { value: 2, done: false } g.next(); // { value: 3, done: true } g.next(); // { value: undefined, done: true } 复制代码
可以看到,generator函数有一个最大的特点,可以在内部执行的过程中交出程序的控制权,yield相当于起到了一个暂停的作用;而当一定的情况下,外部又将控制权再移交回来。 我们用generator来封装代码,在异步任务处使用yield关键词,此时generator会将程序执行权交给其他代码,而在异步任务完成后,调用next方法来恢复yield下方代码的执行。以readFile为例,大致流程如下:
// 我们的主任务——显示关键字 // 使用yield暂时中断下方代码执行 // yield后面为promise对象 const showKeyword = function* (filepath) { console.log('开始读取'); let keyword = yield readFile(filepath); console.log(`关键字为${filepath}`); } // generator的流程控制 let gen = showKeyword(); let res = gen.next(); res.value.then(res => gen.next(res)); 复制代码
ps:这部分暂时没理清楚,待续
5、async/await
可以看到,上面的方法虽然都在一定程度上解决了异步编程中回调带来的问题。然而
- function拆分的方式其实仅仅只是拆分代码块,时常会不利于后续的维护;
- 事件发布/监听方式模糊了异步方法之间的流程关系;
- Promise虽然使得多个嵌套的异步调用能通过链式API进行操作,但是过多的then也增加了代码的冗余,也对阅读代码中各个阶段的异步任务产生了一定的干扰;
- 通过generator虽然能提供较好的语法结构,但是毕竟generator与yield的语境用在这里多少还有点不太贴切。
因此,这里在介绍一个方法,它就是es7中的async/await。 简单介绍一下async/await。基本上,任何一个函数都可以成为async函数,以下都是合法的书写形式
async function foo () {}; const foo = async function () {}; const foo = async () => {}; 复制代码
未完待续——
以上所述就是小编给大家介绍的《如何避免回调地狱》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。