關於promise的一些總結

作爲前端開發人員,對於promise應該都是不陌生,基本上都有過,new promise, 然後.then, 原型裏也提供了一些方案,race, all等等。。。對於解決同步的問題可謂是十分方便。然而,前段時間面試:
“知道promise嗎”,
“知道”,
“那你說說什麼是promise”,
“promise就是解決異步操作的一種方案,避免的函數回調”,
“嗯,那還有呢,具體怎麼實現的呢”
“emmmm…”
“ok,那你知道async麼,這個和promise區別是什麼呢?”
…真的是一時語塞,當時緊張的隨便說了兩句,現在都忘了自己說了些什麼。原來平時真的是專注於怎麼方便怎麼來,本着會用就行了的心態果然很容易把自己的路走窄呀。

所以,還是乖乖關注其方法本身吧。

#####爲什麼要用promise呢?
答案很明顯,promise是爲了解決異步的可操控性。node.js是單線程異步執行環境的。如果兩異步接口存在依賴關係,比如A接口需要依賴B接口,那麼我們希望A接口在B接口之前執行。
我們也許用如下的方式去控制方法的執行順序
B接口裏需要執行ajax請求,在B方法中定義一個變量val來監聽ajax請求是否返回正確的值,再通過val來判斷是否需要調用A方法。(以下代碼爲簡寫,理順思路就好)

   function A(){
      console.log('----輸出A----')
   }
   function B(val){
    if (val < 5) {
      A();
    }
   }
   B(4);  

或者是通過setTimeOut來控制方法的執順序。

   function A(){
      console.log('----輸出A----')
   }
   function B(val){
      console.log('---val----'+val);
      setTimeout(()=>{
         A();
      },5000)
   }
   B(4);

通過函數回調的方式如果是非常簡單的操作,這樣的寫法也未嘗不可。
但是如果是很複雜的邏輯,也許過了十天自己都回憶不起來,這是個啥。。
另外,這樣寫的壞處,就真的是造成了代碼冗餘,而且不能複用,並且讓你覺得寫代碼是件繁瑣的事情。
函數回調,很可能遇見信任問題,what?
1.調用回調過早(在它開始追蹤之前)
2.調用回調過晚(或者不調)
3.調用回調太少或者太多次
4.吞掉了可能發生的錯誤/異常
5.沒有傳遞正確的參數(還是有可能的,例如接口相應超時,超出了等待時間)

總之使用回調的方式,確實會出現很多讓人難受又很奇葩的錯誤。

#####首先會用。ok,會用的程度是什麼樣子的呢?

  new Promise(function(resolve, reject){
    console.log('---執行步驟一,輸出,成功---')
    resolve();
  }).then(function(resolve,reject){
    console.log('---執行步驟二,輸出,失敗----')
    reject();
  }).catch(function(resolve, reject){
    console.log('----最後一步失敗---')
  })

輸出
—執行步驟一,輸出,成功—
—執行步驟二,輸出,失敗----
----最後一步失敗—

甚至,我們在vue/react中,使用axios,fetch,dispatch等請求數據也會去支持promise API,非常直觀的表達了函數的調用順序。比如說:

  dispatch({
    type: 'xxx/xx',
    params:{
      content: 'test1'
    }
  }).then(res => {
    if (res) {
      ...doSomeThing...
    }
  })

以上,我想大部分都用到這個程度吧。

#####來剖析剖析promise

如果你來實現一個promise函數呢?應該怎麼寫?我們沿着這個思路,寫一個promise的實現方案,順便來了解了解它的思想。
我們以生活中的例子:
比如有一天我們需要外出辦一件很緊急的事情,但是朋友又要來家裏聚餐。最理想的狀態就是,當我辦完緊急的事情回來後,可以和朋友們聚在一起,吃着美食。那麼promise在這時候充當的角色就是我的小助手mm,她需要爲我做好一頓美食。接下來,我需要列好任務,告訴她如何才能做好這頓美食。以下就是我的任務清單。
1.去菜市場買菜
2.將買回來的菜熬成湯,做成好吃的東西
3.打電話告訴我的朋友通知他們過來聚餐
4.通知我回家

