異步流程之Promise解析

很久沒有更新文章了,最近恰好遇到考試,並且一直在作數據庫課設。javascript

原本這篇文章是上個星期想要分享給工做室的師弟師妹們的,結果由於考試就落下了。html

其實我並非很想寫Promise,畢竟如今更好的方式是結合await/asyncPromise編寫異步代碼。可是,其實以爲Promise這個東西對於入門ES6,改善「回調地獄」有很大的幫助,那也算是回過頭來複習一下吧。java

本文不少地方參考了阮一峯的《ES6標準入門》這一本書,由於學ES6,這本書是最好的,沒有之一。固然,整理的文章也有我本身的思路在,還有加上了本身的一些理解,適合入門ES6的小夥伴學習。node

若是已經對Promise有必定的瞭解,但並無實際的用過,那麼能夠看一下在實例中使用如何更加優雅的使用Promise一節。mysql

另外,本文中有三個例子涉及「事件循環和任務隊列」(均已在代碼頭部標出),若是暫時不能理解,能夠先學完Promise以後去了解最後一節的知識,而後再回來看,這樣小夥伴你應該就豁然開朗了。jquery

引言

回調函數

所謂回調,就是「回來調用」,這裏拿知乎上「常溪玲」一個很形象的例子: 「 你到一個商店買東西,恰好你要的東西沒有貨,因而你在店員那裏留下了你的電話,過了幾天店裏有貨了,店員就打了你的電話,而後你接到電話後就到店裏去取了貨。在這個例子裏,你的電話號碼就叫回調函數,你把電話留給店員就叫登記回調函數,店裏後來有貨了叫作觸發了回調關聯的事件,店員給你打電話叫作調用回調函數,你到店裏去取貨叫作響應回調事件。」git

至於回調函數的官方定義是什麼,這裏就不展開了,畢竟和咱們本篇文章關係不大。有興趣的小夥伴能夠去知乎搜一下。es6

不友好的「回調地獄」

寫過node代碼的小夥伴必定會遇到這樣的一個調用方式,好比下面mysql數據庫的查詢語句:github

connection.query(sql1, (err, result) => { //ES6箭頭函數
    //第一次查詢
    if(err) {
        console.err(err);
    } else {
        connection.query(sql2, (err, result) => {
            //第二次查詢
            if(err) {
                console.err(err);
            } else {
                ...
            }
        };
    }
})

上面的代碼大概的意思是,使用mysql數據庫進行查詢數據,當執行完sql1語句以後,再執行sql2語句。web

可見,上面執行sql1語句和sql2語句有一個前後的過程。爲了實現先去執行sql1語句再執行sql2語句,咱們只能這樣簡單粗暴的去嵌套調用

若是隻有兩三步操做還好,那麼假如是十步操做或者更多,那代碼的結構是否是更加的複雜了並且還難以閱讀。

因此,Promise就爲了解決這個問題,而出現了。

promise用法

這一部分的內容絕大部分摘抄自《ES6標準入門》一書,若是你已經讀過相關Promise的使用方法,那麼你大能夠快速瀏覽或直接跳過。

同時,你更須要留意一下catch部分和涉及「事件循環」的三個例子

promise是什麼?

promise的定義

所謂Promise,簡單說就是一個容器,裏面保存着某個將來纔會結束的事件的結果。從語法上說,Promise 是一個對象,從它能夠獲取異步操做的消息。Promise 提供統一的 API,各類異步操做均可以用一樣的方法進行處理,讓開發者不用再關注於時序和底層的結果。Promise的狀態具備不受外界影響不可逆兩個特色,與譯後的「承諾」這個詞有着類似的特色。

Promise的三個狀態

首先,Promise對象表明一個異步操做,有三種狀態:pending(進行中)、fulfilled(已成功)、rejected(已失敗)。

只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒有辦法改變這個狀態。

狀態不可逆

其次,狀態是不可逆的。也就是說,一旦狀態改變,就不會再變成其餘的了,日後不管什麼時候,均可以獲得這個結果。

對於Promise的狀態的改變,只有兩種狀況:一是pending變成fulfilled,一是pending變成rejected。(注:下文用resolved指代fulfilled

只要這兩種狀況中的一種發生了,那麼狀態就被固定下來了,不會再發生改變。

同時,若是改變已經發生了,此時再對Promise對象指定回調函數,那麼會當即執行添加的回調函數,返回Promise的狀態。這與事件徹底不一樣。事件的狀態是瞬時性的,一旦錯過,它的狀態將不會被保存。此時再去監聽,確定是得不到結果的。

Promise怎麼用?

promise的基本用法

ES6規定,Promise對象是一個構造函數,用來生成Promise實例。

實例對象

這裏,咱們先來new一個全新的Promise實例。

const promise = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 異步操做成功*/) {
        resolve(value);
    } else {
        reject(error);                    
    }
});

