學習 Promise,掌握將來世界 JS 異步編程基礎

其實想寫 Promise 的使用已經很長時間了。一個是在實際編碼的過程當中常常用到,一個是確實有時候小夥伴們在使用時也會遇到一些問題。
Promise 也確實是 ES6 中 對於寫 JS 的方式,有着真正最大影響的 API 特性之一。
本文是實際使用使用過程當中的一個總結
看一下文件建立時間 2017-10-09,拖延症真是太可怕了。。。仍是得加強執行力啊!不忘初心,加油吧!
博客原址

前言 && 基礎概念

Promise 是解決 JS 異步的一種方案,相比傳統的回調函數,Promise 能解決多個回調嚴重嵌套的問題。html

Promise 對象表明一個異步操做,有三種狀態: pending、fulfilled 或 rejected ,狀態的轉變只能是 pending -> fulfilled 或者 pending -> rejected ,且這個過程一旦發生就不可逆轉前端

<!-- more -->node

我的認爲講解 Promise 實際上須要分紅兩個部分es6

  1. 對於 Promise 構造函數的使用說明。
  2. Promise 原型對象上的一些方法。

Promise 構造函數

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

Promise 構造函數接受一個函數做爲參數,該函數的兩個參數分別是 resolve 和 reject 。它們是兩個函數,由 JavaScript 引擎提供,不用本身部署。api

resolve 函數的做用是將 Promise 對象的狀態從「未完成」變爲「成功」(即從 pending 變爲 fulfilled ),在異步操做成功時調用,並將異步操做的結果,做爲參數傳遞出去;
reject 函數的做用是,將 Promise 對象的狀態從「未完成」變爲「失敗」(即從 pending 變爲 rejected ),在異步操做失敗時調用,並將異步操做報出的錯誤,做爲參數傳遞出去。promise

下面代碼創造了一個 Promise 實例。併發

function request() {
  return new Promise((resolve, reject) => {
    /* 異步操做成功 */
    setTimeout(() => {
      resolve("success");
    }, 1000);
    // 取消註釋這裏能夠體現,Promise 的狀態一旦變動就不會再變化的特性
    // reject('error');
  });
}

接收異步

request()
  .then(result => {
    console.info(result);
  })
  .catch(error => {
    console.info(error);
  });

上述 new Promise() 以後,除去用 catch 去捕獲錯誤以外,也能夠用 then 方法指定 resolvereject 的回調函數
也能達到捕獲錯誤的目的。函數

request().then(
  result => {
    console.info(result);
  },
  error => {
    console.info(error);
  }
);

原型上的方法

Promise.prototype.then()

p.then(onFulfilled, onRejected);

then 方法 是定義在 Promise.prototype 上的方法,如上面的例子同樣,有兩個參數,fulfilled 的回調函數和 rejected 的回調函數,第二個參數時可選的。

兩個關鍵點:

  1. then 方法的返回值是一個新的 Promise 實例,因此對於調用者而言,拿到一個 Promise 對象,調用 then 後仍然返回一個 Promise ,而它的行爲與 then 中的回調函數的返回值有關。以下:
  • 若是 then 中的回調函數返回一個值,那麼 then 返回的 Promise 將會成爲接受狀態,而且將返回的值做爲接受狀態的回調函數的參數值。
  • 若是 then 中的回調函數拋出一個錯誤,那麼 then 返回的 Promise 將會成爲拒絕狀態,而且將拋出的錯誤做爲拒絕狀態的回調函數的參數值。
  • 若是 then 中的回調函數返回一個已是接受狀態的 Promise,那麼 then 返回的 Promise 也會成爲接受狀態,而且將那個 Promise 的接受狀態的回調函數的參數值做爲該被返回的 Promise 的接受狀態回調函數的參數值。
  • 若是 then 中的回調函數返回一個已是拒絕狀態的 Promise,那麼 then 返回的 Promise 也會成爲拒絕狀態,而且將那個 Promise 的拒絕狀態的回調函數的參數值做爲該被返回的 Promise 的拒絕狀態回調函數的參數值。
  • 若是 then 中的回調函數返回一個未定狀態(pending)的 Promise,那麼 then 返回 Promise 的狀態也是未定的,而且它的終態與那個 Promise 的終態相同;同時,它變爲終態時調用的回調函數參數與那個 Promise 變爲終態時的回調函數的參數是相同的。
  1. 鏈式調用。把嵌套回調的代碼格式轉換成一種鏈式調用的縱向模式。

好比說回調形式: 一個回調地獄的例子

a(a1 => {
  b(a1, b1 => {
    c(b1, c1 => {
      d(c1, d1 => {
        console.log(d1);
      });
    });
  });
});

這樣的橫向擴展能夠修改爲(a,b,c,d)均爲返回 Promise 的函數