1,2,3步驟我們就理解爲異步的執行過程吧,但是我要告訴mm我希望的過程。那麼將上面的話用promise的寫法,就可以翻譯成

    // 告訴mm幫我做幾件連貫的事情,先去菜市場買菜
    new Promise(買菜)
    //用買好的菜做飯
    .then((買好的菜)=>{
        return new Promise(做飯);
    })
    //打電話告訴朋友們飯好了
    .then((做好的飯)=>{
        return new Promise(飯好了);
    })
    //通知完朋友打電話通知我
    .then((通知朋友玩了)=>{
        電話通知我();
    })

promise是承若我一定幫你做一件事情,並且會告訴你事情的結果。

那麼promise的核心還在於它的狀態機制,一共有三個狀態。

pending: 異步正在進行中
resolved: 執行成功
reject: 執行失敗

那麼按照狀態,則是pendding => resolved => 返回結果
=> reject => 返回錯誤結果
也就是說,執行成功後,pedding狀態變爲resolved,執行失敗後,pedding狀態變爲reject。這個狀態一旦發生改變了,就不會再變回去了。

#####那就開始實現吧

這裏就簡單說說思路,具體時間這裏整理時間也沒有那麼多了。

promise裏面有then,resolve,reject這兩個靜態方法,也提供了all,race這些方法。

開始的第一步:
初始化promise實例,需要達到的目的:
1.改變promise中的status狀態,將pedding狀態變爲resolved或者rejected。
2.當檢測到是成功還是失敗的狀態時,返回相應的執行函數,傳遞出promise執行對象

  (export default) class PromistMine{
    constructor(executor){
      // 狀態,promise裏面基本上就這種狀態了
      this.status = 'pedding';
      this.value = undefined; // 返回失敗的值
      this.reason = undefined; // 失敗成功原因
      this.onRejectCallBacks = []; // 存放失敗的回調,這個是成功的回調函數
      this.onResolveCallBacks = []; // 存放成功的回調,這個是失敗的回調函數

      /**
       * 處理的兩種狀態
       **/

      // 執行成功的狀態
      let resolve = (reason) => {
        if (this.status === 'pedding') {
          this.status = 'resolved';
          // 把成功後要返回的信息透傳
          this.reason = reason;
          this.onRejectCallBacks(fn => fn());
        }
      }
      
      // 執行失敗的狀態
      let reject = (data) => {
        if (this.status === 'pedding') {
          this.status = 'rejected';
          this.value = data;
          this.onRejectCallBacks(fn => fn())
        }
      }

      // 在new Promise時,都需要傳遞兩個參數,resolve,reject用於處理方法是否成功後的回調
      try {
        executor(resolve,reject);
      } catch (e) {
        reject(e) // 執行執行器,如果throw new Error('error') 就直接走reject了
      }
    }
  }

開始的第二步:
實現resolve和reject這兩個靜態方法。
resolve和reject這兩個方法本質上其實是一樣的。這裏是promise提供的兩個靜態方法,目的在於,狀態是成功或者失敗時,返回自身。
這兩個靜態方法其實和構造函數裏的resolve與reject原理一樣。

開始的第三步:
實現then的鏈式調用。
在平時使用promise,then字面意思是“下一步,接下來”。這個回調函數裏,可以做的事情是:1.執行上一步的方法,並且在上一步的返回結果裏執行一系列的操作。2.返回自身實例,記錄執行結果,便於鏈式調用。

在原型上添加方法,new的時候直接繼承了

  PromistMine.prototype.then((onFulfilled, onRejected) => {
    // 因爲從原型上新增的方法,所以可以拿到this作用域
    if (this.status === 'pendding') {
      // 將報錯或者成功的信息存起來
      this.onRejectCallBacks = this.onRejectCallBacks.push(onRejected);
      this.onResolveCallBacks = this.onResolveCallBacks.push(onFulfilled)
    } else if (this.status === FULFILLED){
      onFulfilled(value);
    } else {
      onRejected(this.reason);
    }
    return this; // 返回其本身
  })

大概的思路就是這些了。能看到這已經很不容易了。

記錄一下常用race,all的使用

先說說all吧。