能夠看到,Promise構造函數接受一個匿名函數做爲參數,在函數中,又分別接受resolvereject兩個參數。這兩個參數表明着內置的兩個函數。

resovle的做用是,將Promise對象的狀態從「未完成(pending)」變爲「成功(resolved)」,一般在異步操做成功時調用,並將異步操做的結果,作爲它的參數傳遞出去。

reject的做用是,將Promise對象的狀態從「未完成(pending)」變成"失敗(rejected)",一般在異步操做失敗時調用,並將異步操做的結果,做爲參數傳遞出去。

接收狀態的回調

Promise實例生成之後,可使用then方法指定resolved狀態和rejected狀態。

//接上「實例對象」的代碼
promise.then(function(value) {
    //success
},function(error) {
    //failure
});

可見,then方法能夠接受兩個回調函數做爲參數。第一個回調函數是Promise對象的狀態變爲resolved時調用,第二個回調函數是promise對象的狀態變爲rejected時調用。其中,第二個函數是可選的。並不必定要提供。另外,這兩個函數都接受Promise對象傳出的值做爲參數。

下面給出了一個簡單的例子:

function timeout(ms) {
    return new Promise((resolve, reject) {
        setTimeout(resolve, ms, 'done');                 
    });
}
timeout(100).then(function(value) {
    console.log(value); //done
});

上面的例子,是在100ms以後,把新建的Promise對象由pending狀態變爲resolved狀態,接着觸發then方法綁定的回調函數。

另外,Promise在新建的時候就會當即執行,所以咱們也能夠直接改變Promise的狀態。

//涉及「事件循環」例子1
let promise = new Promise(function(resolve, reject) {
    console.log('Promise');
    resolve();
});

promise.then(function() {
    console.log('resolved.');
});

console.log('Hi!');
// Promise
// Hi!
// resolved

上面的代碼中,新建了一個Promise實例後當即執行,因此首先輸出的是"Promise",僅接着resolve以後,觸發then的回調函數,它將在當前腳本全部同步任務執行完了以後纔會執行,因此接下來輸出的是"Hi!",最後纔是"resolved"。(注:這裏涉及到JS的任務執行過程和事件循環,若是還不是很瞭解這個流程能夠所有看完後再回過來理解一下這段代碼。)

關於Promise的基本用法,就先講解到這裏。

接下來咱們來看一下Promise封裝的原生方法。

Promise實例上的thencatch

Promise.prototype.then

Promise的原型上有then方法,前面已經說起和體驗過,它的做用是爲Promise實例添加狀態改變時的回調函數。 then的方法的第一個參數是resolved狀態的回調函數,第二個參數(可選)是rejected狀態的回調函數。

then方法返回的是一個Promise實例,所以能夠採用鏈式寫法,也就是說在then後面能夠再調用另外一個then方法。

const promise = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 異步操做成功*/) {
        resolve(obj);
    } else {
        reject(error);                         
    }
});
promise.then(function(obj) {
    return obj.a;
}).then(function(a) {
    //...
});

上面的代碼使用then方法,依次指定了兩個回調函數。第一個回調函數完成之後,會將返回結果做爲參數,傳入第二個回調函數。

也就是說,在Promise中傳參有兩種方式:

  • 一是實例Promise的時候把參數經過resovle()傳遞出去。

  • 二是在then方法中經過return返回給後面的then

