Promise 的鏈式調用與停止

Abstract

本文主要講的是如何實現 Promise 的鏈式調用。也就是 promise().then().then().catch() 的形式,而後討論如何在某一個 then() 裏面停止 Promise。node

在程序中,只要返回了一個 promise 對象,若是 promise 對象不是 Rejected 或 Fulfilled 狀態,then 方法就會繼續調用。利用這個特性,能夠處理多個異步邏輯。但有時候某個 then 方法的執行結果可能會決定是否須要執行下一個 then,這個時候就需停止 promise,主要思想就是使用 reject 來停止 promise 的 then 繼續執行。git

「停止」這個詞不知道用得是否準確。這裏可能仍是 break 的含義更精確,跳出本次 promise,不繼續執行後面的 then 方法。但 promise 依舊會繼續執行。github

Can I use promises

當前瀏覽器對 Promise 的支持狀況見下圖:數據庫

http://caniuse.com/#search=promisejson

caniusepromise

Promise

先簡單複習一下 Promise。Promise 其實很簡單,就是一個處理異步的方法。通常能夠經過 new 方法來調用 Promise 的構造器實例化一個 promise 對象:數組

var promise = new Promise((resolve, reject) => {
    // 異步處理
    // 處理結束後,調用 resolve 或 reject
    //      成功時就調用 resolve
    //      失敗時就調用 reject
});

new Promise 實例化的 promise 對象有如下三個狀態:promise

  • "has-resolution" - Fulfilled。resolve(成功)時,此時會調用 onFulfilled瀏覽器

  • "has-rejection" - Rejected。reject(失敗)時,此時會調用 onRejected網絡

  • "unresolved" - Pending。既不是resolve也不是reject的狀態,也就是promise對象剛被建立後的初始化狀態等session

關於上面這三種狀態的讀法,其中左側爲在 ES6 Promises 規範中定義的術語, 而右側則是在 Promises/A+ 中描述狀態的術語。基本上狀態在代碼中是不會涉及到的,因此名稱也無需太在乎。

promise state

Promise Chain

先來假設一個業務需求:在系統中使用教務系統帳號進行登陸。首先用戶在登陸頁面輸入用戶名(教務系統帳號)和密碼(教務系統密碼);而後判斷數據庫中是否存在該用戶;若是不存在則使用用戶名和密碼模擬登陸教務系統,若是模擬登陸成功,則存儲用戶名和密碼,並返回登陸成功。

聽起來就有點複雜對不對?因而畫了個流程圖來解釋整個業務邏輯:

flow char

上圖只是一個簡化版本,好比密碼加密、session設置等沒有表現出來,你們知道就好。圖中 (1)(2)(3) 三個地方就是會進行異步處理的地方,通常數據庫操做、網絡請求都是異步的。

若是用傳統的回調函數 callback 來處理上面的邏輯,嵌套的層級就會比較深,上面的業務由於有三個異步操做因此有三層回調,代碼大概會是下面的樣子:

// 根據 name 查詢用戶信息
findUserByName(name, function(err, userinfo) {
  if (err) {
    return res.json({
      code: 1000,
      message: '查詢用戶信息,數據庫操做數出現異常',
    });
  }


  if (userinfo.length > 0) {
  // 用戶存在
  if (userinfo[0].pwd === pwd)
    // 密碼正確
    return res.json({
      code: 0,
      message: '登陸成功',
    });
  }

  // 數據庫中不存在該用戶,模擬登陸教務系統
  loginEducationSystem(name, pwd, function(err, result) {
    if (err) {
      return res.json({
        code: 1001,
        message: '模擬登陸教務系統出現異常',
      });
    }

    // 約定正確狀況下,code 爲 0
    if (result.code !== 0) {
      return res.json({
        code: 1002,
        message: '模擬登陸教務系統失敗,多是用戶名或密碼錯誤',
      });
    }

    // 模擬登陸成功,將用戶名密碼存入數據庫
    saveUserToDB(name, pwd, function(err, result) {
      if (err) {
        return res.json({
          code: 1003,
          message: '將用戶名密碼存入數據庫出現異常',
        });
      }
      if (result.code !== 0) {
        return res.json({
          code: 1004,
          message: '將用戶名密碼存入數據庫出現異常',
        });
      }

      return res.json({
        code: 0,
        message: '登陸成功!',
      });
    });
  });
});