promise的all方法,在官方的類型是這樣去描述這個方法的。它允許多個promise對象以數組的形式傳入。

 /**
     * Creates a Promise that is resolved with an array of results when all of the provided Promises
     * resolve, or rejected when any Promise is rejected.
     * @param values An array of Promises.
     * @returns A new Promise.
     */
    all<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>, T4 | PromiseLike <T4>, T5 | PromiseLike<T5>, T6 | PromiseLike<T6>, T7 | PromiseLike<T7>, T8 | PromiseLike<T8>, T9 | PromiseLike<T9>, T10 | PromiseLike<T10>]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]>;

promise提供的all方法,有一種共存亡的概念。當數組內所有的promise都被接受了,那麼執行成功的回調;如果傳入的promise數組中有一個被拒絕了,那麼就執行失敗後的回調;如果存在多個promise被拒絕了,返回的信息將是第一個promise被拒絕的消息。

for example,其他的狀況就不一一列舉了:

  const demo1 = new Promise((resolve, reject)=>{
    reject('接受到')
  });
  const demo2 = new Promise((resolve,reject)=>{
      setTimeout(()=>{
          reject('接收到了')
      },122)
  })
  Promise.all([demo1,demo2]).then(resolve=>{
      console.log(resolve)
  }, reject=>{
      console.log(reject)
  })
  output: 接受到

promise提供的race方法,race按表面的意思可以知道是賽跑的意思。race的入參和all方法是一樣。允許傳入多個promise對象。但是race更追求的是速度,不管是接受狀態還是拒絕狀態,誰快誰先輸出。

var promise1 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 110, 'one');
});

var promise2 = new Promise(function(resolve, reject) {
    setTimeout(reject, 100, 'two');
});

Promise.race([promise1, promise2]).then(function(value) {
  console.log(value);
  // Both resolve, but promise2 is faster
});

在前面提到,在promise中,有三種狀態,pending,resolved,rejected。但在pedding變爲resolved或者rejected時,這個狀態我們叫決議狀態。

在靜態方法reject,resolve這兩個方法,可以改變返回的狀態,reject,resolve不會直接傳參,而是將參數當做拒絕或者接受原因拒絕promise。

在resolve方法中,傳入一個reject,此時返回的也是一個reject,被拒絕的promise。

例如

new Promise((resolve, reject)=>{
  resolve(Promise.reject('---解決了---'));
}).then((resolve)=>{
  console.log('----成功解決---');
  console.log(resolve)
},(reject)=>{
  console.log('-----失敗解決---');
  console.log(reject);
})
> -----失敗解決---
> ---解決了---

記錄一下promise的then
promise提供了then的方法,方便函數的回調。而前面我們也提到,then它自身的意義在於記錄執行結果,返回其本身,這樣就可實現鏈式調用了。
promise在平時用到最多的場景,調用接口後再執行其他的操作。在我剛剛接觸promise時,總會寫出讓人驚奇的代碼,找錯誤找了半天,最主要的就是平時對promise的理解還不夠。
讓人驚奇代碼之一:

  new Promise((resolve,reject)=>{
    console.log('---執行步驟1---');
  }).then(()=>{
    console.log('---執行步驟2---');
  }).then(()=>{
    console.log('---執行步驟3---');
  })
  ---執行步驟1---

這裏的promise並沒有執行resolve,所以promise一直在一個決議的狀態,也沒有返回其自身的實例,所以then裏面的方法不執行那是當然的。

讓人驚奇代碼之二:

    const promise = new Promise((resolve, reject) => {
       resolve('完成');
    });
    promise.then((msg) => {
        console.log('first messaeg: ' + msg);
    }).then((msg) => {
        console.log('second messaeg: ' + msg);
    });
> first messaeg: 完成
> second messaeg: undefined

這裏一直取不到值,又是爲什麼呢?
悄悄的改一下代碼

    const promise = new Promise((resolve, reject) => {
       resolve('完成');
    });
    promise.then((msg) => {
        console.log('first messaeg: ' + msg);
        return 'new'+msg;
    }).then((msg) => {
        console.log('second messaeg: ' + msg);
    });
> first messaeg: 完成
> second messaeg: new完成

爲什麼呢?因爲第二個then裏,接受上一個return的返回值,也就是新的promise是根據上一個函數的返回來進行決議,但是上一個then並沒有return值,默認值爲undefined,所以第一段代碼會return一個undefined。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章