採用鏈式的then,能夠指定一組按照次序調用的回調函數。這時,前一個回調函數,有可能返回的仍是一個Promise對象(即有異步操做),這時後一個回調函數,就會等待該Promise對象的狀態發生變化,纔會被調用。

const promise1 = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 異步操做成功*/) {
        resolve("promise1");
    } else {
        reject(error);                   
    }
});
const promise2 = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 異步操做成功*/) {
        resolve("promise2");
    } else {
        reject(error);                   
    }
});
promise1.then(function() {
    return promise2;
}).then(function funcA(result) {
    console.log(result); //"promise2"
}, function funcB(err){
    console.log("rejected: ", err);
});

上面代碼中,第一個then方法指定的回調函數,返回的是另外一個Promise對象。這時,第二個then方法指定的回調函數,就會等待這個新的Promise對象狀態發生變化。若是變爲resolved,就調用funcA,若是狀態變爲rejected,就調用funcB

Promise.prototype.catch

Promise.prototype.catch方法是.then(null, rejection)的別名,用於指定發生錯誤時的回調函數。

const promise = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 異步操做成功*/) {
        resolve(value);
    } else {
        reject(error);                    
    }
});
promise.then(function(value) {
    //success
},function(error) {
    //failure
});

因而,這段代碼等價爲:

const promise = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 異步操做成功*/) {
        resolve(value);
    } else {
        reject(error);                         
    }
})
promise.then(function() {
    //success
}).catch(function(err) {
    //failure
})

可見,此時「位置1」中的then裏面的兩個參數被剝離開來,若是異步操做拋出錯誤,就會調用catch方法指定的回調函數,處理這個錯誤。

值得一提的是,如今咱們在給rejected狀態綁定回調的時候,更傾向於catch的寫法,而不使用then方法的第二個參數。這種寫法,不只讓Promise看起來更加簡潔,更加符合語義邏輯,接近try/catch的寫法。更重要的是,Promise對象的錯誤具備向後傳遞的性質(書中說「冒泡」我以爲不是很合適,可能會誤解),直到錯誤被捕獲爲止。

const promise1 = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 異步操做成功*/) {
        resolve("promise1");
    } else {
        reject(error);                   
    }
});
const promise2 = new Promise(function(resolve, reject) {
    // ... some code
    if(/* 異步操做成功*/) {
        resolve("promise2");
    } else {
        reject(error);                   
    }
});
promise1.then(function() {
    return promise2;
}).then(function funcA(result) {
    console.log(result); //"promise2"
}).catch(function(err) {
    console.log(err); //處理錯誤
})

上面的代碼中一共有三個Promise,第一個由promise1產生,另外兩個由不一樣的兩個then產生。不管是其中的任何一個拋出錯誤,都會被最後一個catch捕獲。

若是仍是對Promise錯誤向後傳遞的性質不清楚,那麼能夠按照下面的代碼作一下實驗,即可以更加清晰的認知這個特性。

const promise1 = new Promise(function(resolve, reject) {
    //1. 在這裏throw("promise1錯誤"),catch捕獲成功
    // ... some code
    if(true) {
        resolve("promise1");
    } else {
        reject(error);                   
    }
});
const promise2 = new Promise(function(resolve, reject) {
    // ... some code
    //2. 在這裏throw("promise2錯誤"),catch捕獲成功
    if(true) {
        resolve("promise2");
    } else {
        reject(error);                   
    }
});
promise1.then(function() {
    return promise2;
}).then(function funcA(result) {
    console.log(result); //"promise2"
    //3. 在這裏throw("promise3錯誤"),catch捕獲成功
}).catch(function(err) {
    console.log(err); //處理錯誤
})

以上,分別將一、二、3的位置進行解註釋,就可以證實咱們以上的結論。

