JavaScript – Promise

前言

我學 Promise 的時候, 那時還沒有 es6. 曾經還自己實現過. 但時隔多年, 現在 es6 的 promise 已經很完善了.

這篇作爲一個簡單的複習. (畢竟我已經 1 年多沒有寫 JS 了...)

以前寫過相關的文章:

Javascript Promise 學習(上)

Javascript Promise 學習 (中)

$q 就是angular 的promise

angular2 學習筆記 ( Rxjs, Promise, Async/Await 的區別 )

 

參考

阮一峯 – Promise 對象

 

Promise 解決的問題

什麼是異步和回調 callback

JS 是單線程, 無法併發處理事情, 但是它可以異步. 比如發 http request, request 通過網卡發出去後, CPU 就去做別的事情, 而不是傻傻等網卡回覆.

當 response 回來以後, 網卡通知 CPU, 這時再把任務接回來. 這個動作就叫 callback. 就是你別等我, 我好了會通知你.

callback 的寫法長這樣

const callback = () => {
  console.log('timeout');
};
setTimeout(callback, 2000);

setTimeout 是一個異步函數, 調用後, 遊覽器會用另一個線程去計算時間, 主線程繼續處理其它代碼. 時間到, 主線程會被通知, 然後運行後續 (callback) 的代碼.

大概是這個概念. 其它的異步函數包括 Ajax, FileReader 等等 (通常涉及到磁盤 IO, 網路請求都會是異步的. 因爲做這些事情的時候不需要 CPU).

回調地獄

callback 的寫法一旦嵌套就會變成很醜, unreadable.

比如, 我想寫一個

delay 3 秒,

運行 console 'a'

再 delay 2 秒

運行 console 'b'

再 delay 1 秒

運行 console 'c'

寫出來長這樣:

setTimeout(() => {
  console.log('a');
  setTimeout(() => {
    console.log('b');
    setTimeout(() => {
      console.log('c');
    }, 1000);
  }, 2000);
}, 3000);

醜不醜? Promise 就是用來解決醜這個問題的. 它可以把嵌套的回調 "打平" flat

 

Promise 基本用法

Promise 的核心是封裝了異步函數的調用, 和 callback 的寫法, 記住這個點.

promise 使用是這樣的

const promise = new Promise((resolve, _reject) => {
  setTimeout(() => {
    resolve('return value 123');
  }, 3000);
});
promise.then((returnValue) => {
  console.log('returnValue', returnValue); // return value 123
});

分 2 個階段看待

初始化 Promise

Promise 是一個 class. 實例化它會等到 promise 對象.

實例化時, 需要傳入一個函數. 函數裏面封裝了要執行的異步代碼.

比如上面的 setTimeout. 或者是 Ajax, FileReader 等等都行.

resolve 是一個 callback 代理, 當異步完成以後. 我們調用 resolve 告知 Promise 異步完成了. 並且返回異步函數的返回值 (比如 Ajax 後的 response data)

註冊 callback

註冊 callback 是通過 promise 對象來實現的. 調用 .then 函數把 callback 傳進去

callback 會在 resolve 的時候被執行, 並且獲得異步函數的返回值.

注: 有沒有返回值都是 ok 的.

意義何在? 

像這樣把異步函數和 callback wrap 起來, 意義何在呢? 如果只是 1 個 callback 那麼沒有什麼太大的意義.

記得, Promise 要解決的是嵌套的 callback (回調地獄)

 

.then Combo

我們把上面的 setTimeout 用 Promise 封裝一下

function delayAsync(delayTime: number): Promise<void> {
  const promise = new Promise<void>((resolve, _reject) => {
    setTimeout(() => {
      resolve();
    }, delayTime);
  });
  return promise;
}

delayAsync(3000).then(() => {
  console.log('a');
});

相等於

setTimeout(() => {
  console.log('a');
}, 3000);

return promise + .then combo

如果要再嵌套一個 delay, 你可能會認爲是這樣寫

delayAsync(3000).then(() => {
  console.log('a');
  delayAsync(2000).then(() => {
    console.log('b');
  });
});

雖然這個也可以跑, 但是正確的用法不是這樣. 而是這樣

delayAsync(3000)
  .then(() => {
    console.log('a');
    return delayAsync(2000);
  })
  .then(() => {
    console.log('b');
  });

在第一個 then 裏, 我們返回了另一個 promise 對象.

然後再第一個 then 之後 combo 了另一個 then.

這樣的寫法就成功的把嵌套的回調 "打平" 了.

Promise 內部實現原理

其實沒有必要懂底層邏輯, 會用就可以了. 簡單瞭解一下到時可以啦.

Promise 對象的 then 負責註冊 callback. 同時它返回另一個 promise. 你可以把它理解爲一個 child promise (連續幾個 .then 就變成了一個 promise chain)

callback 除了可以返回普通的 value 也可以返回一個 promise 對象.

當返回 promise 對象, Promise 就會等待這個 promise 對象 resolve 才執行 callback.

Promise 內部就是維護着 promise chain 和所以 callback 的執行順序. 這樣就做到了 "打平" 的寫法了.

感悟

Promise 在 es6 之前就有了, 在 JS 的語法基礎上, 通過封裝實現另一種調用方式, 讓代碼更好寫, 更好讀.

jQuery 也是有這種 feel. 還有 Fluent Builder 模式 也是這樣. 都是很聰明的實現.

 

Promise 的執行順序

console.log('1');
const promise = new Promise<void>((resolve) => {
  console.log('2');
  resolve();
  console.log('3');
});
promise.then(() => {
  console.log('5');
});
console.log('4');

new Promise 傳入的函數會馬上被執行 (裏面通常會調用異步函數, 但並沒有強制, 你也可以直接調用 resolve 返回的)

上面我刻意搞了一個同步執行的情況, resolve 雖然馬上被執行了, 但是 callback 並沒有馬上被執行.

一直等到 console.log(4) 完了以後 callback 才被執行.

也就是說任何 promise 的 callback 都會被押後執行, 即使 resolve 沒有被異步調用. 這個是唯一需要特別注意的.

after resolve 依然執行代碼 ?

best practice 的話, resolve 之後就不應該執行代碼了.

刻意習慣性的在 resolve 前加上 return, 確保後續沒有執行代碼. (不然挺亂的)

const promise = new Promise<void>((resolve) => {
  console.log('2');
  return resolve();
});

 

Reject and Catch

上面提到的例子都是 succeed 的情況. Promise 

 

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