如何避免回调地狱

栏目: JavaScript · 发布时间: 6年前

内容简介:目前有几个比较好的解决方法以上代码包括了三个异步操作:我们每增加一个异步请求,就会多添加一层回调函数的嵌套,这样下去,可读性会越来越低,也不易于以后的代码维护。过多的回调也就让我们陷入“回调地狱”。接下来会大概介绍一下规避回调地狱的方法。

目前有几个比较好的解决方法

  1. 拆解function
  2. 事件发布/监听模式
  3. Promise
  4. generator
  5. 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 () => {};
复制代码

未完待续——


以上所述就是小编给大家介绍的《如何避免回调地狱》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Design Handbook

Web Design Handbook

Baeck, Philippe de 编 / 2009-12 / $ 22.54

This non-technical book brings together contemporary web design's latest and most original creative examples in the areas of services, media, blogs, contacts, links and jobs. It also traces the latest......一起来看看 《Web Design Handbook》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具