上面的代碼可能存在的不優雅之處:

  • 隨着業務邏輯變負責,回調層級會愈來愈深

  • 代碼耦合度比較高,不易修改

  • 每一步操做都須要手動進行異常處理,比較麻煩

接下來再用 promise 實現此處的業務需求。使用 promise 編碼以前,能夠先思考兩個問題。

一是如何鏈式調用,二是如何停止鏈式調用。

How to Use Promise Chain

業務中有三個須要異步處理的功能,因此會分別實例化三個 promise 對象,而後對 promise 進行鏈式調用。那麼,如何進行鏈式調用?

其實也很簡單,直接在 promise 的 then 方法裏面返回另外一個 promise 便可。例如:

function start() {
  return new Promise((resolve, reject) => {
    resolve('start');
  });
}

start()
  .then(data => {
    // promise start
    console.log('result of start: ', data);
    return Promise.resolve(1); // p1
  })
  .then(data => {
    // promise p1
    console.log('result of p1: ', data);
    return Promise.reject(2); // p2
  })
  .then(data => {
    // promise p2
    console.log('result of p2: ', data);
    return Promise.resolve(3); // p3
  })
  .catch(ex => {
    // promise p3
    console.log('ex: ', ex);
    return Promise.resolve(4); // p4
  })
  .then(data => {
    // promise p4
    console.log('result of p4: ', data);
  });

上面的代碼最終會輸出:

result of start:  start
result of p1:  1
ex:  2
result of p4:  4

代碼的執行邏輯如圖:

promise chain

從圖中能夠看出來,代碼的執行邏輯是 promise start --> promise p1 --> promise p3 --> promise p4。因此結合輸出結果和執行邏輯圖,總結出如下幾點:

  • promise 的 then 方法裏面能夠繼續返回一個新的 promise 對象

  • 下一個 then 方法的參數是上一個 promise 對象的 resolve 參數

  • catch 方法的參數是其以前某個 promise 對象的 rejecte 參數

  • 一旦某個 then 方法裏面的 promise 狀態改變爲了 rejected,則promise 方法連會跳事後面的 then 直接執行 catch

  • catch 方法裏面依舊能夠返回一個新的 promise 對象

How to Break Promise Chain

接下來就該討論如何停止 promise 方法鏈了。

經過上面的例子,咱們能夠知道 promise 的狀態改變爲 rejected 後,promise 就會跳事後面的 then 方法。

也就是,某個 then 裏面發生異常後,就會跳過 then 方法,直接執行 catch。

因此,當在構造的 promise 方法鏈中,若是在某個 then 後面,不須要再執行 then 方法了,就能夠把它看成一個異常來處理,返回一個異常信息給 catch,其參數可自定義,好比該異常的參數信息爲 { notRealPromiseException: true},而後在 catch 裏面判斷一下 notRealPromiseException 是否爲 true,若是爲 true,就說明不是程序出現異常,而是在正常邏輯裏面停止 then 方法的執行。

代碼大概就這樣:

