异步编程在JavaScript中非常重要。过多的异步编程也带了回调嵌套的问题,本文会提供一些解决“回调地狱”的方法。

setTimeout(function () {
console.log('延时触发');
}, 2000); fs.readFile('./sample.txt', 'utf-8', function (err, res) {
console.log(res);
});

上面就是典型的回调函数,不论是在浏览器中,还是在node中,JavaScript本身是单线程,因此,为了应对一些单线程带来的问题,异步编程成为了JavaScript中非常重要的一部分。

不论是浏览器中最为常见的ajax、事件监听,还是node中文件读取、网络编程、数据库等操作,都离不开异步编程。在异步编程中,许多操作都会放在回调函数(callback)中。同步与异步的混杂、过多的回调嵌套都会使得代码变得难以理解与维护,这也是常受人诟病的地方。

先看下面这段代码

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);
});
});
});

首先我们读取的一个文件中的关键字keyword,然后根据该keyword进行数据库查询,最后依据查询结果请求数据。

其中包含了三个异步操作:

  • 文件读取:fs.readFile
  • 数据库查询:db.find
  • http请求:get

可以看到,我们没增加一个异步请求,就会多添加一层回调函数的嵌套,这段代码中三个异步函数的嵌套已经开始使一段本可以语言明确的代码编程不易阅读与维护了。

抽象出来这种代码会变成下面这样:

asyncFunc1(opt, (...args1) => {
asyncFunc2(opt, (...args2) => {
asyncFunc3(opt, (...args3) => {
asyncFunc4(opt, (...args4) => {
// some operation
});
});
});
});

左侧明显出现了一个三角形的缩进区域,过多的回调也就让我们陷入“回调地狱”。接下来会介绍一些方法来规避回调地狱。

一、拆解function

回调嵌套所带来的一个重要问题就是代码不易阅读与维护。因为普遍来说,过多的缩进(嵌套)会极大的影响代码的可读性。基于这一点,可以进行一个最简单的优化——将各步拆解为单个的function

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');

可以看到,通过上面的改写方式,代码清晰了许多。该方法非常简单,具有一定的效果,但是缺少通用性。

二、事件发布/监听模式

如果在浏览器中写过事件监听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);
});

使用这种模式的实现需要一个事件发布/监听的库。上面代码中使用node原生的events模块,当然你可以使用任何你喜欢的库。

三、Promise

Promise是一种异步解决方案,最早由社区提出并实现,后来写进了es6规范。

目前一些主流的浏览器已经原生实现了Promise的API,可以在Can I use里查看浏览器的支持情况。当然,如果想要做浏览器的兼容,可以考虑使用一些Promise的实现库,例如bluebirdQ等。下面以bluebird为例:

首先,我们需要将异步方法改写为Promise,对于符合node规范的回调函数(第一个参数必须是Error),可以使用bluebird的promisify方法。该方法接收一个标准的异步方法并返回一个Promise对象。

const bluebird = require('bluebird');
const fs = require("fs");
const readFile = bluebird.promisify(fs.readFile);

这样,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对象,并在异步回调中根据不同的情况使用rejectresolve来改变Promise对象的状态。该方法返回这个Promise对象。其他的一些异步方法也可以参照这种方式进行改造。

假设通过改造,readFilequeryDBgetData方法均会返回一个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连接的链式操作。代码的整洁度上有了一个较大的提高。

四、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));

在主任务部分,原本readFile异步的部分变成了类似同步的写法,代码变得非常清晰。而在下半部分,则是对于什么时候需要移交回控制权给generator的流程控制。

然而,我们需要手动控制generator的流程,如果能够自动执行generator——在需要的时候自动移交控制权,那么会更加具有实用性。

为此,我们可以使用 co 这个库。它可以是省去我们对于generator流程控制的代码

const co = reuqire('co');
// 我们的主任务——显示关键字
// 使用yield暂时中断下方代码执行
// yield后面为promise对象
const showKeyword = function* (filepath) {
console.log('开始读取');
let keyword = yield readFile(filepath);
console.log(`关键字为${filepath}`);
} // 使用co
co(showKeyword);