關於catch方法,還有三點須要說起的地方。

  1. Promise中的錯誤傳遞是向後傳遞,並不是是嵌套傳遞,也就是說,嵌套的Promise,外層的catch語句是捕獲不到錯誤的。

    const promise1 = new Promise(function(resolve, reject) {
        // ... some code
        if(true) {
            resolve("promise1");
        } else {
            reject(error);                   
        }
    });
    const promise2 = new Promise(function(resolve, reject) {
        // ... some code
        if(true) {
            resolve("promise2");
        } else {
            reject(error);                   
        }
    });
    promise1.then(function() {
        promise2.then(function() {
            throw("promise2出錯");
        })
    }).catch(function(err) {
        console.log(err);
    });
    //> Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: undefined}
    //Uncaught (in promise) promise2出錯

    因此,代碼出現了未捕獲的錯誤,這就是爲何我強調說是「向後傳遞錯誤而不是冒泡傳遞錯誤」。

  2. Promise沒有使用catch而拋出未處理的錯誤。

    const someAsyncThing = function() {
        return new Promise(function(resolve, reject) {
            // 下面一行會報錯,由於x沒有聲明
            resolve(x + 2);
        });
    };
    
    someAsyncThing().then(function() {
        console.log('everything is great');
    });
    
    setTimeout(() => { console.log(123) }, 2000);
    // Uncaught (in promise) ReferenceError: x is not defined
    // 123

    上面代碼中,someAsyncThing函數產生的Promise 對象,內部有語法錯誤。瀏覽器運行到這一行,會打印出錯誤提示ReferenceError: x is not defined,可是不會退出進程、終止腳本執行,2秒以後仍是會輸出123。這就是說,Promise內部的錯誤不會影響到Promise外部的代碼,通俗的說法就是「Promise會吃掉錯誤」。

    解決的方法就是在then後面接一個catch方法。

  3. 涉及到Promise中的異步任務拋出錯誤的時候。

    //涉及「事件循環」例子2
    const promise = new Promise(function (resolve, reject){
        resolve('ok');
        setTimeout(function () { 
            throw new Error('test')      
        }, 0);
    });
    promise.then(function (value) { 
        console.log(value);
    }).catch(function(err) {
        console.log(err);
    });
    // ok
    // Uncaught Error: test

    能夠看到,這裏的錯誤並不會catch捕獲,結果就成了一個未捕獲的錯誤。

    緣由有二:

    其一,因爲在setTimeout以前已經resolve過了,因爲這個時候的Promise狀態就變成了resolved,因此它走的應該是then而不是catch,就算後面再拋出錯誤,因爲其狀態不可逆的緣由,依舊不會拋出錯誤。也就是下面這種狀況:

    const promise = new Promise(function (resolve, reject) {
         resolve('ok');
         throw new Error('test'); //依然不會拋出錯誤
     });
    //...省略

    其二,setTimeout是一個異步任務,它是在下一個「事件循環」才執行的。當到了下一個事件循環,此時Promise早已經執行完畢了,此時這個錯誤並非在Promise內部拋出了,而是在全局做用域中,因而成了未捕獲的錯誤。(注:這裏涉及到JS的任務執行過程和事件循環,若是還不是很瞭解這個流程能夠所有看完後再回過來理解一下這段代碼。)

    解決的方法就是直接在setTimeout的回調函數中去try/catch

更多的方法

Promise.resolve

這個方法能夠把現有的對象轉換成一個Promise對象,以下:

const jsPromise = Promise.resolve($.ajax('/whatever.json'));

上面代碼把jQuery中生成的deferred對象轉換成了一個新的Promise對象。

