JavaScript:Promise的基礎(chǔ)套路

關(guān)于Promise的基本內(nèi)容,已經(jīng)寫過一篇文章。基本的會用,偶爾也用過,不過知識點(diǎn)比較多,并且代碼在外面套了好幾層,感覺也比較復(fù)雜,一直理不順頭緒。最近看了下面鏈接的幾篇文章,感覺很不錯,對于Promise基本概念的理解比以前要清晰一點(diǎn)了。
Promise的知識點(diǎn)很多,這里是一次從多到少的收斂過程,寫了幾點(diǎn)平時可能會用到的最基礎(chǔ)的用法。

大白話講解Promise(一)
這篇文章寫得還是比較簡單直接的,對于理解概念很有幫助,推薦好好看看。

廖雪峰的Promise
這篇文章對于概念的分解還是比較詳細(xì)的,很不錯。里面的例子直接copy到chrome的控制臺會報錯,不過簡單修改一下就可以了。本文的例子基本上都是從這里簡單修改來的。

適用Promise的三種場景?

  • 場景1: 級聯(lián)調(diào)用,就是幾個調(diào)用依次發(fā)生的場景。這個就是有名的回調(diào)地獄。通用的套路是,新建一個Promise,啟動流程,其他的Promise,放在級聯(lián)的then函數(shù)中。

  • 場景2:幾個異步回調(diào)都成功,然后再進(jìn)行下一步操作,比如圖片比較大,分成幾個小圖下載,然后拼接,就是一個典型的場景。這里用Promise.all這個函數(shù)就很方便。

  • 場景3: 幾個異步回調(diào),只要有一個成功,就可以進(jìn)行下一步。比如像主站和另外兩個備用站點(diǎn)同時請求一張圖片,只要有一個有相應(yīng),其他幾個就可以放棄。這里用Promise.race這個函數(shù)就很方便。

