前言
promise 是前端開發人員必須掌握的知識點,本文來總結一下相關學習筆記。
正文
1、什麼是prommise,promise 解決了什麼問題
a、promise 是什麼
Promise 是承諾的意思,承諾它過一段時間會給你一個結果。Promise 是一種解決非同步程式設計的方案,相比回撥函式和事件更合理和更強大。從語法上講,promise 是一個物件,從它可以獲取非同步操作的訊息;
promise 有三種狀態:pending 初始狀態也叫等待狀態,fulfiled成功狀態,rejected 失敗狀態;狀態一旦改變,就不會再變。創造 promise例項後,它會立即執行。需要注意,promise 的狀態是不可逆的,一旦狀態由 pending 變為 fulfiled 或者reject 狀態,意味著已經產生了結果,同樣,轉為成功狀態會有成功的結果,轉為失敗狀態會返回失敗的原因。
promise 作為建構函式,接收兩個引數,分別是成功和失敗的回撥函式。
b、promise 解決了什麼問題
我們先來看如下程式碼,並不陌生
setTimeout(function () { console.log("開始執行"); }, 3000);
上面的程式碼,粗略的可以認為在 3 秒後,程式輸出開始執行,但是如果業務比較複雜,我們想在3秒後輸出開始執行,再隔 3 秒列印一次第二次執行呢?接著在隔 3 秒列印第三次執行,程式碼會這樣寫:
setTimeout(function () { console.log("開始執行"); setTimeout(function () { console.log("第二次執行"); setTimeout(function () { console.log("第三次執行"); }, 3000); }, 3000); }, 3000);
再看上面的程式碼,如果後面的需求再次優化,需要類似的列印第 4,5,6 次呢?我們的程式碼還是這樣一層層巢狀起來嗎? 這樣多層函式之間互相巢狀,就產生了回撥地獄的問題,這樣寫程式碼有個很大的缺點:(1)程式碼耦合行太強,牽一髮而動全身,可維護性很差,同樣,大量冗餘的程式碼互相巢狀,可讀性很差。因此,為了解決回撥地獄的問題,ES6 提出了 Promise。通過 promise 將上面的程式碼改裝一下將顯的程式碼優雅很多:
function sleep(second) { return new Promise((resolve, reject) => { setTimeout(() => resolve(), second * 1000); }); } sleep(3) .then(() => { console.log("開始執行"); return sleep(3); }) .then(() => { console.log("第二次執行"); return sleep(3); }) .then(() => { console.log("第三次執行"); });
2、ES6 中 promise 的使用
1)then 鏈式呼叫
從表面上看,Promise只是能夠簡化層層回撥的寫法,而實質上,Promise 的精髓是“狀態”,用維護狀態、傳遞狀態的方式來使得回撥函式能夠及時呼叫,它比傳遞 callback 函式要簡單、靈活的多。所以使用 Promise 的正確場景是這樣的:
p.then((data) => { console.log(data); }) .then((data) => { console.log(data); }) .then((data) => { console.log(data); });
then 是例項狀態發生改變時的回撥函式,第一個引數是 resolved 狀態的回撥函式,第二個引數是 rejected 狀態的回撥函式,then方法返回的是一個新的Promise例項,也就是promise能鏈式書寫的原因。預設常寫第一個引數即可,reject 狀態的回撥可以通過 catch 來捕獲異常。
2)catch 方法用來指定 promise 例項狀態變為 rejected 的捕獲
catch 捕獲reject狀態的回撥,相當於 then中的第二個引數,一般寫法如下:
p.then((data) => { console.log("resolved", data); }).catch((err) => { console.log("rejected", err); });
也就是說進到catch方法裡面去了,而且把錯誤原因傳到了reason引數中。即便是有錯誤的程式碼也不會報錯了,這與我們的try/catch語句有相同的功能。
3)all 方法將多個 promise 例項包裝成一個新的 promise 例項(誰跑的慢,以誰為準執行回撥)
Promise.all 方法接收一個陣列(可迭代物件)作為引數,並且陣列中的每個元素都是 Promise 例項,最終返回結果也為一個 Promise 物件,例如:
const p = Promise.all([p1, p2, p3]),例項p的狀態由p1、p2、p3決定,分為兩種:
只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個陣列,傳遞給p的回撥函式;
只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的例項的返回值,會傳遞給p的回撥函式;
常見的寫法如下:
let Promise1 = new Promise(function (resolve, reject) {}); let Promise2 = new Promise(function (resolve, reject) {}); let Promise3 = new Promise(function (resolve, reject) {}); let p = Promise.all([Promise1, Promise2, Promise3]); p.then( (res) => { // 三者都成功則成功,成功後處理 }, (err) => { // 三者只要有失敗的就返回失敗,失敗後處理 } );
有了all 方法,我們就可以並行執行多個非同步操作,並且在一個回撥中處理所有的返回資料,比如開發中開啟網頁時,預先載入需要用到的各種資源如圖片、flash 以及各種靜態檔案,所有的都載入完後,我們再進行頁面的初始化。
(4)race 方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項(誰跑的快,以誰為準執行回撥)
const p = Promise.race([p1, p2, p3]);只要p1、p2、p3之中有一個例項率先改變狀態,p的狀態就跟著改變,率先改變的 Promise 例項的返回值則傳遞給p的回撥函式。
使用場景:比如,我們在頁面載入的時候,需要請求後端獲取某個圖片的URL,這裡我們可以設定請求的超時時間,當在設定的時間內後端介面沒有返回時,頁面給出請求超時提示。程式碼如下:
//請求某個圖片資源 function requestImg() { var p = new Promise((resolve, reject) => { var img = new Image(); img.onload = function () { resolve(img); } img.src = '圖片的路徑'; }); return p; } //延時函式,用於給請求計時 function timeout() { var p = new Promise((resolve, reject) => { setTimeout(() => { reject('圖片請求超時'); }, 5000); }); return p; } Promise.race([requestImg(), timeout()]).then((data) => { console.log(data); }).catch((err) => { console.log(err); });
3、promise 的缺點
1)無法取消 Promise,一旦新建它就會立即執行,無法中途取消
2)如果不設定回撥函式,Promise 內部丟擲的錯誤,不會反映到外部
3)當處於 pending(等待)狀態時,無法得知目前進展到哪一個階段,是剛剛開始還是即將完成
4、手動實現 Promise
function resolvePromise(promise2, x, resolve, reject) { //判斷x是不是promise //規範中規定:我們允許別人亂寫,這個程式碼可以實現我們的promise和別人的promise 進行互動 if (promise2 === x) {//不能自己等待自己完成 return reject(new TypeError('迴圈引用')); }; // x是除了null以外的物件或者函式 if (x != null && (typeof x === 'object' || typeof x === 'function')) { let called;//防止成功後呼叫失敗 try {//防止取then是出現異常 object.defineProperty let then = x.then;//取x的then方法 {then:{}} if (typeof then === 'function') {//如果then是函式就認為他是promise //call第一個引數是this,後面的是成功的回撥和失敗的回撥 then.call(x, y => {//如果Y是promise就繼續遞迴promise if (called) return; called = true; resolvePromise(promise2, y, resolve, reject) }, r => { //只要失敗了就失敗了 if (called) return; called = true; reject(r); }); } else {//then是一個普通物件,就直接成功即可 resolve(x); } } catch (e) { if (called) return; called = true; reject(e) } } else {//x = 123 x就是一個普通值 作為下個then成功的引數 resolve(x) } } class Promise { constructor(executor) { //預設狀態是等待狀態 this.status = 'panding'; this.value = undefined; this.reason = undefined; //存放成功的回撥 this.onResolvedCallbacks = []; //存放失敗的回撥 this.onRejectedCallbacks = []; let resolve = (data) => {//this指的是例項 if (this.status === 'pending') { this.value = data; this.status = "resolved"; this.onResolvedCallbacks.forEach(fn => fn()); } } let reject = (reason) => { if (this.status === 'pending') { this.reason = reason; this.status = 'rejected'; this.onRejectedCallbacks.forEach(fn => fn()); } } try {//執行時可能會發生異常 executor(resolve, reject); } catch (e) { reject(e);//promise失敗了 } } then(onFuiFilled, onRejected) { //防止值得穿透 onFuiFilled = typeof onFuiFilled === 'function' ? onFuiFilled : y => y; onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; } let promise2;//作為下一次then方法的promise if (this.status === 'resolved') { promise2 = new Promise((resolve, reject) => { setTimeout(() => { try { //成功的邏輯 失敗的邏輯 let x = onFuiFilled(this.value); //看x是不是promise 如果是promise取他的結果 作為promise2成功的的結果 //如果返回一個普通值,作為promise2成功的結果 //resolvePromise可以解析x和promise2之間的關係 //在resolvePromise中傳入四個引數,第一個是返回的promise,第二個是返回的結果,第三個和第四個分別是resolve()和reject()的方法。 resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e); } }, 0) }); } if (this.status === 'rejected') { promise2 = new Promise((resolve, reject) => { setTimeout(() => { try { let x = onRejected(this.reason); //在resolvePromise中傳入四個引數,第一個是返回的promise,第二個是返回的結果,第三個和第四個分別是resolve()和reject()的方法。 resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e); } }, 0) }); } //當前既沒有完成也沒有失敗 if (this.status === 'pending') { promise2 = new Promise((resolve, reject) => { //把成功的函式一個個存放到成功回撥函式陣列中 this.onResolvedCallbacks.push(() => { setTimeout(() => { try { let x = onFuiFilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0) }); //把失敗的函式一個個存放到失敗回撥函式陣列中 this.onRejectedCallbacks.push(() => { setTimeout(() => { try { let x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }, 0) }) }) } return promise2;//呼叫then後返回一個新的promise } catch(onRejected) { // catch 方法就是then方法沒有成功的簡寫 return this.then(null, onRejected); } } Promise.all = function (promises) { //promises是一個promise的陣列 return new Promise(function (resolve, reject) { let arr = []; //arr是最終返回值的結果 let i = 0; // 表示成功了多少次 function processData(index, data) { arr[index] = data; if (++i === promises.length) { resolve(arr); } } for (let i = 0; i < promises.length; i++) { promises[i].then(function (data) { processData(i, data) }, reject) } }) } // 只要有一個promise成功了 就算成功。如果第一個失敗了就失敗了 Promise.race = function (promises) { return new Promise((resolve, reject) => { for (var i = 0; i < promises.length; i++) { promises[i].then(resolve, reject) } }) } // 生成一個成功的promise Promise.resolve = function (value) { return new Promise((resolve, reject) => resolve(value); } // 生成一個失敗的promise Promise.reject = function (reason) { return new Promise((resolve, reject) => reject(reason)); } module.exports = Promise;
5、async 和 await相關
async/await 是 ES7 提出的基於 Promise 的解決非同步的最終方案。
async 就是 generation 和 promise 的語法糖,async 就是將 generator的*換成 async,將 yiled 換成 await函式前必須加一個 async,非同步操作方法前加一個 await 關鍵字,意思就是等一下,執行完了再繼續走,注意:await 只能在 async 函式中執行,否則會報錯,Promise 如果返回的是一個錯誤的結果,如果沒有做異常處理,就會報錯,所以用 try..catch 捕獲一下異常就可以了。
async
async是一個加在函式前的修飾符,被async定義的函式會預設返回一個Promise物件resolve的值。因此對async函式可以直接then,返回值就是then方法傳入的函式。使用如下:
async function fun() { console.log(1); return 1; } fun().then(val => { console.log(val) // 1,1 })
await
await 也是一個修飾符,只能放在async定義的函式內。可以理解為等待。await 修飾的如果是Promise物件:可以獲取Promise中返回的內容(resolve或reject的引數),且取到值後語句才會往下執行;如果不是Promise物件:把這個非promise的東西當做await表示式的結果。使用如下:
async function fun() { let a = await new Promise((resolve, reject) => { setTimeout(function () { resolve('setTimeout promise') }, 3000) }) let b = await "表示式"; let c = await function () { return '函式表示式' }() console.log(a, b, c) } fun(); // 3秒後輸出:"setTimeout promise" "表示式" "函式表示式"
寫在最後
以上就是本文的全部內容,希望給讀者帶來些許的幫助和進步,方便的話點個關注,小白的成長之路會持續更新一些工作中常見的問題和技術點。