其中,yeild关键字后面需要是functio, promise, generator, arrayobject。可以改写文章一开始的例子:

const co = reuqire('co');

const task = function* (filepath) {
let keyword = yield readFile(filepath);
let count = yield queryDB(keyword);
let data = yield getData(res.length);
console.log(data);
}); co(task, './sample.txt');

五、async/await

可以看到,上面的方法虽然都在一定程度上解决了异步编程中回调带来的问题。然而

  • function拆分的方式其实仅仅只是拆分代码块,时常会不利于后续维护;
  • 事件发布/监听方式模糊了异步方法之间的流程关系;
  • Promise虽然使得多个嵌套的异步调用能够通过链式的API进行操作,但是过多的then也增加了代码的冗余,也对阅读代码中各阶段的异步任务产生了一定干扰;
  • 通过generator虽然能提供较好的语法结构,但是毕竟generatoryield的语境用在这里多少还有些不太贴切。

因此,这里再介绍一个方法,它就是es7中的async/await。

简单介绍一下async/await。基本上,任何一个函数都可以成为async函数,以下都是合法的书写形式:

async function foo () {};
const foo = async function () {};
const foo = async () => {};

async函数中可以使用await语句。await后一般是一个Promise对象。

async function foo () {
console.log('开始');
let res = await post(data);
console.log(`post已完成,结果为:${res}`);
};

当上面的函数执行到await时,可以简单理解为,函数挂起,等待await后的Promise返回,再执行下面的语句。

值得注意的是,这段异步操作的代码,看起来就像是“同步操作”。这就大大方便了异步代码的编写与阅读。下面改写我们的例子。

const printData = async function (filepath) {
let keyword = await readFile(filepath);
let count = await queryDB(keyword);
let data = await getData(res.length);
console.log(data);
}); printData('./sample.txt');

可以看到,代码简洁清晰,异步代码也具有了“同步”代码的结构。

注意,其中readFilequeryDBgetData方法都需要返回一个Promise对象。这可以通过在第三部分Promise里提供的方式进行改写。

后记

异步编程作为JavaScript中的一部分,具有非常重要的位置,它帮助我们避免同步代码带来的线程阻塞的同时,也为编码与阅读带来了一定的困难。过多的回调嵌套很容易会让我们陷入“回调地狱”中,使代码变成一团乱麻。为了解决“回调地狱”,我们可以使用文中所述的这五种常用方法:

  • function拆解
  • 事件发布/订阅模式
  • Promise
  • Generator
  • async / await

理解各类方法的原理与实现方式,了解其中利弊,可以帮助我们更好得进行异步编程。

作者:AlienZHOU
链接:https://www.jianshu.com/p/bc7b8d542dcd
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