a()
  .then(b)
  .then(c)
  .then(d)
  .then(d1 => {
    console.log(d1);
  });
//===== 可能上面的例子並不太好看 ===下面這樣更直觀
a()
  .then(a1 => b(a1))
  .then(b1 => c(b1))
  .then(c1 => d(c1))
  .then(d1 => {
    console.log(d1);
  });

這樣的縱向結構,看上去清爽多了。

Promise.prototype.catch()

除了 then() ,在 Promise.prototype 原型鏈上的還有 catch() 方法,這個是拒絕的狀況的處理函數。

其實 它的行爲與調用 Promise.prototype.then(undefined, onRejected) 相同。 (事實上, calling obj.catch(onRejected) 內部 calls obj.then(undefined, onRejected)).

// 1.
request().then(
  result => {
    console.info(result);
  },
  error => {
    console.info(error);
  }
);

// 2.
request()
  .then(result => {
    console.info(result);
  })
  .catch(error => {
    console.info(error);
  });

如上這個例子:兩種方式在使用,與結果基本上是等價的,可是 仍然推薦第二種寫法,下面我會給出緣由:

  1. 在 Promise 鏈中 Promise.prototype.then(undefined, onRejected)onRejected 方法沒法捕獲當前 Promise 拋出的錯誤,然後續的 .catch 能夠捕獲以前的錯誤。
  2. 代碼冗餘
new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("reject");
  }, 1000);
})
  .then(
    result => {
      console.log(result + "1");
      throw Error(result + "1"); // 拋出一個錯誤
    },
    error => {
      console.log(error + ":1"); // 不會走到這裏
    }
  )
  .then(
    result => {
      console.log(result + "2");
      return Promise.resolve(result + "2");
    },
    error => {
      console.log(error + ":2");
    }
  );
// reject1, Error: reject1:2

若是使用 .catch 方法,代碼會簡化不少,這樣其實是延長了 Promise 鏈

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("reject");
  }, 1000);
})
  .then(result => {
    console.log(result + "1");
    throw Error(result + "1"); // 拋出一個錯誤
  })
  .then(result => {
    console.log(result + "2");
    return Promise.resolve(result + "2");
  })
  .catch(err => {
    console.log(err);
  });
// reject1, Error: reject1:2

Promise.prototype.finally()

暫未徹底成爲標準的一部分,處於:Stage 4

finally() 方法返回一個 Promise,在執行 then()catch() 後,都會執行finally指定的回調函數。(回調函數中無參數,僅僅表明 Promise 的已經結束

等同於使用 .then + .catch 延長了原有的 Promise 鏈的效果,避免一樣的語句須要在 then()catch() 中各寫一次的狀況。

mdn-Promise-finally

Promise 對象上的方法

Promise.all() 用來處理 Promise 的併發

Promise.all 會將多個 Promise 實例封裝成一個新的 Promise 實例,新的 promise 的狀態取決於多個 Promise 實例的狀態,只有在全體 Promise 都爲 fulfilled 的狀況下,新的實例纔會變成 fulfilled 狀態。;若是參數中 Promise 有一個失敗(rejected),此實例回調失敗(rejecte),失敗緣由的是第一個失敗 Promise 的結果。

舉個例子:

Promise.all([
  new Promise(resolve => {
    setTimeout(resolve, 1000, "p1");
  }),
  new Promise(resolve => {
    setTimeout(resolve, 2000, "p2");
  }),
  new Promise(resolve => {
    setTimeout(resolve, 3000, "p3");
  })
])
  .then(result => {
    console.info("then", result);
  })
  .catch(error => {
    console.info("catch", error);
  });
// [p1,p2,p3]

Promise.all([
  new Promise(resolve => {
    setTimeout(resolve, 1000, "p1");
  }),
  new Promise(resolve => {
    setTimeout(resolve, 2000, "p2");
  }),
  Promise.reject("p3 error")
])
  .then(result => {
    console.info("then", result);
  })
  .catch(error => {
    console.info("catch", error);
  });
// p3 error

獲取 cnode 社區的 精華貼的前十條內容

fetch("https://cnodejs.org/api/v1/topics?tab=good&limit=10")
  .then(res => res.json())
  .then(res => {
    const fetchList = res.data.map(item => {
      return fetch(`https://cnodejs.org/api/v1/topic/${item.id}`)
        .then(res => res.json())
        .then(res => res.data);
    });
    Promise.all(fetchList).then(list => {
      console.log(list);
    });
  });

Promise.race() 競態執行

Promise.race 也會將多個 Promise 實例封裝成一個新的Promise實例,只不過新的 Promise 的狀態取決於最早改變狀態的 Promise 實例的狀態。

在前端最典型的一個用法是爲 fetch api 模擬請求超時。

Promise.race([
  fetch("https://cnodejs.org/api/v1/topics?tab=good&limit=10").then(res =>
    res.json()
  ),
  new Promise((resolve, reject) => {
    setTimeout(reject, 1, "error");
  })
])
  .then(result => {
    console.info("then", result);
  })
  .catch(error => {
    console.info("catch", error); // 進入這裏
  });

上述例子中只要請求 未在 1 毫秒內結束就會進入 .catch() 方法中,雖然不能將請求取消,可是超時模擬卻成功了

Promise.resolve(value) && Promise.reject(reason)

這兩個方法都能用來建立並返回一個新的 Promise , 區別是 Promise.resolve(value) 攜帶進新的 Promise 狀態是 fulfilled。而 Promise.reject(reason) 帶來的 rejected

有的時候能夠用來簡化一些建立 Promise 的操做如:

const sleep = (time = 0) => new Promise(resolve => setTimeout(resolve, time));
// 這裏建立一個 睡眠,而且打印的鏈
Promise.resolve()
  .then(() => {
    console.log(1);
  })
  .then(() => sleep(1000))
  .then(() => {
    console.log(2);
  })
  .then(() => sleep(2000))
  .then(() => {
    console.log(3);
  });

有時也用來 手動改變 Promise 鏈中的返回狀態 ,固然這樣實際上和 直接返回一個值,或者是 使用 throw Error 來構造一個錯誤,並沒有區別。到底要怎麼用 就看我的喜愛了

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("resolve"); // 1.
  }, 1000);
})
  .then(result => {
    return Promise.reject("reject1"); // 2.
  })
  .then(
    result => {
      return Promise.resolve(result + "2");
    },
    err => {
      return Promise.resolve(err); // 3.
    }
  )
  .then(res => {
    console.log(res); // 4.
  })
  .catch(err => {
    console.log(err + "err");
  });