Promise的參數大體分下面四種:

  1. 若是參數是Promise實例,那麼Promise.resolve將不作任何修改、原封不動地返回這個實例。

  2. 參數是一個thenable對象。

    thenable對象指的是具備then方法的對象,好比下面這個對象。

    let thenable = {
        then: function(resolve, reject) {
        resolve(42);
        }
    };

    Promise.resolve方法會將這個對象轉爲Promise對象,而後就當即執行thenable對象的then方法,以下:

    let thenable = {
        then: function(resolve, reject) {
        resolve(42);
        }
    };
    
    let p1 = Promise.resolve(thenable);
    p1.then(function(value) {
        console.log(value);  // 42
    });
  3. 參數不是具備then方法的對象,或根本就不是對象。

    若是參數是一個原始值,或者是一個不具備then方法的對象,則Promise.resolve方法返回一個新的Promise對象,狀態爲resolved

  4. 不帶有任何參數。

    Promise.resolve方法容許調用時不帶參數,直接返回一個resolved狀態的Promise對象。

    //涉及「事件循環」例子3
    setTimeout(function () {
        console.log('three');
    }, 0);
    
    Promise.resolve().then(function () {
        console.log('two');
    });
    
    console.log('one');
    
    // one
    // two
    // three

    上面這個例子,因爲Promise算是一個微任務,當第一次事件循環執行完了以後(console.log('one')),會取出任務隊列中的全部微任務執行完(Promise.resovle().then),再進行下一次事件循環,也就是以後再執行setTimeout。因此輸出的順序就是onetwothree。(注:這裏涉及到JS的任務執行過程和事件循環,若是還不是很瞭解這個流程能夠所有看完後再回過來理解一下這段代碼。)

Promise.reject

Promise.reject(reason)方法也會返回一個新的Promise實例,該實例的狀態爲rejected,並當即執行其回調函數。

注意,Promise.reject()方法的參數,會原封不動地做爲reject的理由,變成後續方法的參數。這一點與Promise.resolve方法不一致。

const thenable = {
    then(resolve, reject) {
        reject('出錯了');
    }
};

Promise.reject(thenable)
    .catch(e => {
        console.log(e === thenable)
    });
// true

上面代碼中,Promise.reject方法的參數是一個thenable對象,執行之後,後面catch方法的參數不是reject拋出的「出錯了」這個字符串,而是thenable對象。

其餘

下面的方法只作簡單的介紹,若是須要更詳細的瞭解它,請到傳送門處查詢相關資料。

Promise.all

Promise.all方法用於將多個Promise實例,包裝成一個新的Promise實例。

const p = Promise.all([p1, p2, p3]);

上面代碼中,Promise.all方法接受一個數組做爲參數,p1p2p3都是Promise實例,若是不是,就會先調用上面講到的Promise.resolve方法,將參數轉爲Promise實例,再進一步處理。

p的狀態由p1p2p3決定,分紅兩種狀況。

(1)只有p1p2p3的狀態都變成fulfilledp的狀態纔會變成fulfilled,此時p1p2p3的返回值組成一個數組,傳遞給p的回調函數。

(2)只要p1p2p3之中有一個被rejectedp的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。

Promise.race

const p = Promise.all([p1, p2, p3]);

上面代碼中,Promise.race方法接受一個數組做爲參數,p1p2p3都是Promise實例,若是不是,就會先調用上面講到的Promise.resolve方法,將參數轉爲Promise實例,再進一步處理。

Promise.all不一樣,只要其中有一個實例率先改變狀態,p的狀態就跟着改變。那麼率先改變的Promise實例的返回值,就傳遞給p的回調函數。

done

Promise對象的回調鏈,無論以then方法或catch方法結尾,要是最後一個方法拋出錯誤,都有可能沒法捕捉到。所以,咱們能夠提供一個done方法,老是處於回調鏈的尾端,保證拋出任何可能出現的錯誤。

它的實現代碼至關簡單。

Promise.prototype.done = function (onFulfilled, onRejected) {
      this.then(onFulfilled, onRejected)
        .catch(function (reason) {
        // 拋出一個全局錯誤
        setTimeout( function() { throw reason }, 0);
        });
};

從上面代碼可見,done方法的使用,能夠像then方法那樣用,提供fulfilledrejected狀態的回調函數,也能夠不提供任何參數。但無論怎樣,done都會捕捉到任何可能出現的錯誤,並向全局拋出。

finally

finally方法用於指定無論Promise對象最後狀態如何,都會執行的操做。它與done方法的最大區別,它接受一個普通的回調函數做爲參數,該函數無論怎樣都必須執行。

下面是一個例子,服務器使用Promise處理請求,而後使用finally方法關掉服務器。

server.listen(0)
    .then(function () {
        // run test
    });
    .finally(server.stop);

它的實現也很是的簡單。