JavaScript异步编程__“回调地狱”的一些解决方案的更多相关文章

  1. javascript异步代码的回调地狱以及JQuery.deferred提供的promise解决方式

    我们先来看一下编写AJAX编码常常遇到的几个问题: 1.因为AJAX是异步的,全部依赖AJAX返回结果的代码必需写在AJAX回调函数中.这就不可避免地形成了嵌套.ajax等异步操作越多,嵌套层次就会越 ...

  2. 探索Javascript异步编程

    异步编程带来的问题在客户端Javascript中并不明显,但随着服务器端Javascript越来越广的被使用,大量的异步IO操作使得该问题变得明显.许多不同的方法都可以解决这个问题,本文讨论了一些方法 ...

  3. Javascript异步编程之二回调函数

    上一节讲异步原理的时候基本上把回掉函数也捎带讲了一些,这节主要举几个例子来具体化一下.在开始之前,首先要明白一件事,在javascript里函数可以作为参数进行传递,这里涉及到高阶函数的概念,大家可以 ...

  4. JavaScript异步编程原理

    众所周知,JavaScript 的执行环境是单线程的,所谓的单线程就是一次只能完成一个任务,其任务的调度方式就是排队,这就和火车站洗手间门口的等待一样,前面的那个人没有搞定,你就只能站在后面排队等着. ...

  5. 深入解析Javascript异步编程

    这里深入探讨下Javascript的异步编程技术.(P.S. 本文较长,请准备好瓜子可乐 :D) 一. Javascript异步编程简介 至少在语言级别上,Javascript是单线程的,因此异步编程 ...

  6. Func-Chain.js 另一种思路的javascript异步编程解决方案

    本文转载自:https://www.ctolib.com/panruiplay-func-chain.html Func-Chain.js 另一种思路的javascript异步编程,用于解决老式的回调 ...

  7. JavaScript异步编程的主要解决方案—对不起,我和你不在同一个频率上

    众所周知(这也忒夸张了吧?),Javascript通过事件驱动机制,在单线程模型下,以异步的形式来实现非阻塞的IO操作.这种模式使得JavaScript在处理事务时非常高效,但这带来了很多问题,比如异 ...

  8. javascript异步编程的前世今生,从onclick到await/async

    javascript与异步编程 为了避免资源管理等复杂性的问题, javascript被设计为单线程的语言,即使有了html5 worker,也不能直接访问dom. javascript 设计之初是为 ...

  9. JavaScript异步编程(2)- 先驱者:jsDeferred

    JavaScript当前有众多实现异步编程的方式,最为耀眼的就是ECMAScript 6规范中的Promise对象,它来自于CommonJS小组的努力:Promise/A+规范. 研究javascri ...

随机推荐

  1. html 音频

    <!DOCTYPE html><meta charset="utf-8"><video src="movie.webm" cont ...

  2. 没的选择时,存在就是合理的::与李旭科书法字QQ聊天记录

    2015,8,11,晚上,与李旭科书法字作者,在Q上聊了下 有些资料 涉及到字库设计.字库产业,对大家也有益处 按惯例 没细整理,直接发blog了 ps,9.11 靠,今天是911,早上查资料,在 f ...

  3. Entity Framework 并发处理(转)

    什么是并发? 并发分悲观并发和乐观并发. 悲观并发:比如有两个用户A,B,同时登录系统修改一个文档,如果A先进入修改,则系统会把该文档锁住,B就没办法打开了,只有等A修改完,完全退出的时候B才能进入修 ...

  4. Python3:sqlalchemy对sybase数据库操作,非sql语句

    Python3:sqlalchemy对sybase数据库操作,非sql语句 # python3 # author lizm # datetime 2018-02-01 10:00:00 # -*- c ...

  5. 20145307陈俊达《网络对抗》Exp2 后门原理与实践

    20145307陈俊达<网络对抗>Exp2 后门原理与实践 基础问题回答 例举你能想到的一个后门进入到你系统中的可能方式? 非正规网站下载的软件 脚本 或者游戏中附加的第三方插件 例举你知 ...

  6. 2017-2018-1 JaWorld 第八周作业

    2017-2018-1 JaWorld 第八周作业 团队分工 成员 分工 陈是奇 统计成员工具选择 马平川 类图 王译潇 编码规范 李昱兴 用例图 林臻 状态图 张师瑜 推进工作进展.写博客 UML ...

  7. 2016湘潭邀请赛—Gambling

    http://202.197.224.59/OnlineJudge2/index.php/Problem/read/id/1244 题意:有a个红球,b个绿球,c个黄球,先拿完a个红球一等奖,先拿完b ...

  8. JAVA基础知识详解

    1. JVM是什么 JVM是Java Virtual Mechine的缩写.它是一种基于计算设备的规范,是一台虚拟机,即虚构的计算机. JVM屏蔽了具体操作系统平台的信息(显然,就像是我们在电脑上开了 ...

  9. gif&png&jpg&webp

    几种图片格式的区别和联系 1.http://www.tuicool.com/articles/AbUvI3A

  10. 流量监控iftop安装-CentOS7

    继之前撘的服务器后路由器一直崩溃,今天找到了原因.之前被下的木马并没有被删掉,而是一直在传输数据.占用了所有宽带. 官网(http://www.ex-parrot.com/pdw/iftop/down ...