JavaScript非同步編程__“回調地獄”的一些解決方案

來源:互聯網
上載者:User

標籤:增加   方法   www.   常用   基本   作用   產生   事件監聽   ref   

非同步編程在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的實現庫,例如bluebird、 Q等。下面以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串連的鏈式操作。代碼的整潔度上有了一個較大的提高。

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

在主任務部分,原本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}`);}// 使用coco(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
連結:http://www.jianshu.com/p/bc7b8d542dcd
來源:簡書

JavaScript非同步編程__“回調地獄”的一些解決方案

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.