美文网首页
如何避免回调地狱

如何避免回调地狱

作者: 广州芦苇科技web前端 | 来源:发表于2018-12-14 23:45 被阅读0次

问题来源

平时我们日常写代码中,可能会遇到这种某个回调有异步请求,请求的回调又有异步请求、循环

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

  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前端工程师

相关文章

  • 如何避免回调地狱

    问题来源 平时我们日常写代码中,可能会遇到这种某个回调有异步请求,请求的回调又有异步请求、循环 目前有几个比较好的...

  • 封装 MySQL(一)做个help先

    Node.js 环境里面访问 MySQL 的默认方式,采用了古老的回调方式,这样很容易产生回调地狱。那么如何避免呢...

  • 第二十一天web前端面试题

    1,手写promise封装axios 2,如何解决回调地狱 首先回调地狱是什么?函数作为参数层层嵌套 什么是回调函...

  • 异步问题

    什么是回调地狱(函数作为参数层层嵌套)回调函数(一个函数作为参数需要依赖另一个函数执行调用)如何解决回调地狱 pr...

  • Promise

    1.为什么需要Promise 回调地狱回调函数中嵌套回调Promise解决了回调地狱 2. Promise 的基本...

  • ES6中await从零起步(三)

    上面一章,讲了generator中是如何实现异步的,也留下了一个问题,就是如何避免回调地狱。 稍稍观察一下这段代码...

  • 回调地狱及如何解决回调地狱

    回调地狱 根据我们在回调函数和异步任务[https://www.jianshu.com/p/ffc633a9e47...

  • eventProxy的使用

    异步回调地狱的避免方式除了用async模块之外,还可以使用eventProxy,代码如下:

  • Promise

    什么是回调地狱 多层回调函数的相互嵌套,就形成了回调地狱 回调地狱的缺点: 代码耦合性太强,牵一发而动全身,难以维...

  • promise的使用

    1、promise是用来干什么的? 用来处理回调,避免回调地狱 用来更好地处理异步行为 坏的例子:AJAX多级嵌套...

网友评论

      本文标题:如何避免回调地狱

      本文链接:https://www.haomeiwen.com/subject/vlxohqtx.html