start()
  .then(data => {
    // promise start
    console.log('result of start: ', data);
    return Promise.resolve(1); // p1
    )
  .then(data => {
    // promise p1
    console.log('result of p1: ', data);
    return Promise.reject({
      notRealPromiseException: true,
    }); // p2
  })
  .then(data => {
    // promise p2
    console.log('result of p2: ', data);
    return Promise.resolve(3); // p3
  })
  .catch(ex => {
    console.log('ex: ', ex);
    if (ex.notRealPromiseException) {
      // 一切正常,只是經過 catch 方法來停止 promise chain
      // 也就是停止 promise p2 的執行
      return true;
    }
    // 真正發生異常
    return false;
  });

這樣的作法可能不符合 catch 的語義。不過從某種意義上來講,promise 方法鏈沒有繼續執行,也能夠算是一種「異常」。

Refactor Callback with Promise

講了那麼多道理,如今就改來使用 promise 重構以前用回調函數寫的異步邏輯了。

// 據 name 查詢用戶信息
const findUserByName = (name, pwd) => {
  return new Promise((resolve, reject) => {
    // 數據庫查詢操做
    if (dbError) {
      // 數據庫查詢出錯,將 promise 設置爲 rejected
      reject({
        code: 1000,
        message: '查詢用戶信息,數據庫操做數出現異常',
      });
    }
    // 將查詢結果賦給 userinfo 變量
    if (userinfo.length === 0) {
      // 數據庫中不存在該用戶
      resolve();
    }
    // 數據庫存在該用戶,判斷密碼是否正確
    if (pwd === userinfo[0].pwd) {
      // 密碼正確,停止 promise 執行
      reject({
        notRealPromiseException: true,
        data: {
          code: 0,
          message: '密碼正確,登陸成功',
        }
      });
    }
    // 密碼不正確,登陸失敗,將 Promise 設置爲 Rejected 狀態
    reject({
      code: 1001,
      message: '密碼不正確,登陸失敗',
    });
  });
};


// 模擬登陸教務系統
const loginEducationSystem = (name, pwd) => {
  // 登陸邏輯...
  // 登陸成功
  resolve();
  // 登陸失敗
  reject({
    code: 1002,
    message: '模擬登陸教務系統失敗',
  });
};


// 將用戶名密碼存入數據庫
const saveUserToDB(name, pwd) => {
  // 數據庫存儲操做
  if (dbError) {
    // 數據庫存儲出錯,將 promise 設置爲 rejected
    reject({
      code: 1004,
      message: '數據庫存儲出錯,將出現異常',
    });
  }
  // 數據庫存儲操做成功
  resolve();
};


findUserByName(name)
.then(() => {
  return loginEducationSystem(name, pwd);
})
.then(() => {
  return saveUserToDB(name, pwd);
})
.catch(e => {
  // 判斷異常出現緣由
  if (e.notRealPromiseException) {
    // 正常停止 promise 而故意設置的異常
    return res.json(e.data);
  }
  // 出現錯誤或異常
  return res.json(e);
});

在上面的代碼中,實例化了三個 promise 對象,分別實現業務需求中的三個功能。而後經過 promise 方法鏈來調用。相比用回調函數而言,代碼結構更加清晰,也更易讀易懂耦合度更低更易擴展了。

Promise.all && Promise.race

仔細觀察能夠發現,在上面的 promise 代碼中,loginEducationSystemsaveUserToDB 兩個方法執行有前後順序要求,但沒有數據傳遞。

其實 promise 方法鏈更好用的一點是,當下一個操做依賴於上一個操做的結果的時候,能夠很方便地經過 then 方法的參數來傳遞數據。前面頁提到過,下一個 then 方法的參數就是上一個 then 方法裏面 resolve 的參數,因此固然就能夠把上一個 then 方法的執行結果做爲參數傳遞給下一個 then 方法了。

還有些時候,可能 then 方法的執行順序也沒有太多要求,只須要 promise 方法鏈中的兩個或多個 promise 所有都執行正確。這時,若是依舊一個一個去寫 then 可能就比較麻煩,好比:

function p1() {
  return new Promise((resolve) => {
    console.log(1);
    resolve();
  });
}

function p2() {
  return new Promise((resolve) => {
    console.log(2);
    resolve();
  });
}

function p3() {
  return new Promise((resolve) => {
    console.log(3);
    resolve();
  });
}

如今只須要 p1 p2 p3 這三個 promise 都執行,而且 promise 最終狀態都是 Fulfilled,那麼若是仍是使用方法鏈,這是這樣調用:

p1()
.then(() => {
  return p2();
})
.then(() => {
  return p3();
})
.then(() => {
  console.log('all done');
})
.catch(e => {
  console.log('e: ', e);
});

// 輸出結果:
// 1
// 2
// 3
// all done

代碼貌似就不那麼精煉了。這個時候就有了 Promise.all 這個方法。

Promise.all 接收一個 promise對象的數組做爲參數,當這個數組裏的全部 promise 對象所有變爲 resolve 或 reject 狀態的時候,它纔會去調用 then 方法。

因而,調用這幾個 promise 的代碼就能夠這樣寫了:

p1()
.then(() => {
  return Promise.all([
    p2(),
    p3(),
  ]);
})
.then(() => {
  console.log('all done');
})
.catch((e) => {
  console.log('e: ', e);
});

// 輸出結果:
// 1
// 2
// 3
// all done

這樣看起來貌似就精煉些了。

而對於 Promise.race,其參數也跟 Promise.all 同樣是一個數組。只是數組中的任何一個 promise 對象若是變爲 resolve 或者reject 的話,該函數就會返回,並使用這個 promise 對象的值進行 resolve 或者 reject。

這裏就不舉例了。

Conclusion

到目前爲止,咱們就基本瞭解了 Promise 的用法及特色,並實現用 Promise 重構用回調函數寫的異步操做。如今對 Promise 的使用,應該得心應手了。

完。


Github Issue: https://github.com/nodejh/nodejh.github.io/issues/23

相關文章
相關標籤/搜索