Promise.prototype.finally = function (callback) {
    let P = this.constructor;
    return this.then( function(value) {
        P.resolve(callback()).then(function() {
            return value;
        });
    }, function(reason) {
        reason => P.resolve(callback()).then(function() {
            throw reason;
        });
    });
};

JQuery的Deferred對象

最初,在低版本的JQuery中,對於回調函數,它的功能是很是弱的。無限「嵌套」回調,編程起來十分不友好。爲了改變這個問題,JQuery團隊就設計了deferred對象。

它把回調的嵌套調用改寫成了鏈式調用,具體的寫法也十分的簡單。這裏也不詳細講,想了解的小夥伴也能夠直接到這個連接去看。傳送門

外部修改狀態

可是,因爲deferred對象它的狀態能夠在外部被修改到,這樣會致使混亂的出現,因而就有了deferred.promise

它是在原來的deferred對象上返回另一個deferred對象,後者只開放與改變執行狀態無關的方法,屏蔽與改變執行狀態有關的方法。從而來避免上述提到的外部修改狀態的狀況。

若是有任何疑問,能夠回到傳送門一看便知。

值得一提的是,JQuery中的Promise與咱們文章講的Promise並無關係,只是名字同樣罷了。

雖然二者遵循的規範不相同,可是都致力於一件事情,那就是:基於回調函數更好的編程方式。

promise編程結構

返回新Promise

既然咱們學了Promise,那麼就應該在平常開發中去使用它。

然而,對於初學者來講,在使用Promise的時候,可能會出現嵌套問題。

好比說下面的代碼:

var p1 = new Promise(function() {
    if(...) {
        reject(...);
    } else {
        resolve(...);
    }
});
var p2 = new Promise(function() {
    if(...) {
        reject(...);
    } else {
        resolve(...);
    }
});
var p3 = new Promise(function() {
    if(...) {
        reject(...);
    } else {
        resolve(...);
    }
});

p1.then(function(p1_data) {
    p2.then(function(p2_data) {
        // do something with p1_data
        p3.then(fuction(p3_data) {
        // do something with p2_data
            // p4...
        });
    });
});

假如說如今須要p1p2p3按照順序執行,那麼剛入門的小夥伴可能會這樣寫。

其實也沒有錯,這裏是用了Promise,可是用得並不完全,依然存在「回調」地獄,沒有深刻到Promise的核心部分。

那麼咱們應該怎麼樣更好的去運用它呢?

回顧一下前面Promise部分,你應該能夠獲得答案。

下面,看咱們修正後的代碼。

//同上,省略定義。

p1.then(function(p1_data) {
    return p2; //位置1
}).then(function(p2_data){ //位置2
    return p3;
}).then(function(p3_data){
    return p4;
}).then(function(p4_data){
    //final result
}).catch(function(error){
    //同一處理錯誤信息
});

能夠看到,每次執行完了then方法以後,咱們都return了一個新的Promise。那麼當新的Promiseresolve以後,那麼顯而易見的,它會執行跟在它後面的then之中。

也就是說,在p1then方法執行完了以後,如今咱們要去執行p2,那麼這個時候咱們在「位置1」給它return了一個新的Promise,因此此時的代碼能夠等價爲:

p2.then(function(p2_data){ //位置2
    return p3;
}).then(function(p3_data){
    return p4;
}).then(function(p4_data){
    //final result
}).catch(function(error){
    //同一處理錯誤信息
});

可見,p2resolve以後,就能夠被「位置2」的then接收到了。

因而,基於這個結構,咱們就能夠在開發中去封裝出一個Promise供咱們來使用。

在實例中使用

恰好最近在作一個mysql的數據庫課設,這裏就把我如何封裝promise給貼出來。

下面的例子,可能有些接口剛接觸node的小夥伴會看不懂,那麼,我會盡可能的作到無死角註釋,你們也儘可能關注一下封裝的過程(注:重點關注標「*」的地方)。

首先是mysql.js封裝文件。

var mysql = require("mysql");//引入mysql庫
//建立一個鏈接池,同一個鏈接池能夠同時存在多個鏈接,鏈接完成須要釋放
var pool = mysql.createPool({ 
  ...//省略鏈接的配置
});

