如何避免回調地獄

問題來源

平時咱們平常寫代碼中,可能會遇到這種某個回調有異步請求,請求的回調又有異步請求、循環

目前有幾個比較好的解決方法前端

  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);
        });
    });
});
複製代碼

以上代碼包括了三個異步操做:node

  • 文件讀取: fs.readFile
  • 數據庫查詢:db.find
  • http請求:get

咱們每增長一個異步請求,就會多添加一層回調函數的嵌套,這樣下去,可讀性會愈來愈低,也不易於之後的代碼維護。過多的回調也就讓咱們陷入「回調地獄」。接下來會大概介紹一下規避回調地獄的方法。es6

一、拆分function

回調嵌套所帶來的一個重要的問題就是代碼不易閱讀與維護。由於廣泛來講,過多的嵌套(縮進)會極大的影響代碼的可讀性。基於這一點,能夠進行一個最簡單的優化----將各個步驟拆解爲單個functionweb

//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');
複製代碼

經過改寫,再加上註釋,能夠很清晰的知道這段代碼要作的事情。該方法很是簡單,具備必定的效果,可是缺乏通用性。數據庫

二、事件發佈/監聽模式

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實現這種模式只須要一個事件發佈/監聽的庫。小程序

三、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來改造該方法: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的鏈式改造。使代碼的整潔度在必定的程度上有了一個較大的提升。

四、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:這部分暫時沒理清楚,待續

五、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前端開發工程師,喜歡處處尋找好吃的,平時愛好是跳舞,打籃球,聽音樂,有時會出席一些大型的舞蹈商演活動,目前是Acum.Revolution現狀革命成員之一。而且表明做品:萌雞駕到、美旅出行小程序、電競桌子小程序。擅長網站建設、公衆號開發、微信小程序開發、小遊戲、公衆號開發,專一於前端領域框架、交互設計、圖像繪製、數據分析等研究,訪問 www.talkmoney.cn 瞭解更多。
相關文章
相關標籤/搜索