Promise和回調(diào)地獄是什么關(guān)系?

  • 單個的回調(diào)函數(shù)不會形成回調(diào)地獄。串行的回調(diào),也就是上面提到的場景1,層層嵌套,導(dǎo)致代碼越套越深,結(jié)構(gòu)復(fù)雜,才形成了回調(diào)地獄。

  • Promise并不是替代回調(diào)函數(shù),而是對回調(diào)函數(shù)的一層封裝。比如,有名的setTimeOut函數(shù),Promise并不能替代它,只是對它進(jìn)行了一層包裝。

  • 當(dāng)然,也不是簡單的包裝,最本質(zhì)的變化,就是將對回調(diào)函數(shù)的調(diào)用,修改成了消息發(fā)送。將對結(jié)果的處理剝離出去,交給后續(xù)的對象處理。
    resolve(data); reject(error);這兩個理解為消息發(fā)送函數(shù)更確切一點(diǎn)。一方面,將所處的Promise的狀態(tài)由pending改為resolved或者reject。另一方面,生成一個新的Promise,并返回,同時將數(shù)據(jù)作為參數(shù)傳遞出去。

  • Promise包裝的函數(shù),同步和異步都是可以的,沒有本質(zhì)區(qū)別,按照一套思路去理解就可以了。同步的代碼基本不需要包裝,本來就簡單,比如用Promise.resolve(初始值)發(fā)起一個流程。大多數(shù)情況,Promise包裝的都是異步對象。本質(zhì)是為了把層層嵌套異步回調(diào)代碼,回調(diào)地獄 callback hell,轉(zhuǎn)變?yōu)榇械逆準(zhǔn)秸{(diào)用。

  • Promise對象新建的時候,狀態(tài)是Pending,可以理解為“正在進(jìn)行中,需要等待”。
    resolve(data);一下,狀態(tài)變成了Resolved,鏈?zhǔn)秸{(diào)用的控制權(quán)就轉(zhuǎn)移到了下一級的then(data => {callback(data)})函數(shù)中。這里就是傳統(tǒng)的回調(diào)函數(shù)執(zhí)行的地方。
    reject(error);一下,狀態(tài)變成了Rejected,鏈?zhǔn)秸{(diào)用的控制權(quán)就轉(zhuǎn)移到了catch(error => { })函數(shù)中,一般建議放在最后面。這里就是集中處理錯誤的地方。
    所以,不要糾結(jié)同步還是異步,將重點(diǎn)放在Promise的對象的狀態(tài)以及鏈?zhǔn)秸{(diào)用的控制權(quán)的轉(zhuǎn)移上面。

Promise的編程范式:面向?qū)ο?or 函數(shù)式?

  • 創(chuàng)建Promise對象,需要用到new關(guān)鍵字,并且名字也一般叫做對象。不過,Promise并不是面向?qū)ο蟮木幊蹋嗟倪€是函數(shù)式。范疇或者集合,用類來模擬。如果能夠提供一個靜態(tài)函數(shù)Promise.of來替代new關(guān)鍵字,函數(shù)式的味道就更濃厚一點(diǎn)。

  • 鏈?zhǔn)秸{(diào)用,比較方便,要做到這一點(diǎn),每個函數(shù),比如then,catch等,都返回Promise對象,叫范疇或者集合更確切一點(diǎn)。不過,每一個Promise都是不同的,這個符合函數(shù)式編程的習(xí)慣:生成新的對象,而不是改變對象本身。之間的聯(lián)系主要是數(shù)據(jù)的傳遞,自身內(nèi)部狀態(tài)的變化。

Promise對異步調(diào)用的常用封裝套路:

function promiseFunction(resolve, reject) {
    let timeOut = Math.random() * 2;
    let flag = (timeOut < 1);
    let callback = () => {
        if (flag) {
            console.log('success...');
            return resolve('data: 200 OK'); // 加個return是個好習(xí)慣
        } else {
            console.log('fail...');
            return reject('error: timeout in ' + timeOut + ' seconds.'); // 加個return是個好習(xí)慣
        }
    };
    // start process
    console.log('start async function ...');
    setTimeout(callback, (timeOut * 1000));
}

function asyncFunction () {
    let promise = new Promise(promiseFunction);
    return promise;
}
  • 對callback的改造:
    一般的callback,應(yīng)該定義對結(jié)果的處理過程以及出錯的處理過程。Promise剝離了這些具體的處理過程,改成了發(fā)消息。成功就發(fā)送resolve(data);失敗就發(fā)送reject(error);
    這里要注意的一點(diǎn)是,resolve(data);reject(error);,這兩個消息函數(shù)至少要用一個,當(dāng)然多用是沒關(guān)系的,否則流程就啟動不了。
    resolve(data);reject(error);之后,流程就交給后面的then或者catch來處理了,這之后的代碼都不會執(zhí)行。所以resolve(data);reject(error);前面加個return,可以更加明確這種意圖,是個好習(xí)慣

  • 對流程函數(shù)的封裝:
    一般的異步過程都分為兩步:在主線程發(fā)起異步過程,然后主線程就去做其他事情了;具體的工作一般在工作者線程中執(zhí)行。工作完成后,調(diào)用callback,通知主線程,讓主線程拿著結(jié)果做想做的事。
    Promise把發(fā)起異步過程,(這里用setTimeout函數(shù)模擬),這個步驟封裝在一個函數(shù)中,(就是Promise構(gòu)造函數(shù)的executor參數(shù)),這個函數(shù)格式固定,參數(shù)是resolve, reject,這里用一個名字promiseFunction把他列出來。

  • 函數(shù)式編程范式的封裝:
    函數(shù)式編程一般會簡化為范疇或者集合的操作,數(shù)據(jù)和函數(shù)都包裹在一個集合容器中。
    這里用Promise類的對象來模擬,這也是導(dǎo)致誤認(rèn)為面向?qū)ο缶幊痰脑颉R怨潭ㄌ茁返暮瘮?shù)(resolve, reject)作為參數(shù),通過Promise構(gòu)造函數(shù),用new關(guān)鍵字,得到了一個promise對象,完成封裝。
    所以,Promise對象在構(gòu)建過程中,異步流程就已經(jīng)發(fā)起了,Promise對象的狀態(tài)就是pending===這個也是參考文章大白話講解Promise(一)中提到的注意點(diǎn)
    如果不resolve或者reject一下,(throw error跟reject是同一個意思),Promise對象就一直pending,這個鏈?zhǔn)秸{(diào)用就一直停著,動不了。

  • 接口函數(shù)的封裝:
    這層封裝是從軟件工程的角度,方便使用者使用的角度來做的。
    函數(shù)式編程用來完成跟界面和業(yè)務(wù)無關(guān)的具體功能是比較好的,操作的也是集合。但是一般來說,業(yè)務(wù)層用面向?qū)ο蟮哪J竭M(jìn)行設(shè)計的,調(diào)用函數(shù)式編程的集合不是很方便。所以,封裝成功能型的函數(shù),(也就是上面的asyncFunction函數(shù)),用起來就比較順手了。
    當(dāng)然,把生成的Promise對象return出去,是為了方便鏈?zhǔn)秸{(diào)用。

在實(shí)際使用中,可以寫得簡潔一些,上面的代碼可以精簡如下:

function asyncFunction () {
    return new Promise(function(resolve, reject) {
        let timeOut = Math.random() * 2;
        let flag = (timeOut < 1);
        // start process
        console.log('start async function ...');
        setTimeout(() => {
            if (flag) {
                console.log('success...');
               return  resolve('data: 200 OK'); // 加個return是個好習(xí)慣
            } else {
                console.log('fail...');
                return reject('error: timeout in ' + timeOut + ' seconds.'); // 加個return是個好習(xí)慣
            }
        }, (timeOut * 1000));
    });
}

Promise簡單使用的套路:

  • 所謂簡單使用,就考慮最簡單的異步調(diào)用,(a)發(fā)起流程,等結(jié)果;(b)成功,處理結(jié)果;(c)失敗,報錯

  • Promise.prototype.then(callback)就是用來處理成功結(jié)果的回調(diào)函數(shù),具體的處理過程在這里定義。
    then函數(shù)的第二個參數(shù)可以用來處理出錯結(jié)果,不過一般都不用。在這里處理錯誤是一種很差的方法。
    then函數(shù)會返回一個Promise對象。這個前面已經(jīng)提過,這個Promise對象是then函數(shù)內(nèi)部新建的,和流程發(fā)起的那個Promise對象是不一樣的。

  • then函數(shù)一般建議寫同步過程,這里是執(zhí)行以往回調(diào)函數(shù)功能的地方。在流程最后,把接收到的datareturn回去是個好習(xí)慣,萬一后面還有其他的then要用,數(shù)據(jù)data就可以順著節(jié)點(diǎn)傳一下,不至于中斷。
    return data; 和 return Promise.resolve(data);是等價的,內(nèi)部估計會裝換一下。所以本質(zhì)上還是return了一個Promise對象
    如果是異步過程,建議新建一個Promise對象包裝一下,再return,這樣就形成了串行依賴關(guān)系。
    如果什么都不return,那么內(nèi)部會新建一個沒有值的Promise對象,相當(dāng)于return Promise.resolve(undefined);;所以這種情況,鏈?zhǔn)秸{(diào)用還可以繼續(xù),但是參數(shù)傳遞會中斷。

then函數(shù)中一般不建議放異步過程,這樣做會增加理解的難度。下面這篇文章中就有這樣的例子:
Promise.prototype.then()

  • Promise.prototype.catch()本質(zhì)上是.then(null, rejection)的別名,這里是集中處理錯誤的地方,一般放在鏈?zhǔn)秸{(diào)用所有then的后面。這樣可以捕獲流程中的所有錯誤,包括主流程的以及后續(xù)then中出現(xiàn)的錯誤。

  • 再簡單的過程,(一般是異步過程,同步過程也一樣,比如直通),也用函數(shù)包一下,(對外的接口統(tǒng)一為函數(shù),把Promise對象隱藏起來),至少給一個then,最后跟一個catch。將原來一個整體的異步調(diào)用(流程發(fā)起,成功,失敗)轉(zhuǎn)化成了3級的鏈?zhǔn)秸{(diào)用,代碼結(jié)構(gòu)清晰很多。
    比如上面通過Promise封裝好的異步函數(shù),典型的使用套路如下:

asyncFunction().then((data) => {
    console.log(data);
    return data; // 把數(shù)據(jù)往下傳,是個好習(xí)慣
    }).catch((error) => {
    console.log(error);
});

場景1: 級聯(lián)調(diào)用使用的套路:

  • 這就是著名的回調(diào)地獄,callback hell,采用Promise包裝之后,可以改為簡潔的鏈?zhǔn)秸{(diào)用。其實(shí)就是用級聯(lián)的then來體現(xiàn)這種級聯(lián)的調(diào)用關(guān)系。
    job1.then(job2).then(job3).catch(handleError);
    其中,job1、job2和job3都是封裝了Promise對象的函數(shù)

  • 注意,這里的job1、job2和job3都要求是一個參數(shù)的函數(shù)。因為,不論是resolve,還是reject,傳值的參數(shù)個數(shù)都只有一個。這個可以聯(lián)想到函數(shù)式編程中的柯里化,每次只傳一個參數(shù),簡單直接。
    如果想傳個參數(shù)怎么辦呢?將所有參數(shù)包裝成一個對象就可以了,resolve和reject都是可以傳遞對象的,只是個數(shù)規(guī)定為一個而已。不過運(yùn)算的時候需要解析參數(shù),再傳值的時候需要重新組裝參數(shù),相對就麻煩一點(diǎn)了。

  • 靜態(tài)函數(shù)Promise.resolve(data)可以快捷地返回一個Promise對象,一般可以用在鏈?zhǔn)秸{(diào)用的開頭,提供初始值。

  • 一般來說,在新建的Promise對象中發(fā)起異步流程,resolve(data)消息發(fā)出之后,then(data => { callback(data) })接收到數(shù)據(jù)data,將原先的callback在這里執(zhí)行就好了。現(xiàn)在這里不放回調(diào)代碼,而是return一個新的Promise對象,形成一個依賴鏈。

下面這個例子,就是先用Promise包裝了一個異步過程,(乘10的函數(shù));以及一個同步過程,(加100的函數(shù));用隨機(jī)數(shù)的方式,模擬過程失敗的情況。然后通過then函數(shù)級聯(lián)的方式定義依賴過程。最后用catch捕捉過程中遇到的錯誤。

Step1:用Promise封裝過程

// input*10的計算結(jié)果; setTimeout模擬異步過程;
function multiply10(input) {
    return new Promise(function (resolve, reject) {
        let temp = Math.random() * 1.2;
        let flag = (temp < 1);
        console.log('calculating ' + input + ' x ' + 10 + '...');
        setTimeout(() => {
            if (flag) {
                return resolve(input * 10);
            } else {
                return reject('multiply error:' + temp);
            }
        }, 500);
    });
}

// input+100的計算結(jié)果;同步過程
function add100(input) {
    return new Promise(function (resolve, reject) {
        let temp = Math.random() * 1.2;
        let flag = (temp < 1);
        console.log('calculating ' + input + ' + ' + 100 + '...');
        if (flag) {
            return resolve(input + 100);
        } else {
            return reject('add error:' + temp);
        }
    });
}

Step2:用then函數(shù)級聯(lián)的方式定義依賴過程:

// 結(jié)果是3300,或者報錯
Promise.resolve(32).then(multiply10).then(multiply10).then(add100).then(data => {
    console.log('Got value: ' + data);
    return data;
}).catch(error => {
    console.log(error);
});

// 結(jié)果是1160,或者報錯
Promise.resolve(6).then(add100).then(multiply10).then(add100).then(data => {
    console.log('Got value: ' + data);
    return data;
}).catch(error => {
    console.log(error);
});

// ... ... 還能寫出很多的組合情況
  • 一般情況then(data => { callback(data) })函數(shù)的主要工作是接收數(shù)據(jù),然后執(zhí)行原來的回調(diào)函數(shù)。
    這里一般放同步代碼;如果是異步代碼,就像上面那樣,可以新建一個Promise對象并返回,形成一個調(diào)用鏈。

  • 如果既有同步的回調(diào)代碼需要執(zhí)行,又有異步的過程需要包裝鏈接,怎么辦呢?比如上面的例子,增加顯示中間過程的功能。
    可以考慮用兩個級聯(lián)的then函數(shù)分別來做這兩件事。

一個then用來執(zhí)行同步的回調(diào)函數(shù)。這里要注意將要傳遞的data return出去,不然,整個鏈?zhǔn)秸{(diào)用參數(shù)傳遞會中斷。

.then(data => { 
    callbacek(data);
    return data;   // 這里要把接收到的data傳出去,不然整個調(diào)用鏈的參數(shù)傳遞會斷掉。
})

一個then用來包裝異步過程的,并把這個新建的Promise return出去,形成異步過程依賴鏈。

.then(data => { 
    return new Promise(function(resolve, reject) {
        let flag = ((Math.random() * 2) < 1);  // demo flag
        let newData = data + 1; // demo data 
        setTimeout(() => { // demo async function
            if (flag) {
                resolve(newData);
            } else {
                reject(new Error('error message'));
            }
        }, 10);
    });
})
  • 上面的新需求可以按照下面的套路簡單實(shí)現(xiàn):
// 結(jié)果是9880,或者報錯
Promise.resolve(888).then(add100).then(data => {
    console.log('add100之后的結(jié)果為:' + data);
    return data;
}).then(multiply10).then(data => {
    console.log('multiply10之后的結(jié)果為:' + data);
    return data;
}).then(data => {
    console.log('Got value: ' + data);
    return data; // 這里是最后了,不return data對流程沒影響。不過誰知道以后會不會加新的節(jié)點(diǎn),return一下還是好的。
}).catch(error => {
    console.log(error);
});

場景2: 幾個異步回調(diào)都成功,然后再進(jìn)行下一步操作
場景3: 幾個異步回調(diào),只要有一個成功,就可以進(jìn)行下一步

  • 這兩種的實(shí)現(xiàn)方式很類似,可以按照一種套路模式

  • 只考慮異步過程,不考慮同步過程

  • 這里用到了兩個靜態(tài)函數(shù),分別是Promise.all(),(場景2);Promise.race(),(場景3);

  • 這兩個函數(shù)的參數(shù)都是一個數(shù)組,數(shù)組的成員是Promise對象。

  • 后面跟一個then和catch,就像是普通的使用場景。

  • Promise.all()成功時,傳遞過來的是一個結(jié)果數(shù)組;失敗時,傳遞過來的是出錯對應(yīng)的值。

  • 這里沒有鏈?zhǔn)秸{(diào)用,一長串的數(shù)據(jù)傳遞,所以這里的函數(shù)的參數(shù)個數(shù)沒有限制。不過,統(tǒng)一為一個是比較好的習(xí)慣。就算沒有參數(shù),給個空對象也可以,萬一以后要傳呢

  • 大白話講解Promise(一)
    Promise.all(),「誰跑的慢,以誰為準(zhǔn)執(zhí)行回調(diào)」;
    Promise.race(),「誰跑的快,以誰為準(zhǔn)執(zhí)行回調(diào)」;
    這個表述還是形象而準(zhǔn)確的。

function asyncFunction1(data = null) {
    return new Promise(function(resolve, reject) {
        let temp = Math.random() * 2;
        let flag = (temp < 1);
        // start process
        console.log('start asyncfunction1 ...');
        setTimeout(() => {
            if (flag) {
                if (data) {
                    return resolve(data);
                } else {
                    return resolve('success: asyncfunction1===');
                }
            } else {
                return reject(`fail:asyncfunction1; temp:${temp}`);
            }
        }, 500);
    });
}

function asyncFunction2(data = null) {
    return new Promise(function(resolve, reject) {
        let temp = Math.random() * 2;
        let flag = (temp < 1);
        // start process
        console.log('start asyncfunction2 ...');
        setTimeout(() => {
            if (flag) {
                if (!data) {
                    return resolve(data);
                } else {
                    return resolve('success: asyncfunction2');
                }
            } else {
                return reject(`fail:asyncfunction2; temp:${temp}`);
            }
        }, 500);
    });
}

// 這里傳過來的是成功結(jié)果的數(shù)組
Promise.all([asyncFunction1(), asyncFunction2()]).then(array => {
    console.log(JSON.stringify(array));
    return array; // 這里傳遞的是數(shù)組,比較特殊
}).catch(error => {
    console.log(error);
});

// 結(jié)果是success: asyncfunction1;跑得比較快
Promise.race([asyncFunction1(), asyncFunction2()]).then(data => {
    console.log(data);
    return data;
}).catch(error => {
    console.log(error);
});

done、finally、success、fail等其他內(nèi)容呢?

  • 這些一些框架提供的便利方法,當(dāng)然,如果有需要,也可以自己實(shí)現(xiàn)。

  • 上面這些是基本的使用套路,簡單直接。一個基礎(chǔ)應(yīng)用加三個典型場景,可以應(yīng)付平時大多數(shù)的異步過程。

  • 當(dāng)然,Promise還有很多高級而靈活的用法。下面推薦幾篇文章,里面的內(nèi)容很豐富。

Promise 對象(阮一峰)

JavaScript Promise迷你書(中文版)

Promise MDN

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。