/**
 * 把mySQL查詢功能封裝成一個promise
 * @param String sql 
 * @returns Promise
**/
  
var QUERY = (sql) => {
    //注意這裏new了一個新的promise(*)
    var connect = new Promise((resolve, reject) => { 
          //建立鏈接
        pool.getConnection((err, connection) => {
            //下面是狀態執行(*)
            if (err) {
                reject(err);//若是建立鏈接失敗的話直接reject(*)
            } else {
                //不然能夠進行查詢了
                connection.query(sql, (err, results) => {
                    //執行完查詢釋放鏈接
                    connection.release();
                    //在查詢的時候若是出錯直接reject
                    if (err) {
                        reject(err);//(*)
                    } else {
                        //不然成功,把查詢的結果resolve出去
                        //而後給後面then去使用
                        resolve(results);//(*)
                    }
                });
            }
        });
    });
    //最後把promise給return出去(*)
    return connect; 
};

module.exports = QUERY; //把封裝好的庫導出

接下來,去使用咱們封裝好的查詢Promise

假如咱們如今想要使用查詢功能獲取某個數據表的全部數據:

var QUERY = require("mysql"); //把咱們寫的庫給導入
var sql = `SELECT * FROM student`;//sql語句,看不懂直接忽略
//執行查詢操做
QUERY(sql).then((results) => { //(*)
    //這裏就可使用查詢到的results了
}).catch((err) => {
    //使用catch能夠捕獲到整條鏈拋出的錯誤。(*)
    console.log(err);
})

以上,就是一個實例了。因此之後,若是你想要封裝一個Promise來使用,你能夠這樣來寫。

如何更優雅的使用Promise?

那麼,如今問題又來了,若是咱們如今須要進行不少異步操做(好比Ajax通訊),那麼若是按照上面的寫法,會致使then鏈條過長。因而,須要咱們不停的去return一個新的Promise對象供後面使用。以下:

function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
var request = {
        comment: function getComment() {
            return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
        },
        people: function getPeople() {
            return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
        }
    };
function main() {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    // [] 用來保存初始化的值 至關於聲明results = []
    var pushValue = recordValue.bind(null, []);
    return request.comment() //位置1
        .then(pushValue)
          .then(request.people)
          .then(pushValue); 
}
// 運行示例
main().then(function (value) {
    console.log(value);
}).catch(function(error){
    console.error(error);
});

能夠看到,在「位置1」處的代碼,return request.comment().then(pushValue).then(request.people).then(pushValue); 使用了三個thennew了兩個新的Promise

所以,若是咱們將處理內容統一放到數組裏,再配合for循環進行處理的話,那麼處理內容的增長將不會再帶來什麼問題。首先咱們就使用for循環來完成和前面一樣的處理。

function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
var request = {
        comment: function getComment() {
            return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
        },
        people: function getPeople() {
            return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
        }
    };

前面這一部分是不須要改變的。

function main() {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    // [] 用來保存初始化值
    var pushValue = recordValue.bind(null, []);
    // 返回promise對象的函數的數組
    var tasks = [request.comment, request.people];
    var promise = Promise.resolve();
    // 開始的地方
    for (var i = 0; i < tasks.length; i++) {
        var task = tasks[i];
        promise = promise.then(task).then(pushValue);
    }
    return promise;
}
// 運行示例
main().then(function (value) {
    console.log(value);
}).catch(function(error){
    console.error(error);
});

使用for循環的時候,每次調用then都會返回一個新建立的Promise對象 所以相似promise = promise.then(task).then(pushValue);的代碼就是經過不斷對promise進行處理,不斷的覆蓋 promise變量的值,以達到對Promise對象的累積處理效果。 可是這種方法須要promise這個臨時變量,從代碼質量上來講顯得不那麼簡潔。 若是將這種循環寫法改用Array.prototype.reduce的話,那麼代碼就會變得聰明多了。

因而咱們再對main函數進行修改:

function main() {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    var pushValue = recordValue.bind(null, []);
    var tasks = [request.comment, request.people];
    return tasks.reduce(function (promise, task) {
        return promise.then(task).then(pushValue);
    }, Promise.resolve());
}