// reject1

幾個例子

下面來看幾個例子:

關於執行順序,具體可搜索,js 循環

new Promise((resolve, reject) => {
  console.log("step 1");
  resolve();
  console.log("step 2");
}).then(() => {
  console.log("step 3");
});
console.log("step 4");

// step 1, step 2, step 4 , step 3

在使用 Promise 構造函數構造 一個 Promise 時,回調函數中的內容就會當即執行,而 Promise.then 中的函數是異步執行的。

關於狀態不可變動

let start;
const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    start = Date.now();
    console.log("once");
    resolve("success");
  }, 1000);
});
p.then(res => {
  console.log(res, Date.now() - start);
});
p.then(res => {
  console.log(res, Date.now() - start);
});
p.then(res => {
  console.log(res, Date.now() - start);
});

Promise 構造函數只執行一次,內部狀態一旦改變,有了一個值,後續不論調用多少次then()都只拿到那麼一個結果。

關於好像狀態能夠變動

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("success");
  }, 1000);
});

const p2 = p1.then((resolve, reject) => {
  throw new Error("error");
});

console.log("p1", p1);
console.log("p2", p2);

setTimeout(() => {
  console.log("p1", p1);
  console.log("p2", p2);
}, 2000);

觀察這一次的打印
第一次打印出兩個 Promise 的時候都是 pending ,由於 p2 是基於 p1 的結果,p1 正在 pending ,當即打印出的時候確定是 pending ;第二次打印的時候,由於 p1 的狀態爲 resolved ,p2 爲 rejected ,這個並非已經爲 fulfilled 狀態改變爲 rejected ,而是 p2 是一個新的 Promise 實例,then() 返回新的 Promise 實例。

關於透傳

Promise.resolve(11)
  .then(1)
  .then(2)
  .then(3)
  .then(res => {
    console.info("res", res);
  });
//   11

給 then 方法傳遞了一個非函數的值,等同於 then(null),會致使穿透的效果,就是直接過掉了這個 then() ,直到符合規範的 then() 爲止。

Promise 的串行調用

使用 Array.reduce 方法串行執行 Promise

const sleep = (time = 0) => new Promise(resolve => setTimeout(resolve, time));
[1000, 2000, 3000, 4000].reduce((Promise, item, index) => {
  return Promise.then(res => {
    console.log(index + 1);
    return sleep(item);
  });
}, Promise.resolve());
// 在分別的等待時間後輸出 1,2,3,4

這篇文章到這裏就基本上結束了,相信 若是能理解上面的內容,而且在實際項目中使用的話。應該會讓工做更高效吧,對於新的異步使用應該也會更加的駕輕就熟。Promise 的使用相對簡單,可能後續再出一篇如何實現一個 Promise 吧

那些收集的 Promise 的優質文章。

相關文章
相關標籤/搜索