(注:Array.prototype.reduce第一個參數執行數組每一個值的回調函數,第二個參數是初始值。回調函數中,第一個參數是上一次調用回調返回的值或提供的初始值,第二個是數組中正在處理的元素。)

最後,重寫完了整個函數就是:

function sequenceTasks(tasks) {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    var pushValue = recordValue.bind(null, []);
    return tasks.reduce(function (promise, task) {
        return promise.then(task).then(pushValue);
    }, Promise.resolve());
}
function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
var request = {
        comment: function getComment() {
            return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
        },
        people: function getPeople() {
            return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
        }
    };
function main() {
    return sequenceTasks([request.comment, request.people]);
}
// 運行示例
main().then(function (value) {
    console.log(value);
}).catch(function(error){
    console.error(error);
});

須要注意的是,在sequenceTasks中傳入的應該是返回Promise對象的函數的數組,而不是一個Promise對象,由於一旦返回一個對象的時候,異步任務其實已是開始執行了。

綜上,在寫順序隊列的時候,核心思想就是不斷的去return新的Promise並進行狀態判斷 。而至於怎麼寫,要根據實際狀況進行編程。

是回調很差仍是嵌套很差?

本質上來講,回調自己沒有什麼很差的,可是由於回調的存在,使得咱們無限的嵌套函數構成了「回調地獄」,這對開發者來講無疑是特別不友好的。而雖然Promise只是回調的語法糖,可是卻提供給咱們更好的書寫方式,解決了回調地獄嵌套的難題。

更多

最後,這裏算是一個拓展和學習方向,學習起來有必定的難度。

爲何JavaScript使用異步的方式來處理任務?

因爲JavaScript是一種單線程的語言,所謂的單線程就是按照咱們書寫的代碼同樣一行一行的執行下來,因而每次只能作一件事。

若是咱們不是用異步的方式而用同步的方式去處理任務,假如如今咱們有一個網絡請求,請求後面是與其無關的一些操做代碼。那麼當請求發送出去的時候,因爲如今執行代碼是循序漸進的,因而咱們就必須等待網絡請求的應答以後,咱們才能繼續往下執行咱們的代碼。而這個等待,不只花費了咱們不少時間。同時,也阻塞了咱們後面的代碼。形成了沒必要要的資源浪費。

因而,當使用異步的方式來處理任務的時候,每次發送請求,JavaScript中的執行棧會把異步操做交給瀏覽器的webCore內核來處理,而後繼續往下執行代碼。當主線程的執行棧代碼執行完畢以後,就會去檢查任務隊列中有沒有任務須要執行的。

若是有,則取出來到主線程的執行棧中執行,執行完畢後,更新dom,而後再進行一次一樣的循環。

而任務隊列中任務的添加,則是靠瀏覽器內核webCore。每次異步操做完成以後,webCore內核就會把相應的回調函數添加到任務隊列中。

值得注意的是,任務隊列中任務按照任務性質劃分爲宏任務和微任務。而因爲任務類型的不一樣,可能存在多個類型的任務隊列。可是事件循環只能有一個。

因此如今咱們把宏任務和微任務考慮進去,第一次執行完腳本的代碼(算是一次宏任務),那麼就會到任務隊列的微任務隊列中取出其全部任務放到主線程的執行棧中來執行,執行完畢後,更新dom。下一次事件循環,再從任務隊列中取出一個宏任務,而後執行微任務隊列中的全部微任務。再循環...

注:第一次執行代碼的時候,就已經開始了第一次的事件循環,此時的script同步代碼是一個宏任務。

整個過程,也就是下面的這一個圖:

常見的異步任務有:網絡請求、IO操做、計時器和事件綁定等。

以上,若是你可以看懂我在講什麼,那麼說明你真正理解了JS中的異步,若是不懂,那麼你須要去了解一下「事件循環、任務隊列、宏任務與微任務」,下面是兩篇不錯的博客,值得學習。

事件循環:http://www.ruanyifeng.com/blo...

對JS異步任務執行的一個總結:http://www.yangzicong.com/art...

相關文章
相關標籤/搜索