Promise 學習筆記一 :: 有 Promise 好安心

Promise 學習筆記一 :: 有 Promise 好安心
Promise 學習筆記一 :: 有 Promise 好安心

想像一下當你從網路上購買完商品後,只需要等包裹配送成功或是配送失敗,就可以去做原本的事情,直到包裹到貨再去開箱或是包裹配送失敗聯絡客服處理後續事宜。

網路購物的範例就跟Promise處理非同步的邏輯很像,程式碼如下:

new Promise((resolve, reject) => {
	setTimeout(() => {
		resolve(`包裹送達`)
	}, 5000);
})
	.then(
		result => {
			console.log(`success: `, result)
		},
		reason => {
			console.log(`error: `, reason)
		}
	)
	
// 輸出 "success: 包裹送達"

在實際開發中常會需要與API溝通取得或傳遞資料的情境,並且與API溝通是採取非同步的方式,所以非常適合使用搭配Promise來處理這樣的事情。

在還沒產生Promise的時候,也是有執行API來取得資料的情境,那這時都如何處理呢?

以下將XMLHttpRequest封裝一個簡單的request函式來舉例。

function request(url, data, success, error){
	const xhr = new XMLHttpRequest();
	xhr.onreadystatechange = function(){
		// 當 readyState === 4 表示請求成功
		if (xhr.readyState !== 4) {
			return;
		}
		if (xhr.status >= 200 && xhr.status < 300) {
			// 當成功時執行success回呼函式並將ResponseText訊息傳入
			success(xhr.responseText);
		}else{
			// 執行error回呼函式將錯誤訊息傳入
			error(xhr.status);
		}
	}
	xhr.open('POST', url, true);
	xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
	xhr.send(JSON.stringify(data));
}

情境是需要先請求A,完成後再將A的回傳值給B進行請求。

request(
	`xxxx.com/api/getA`,
	{},
	function (resultA){
		console.log(`success A: `, resultA);
		request(
			`xxxx.com/api/getA`,
			{resultA},
			function (resultB){
				console.log(`success B: `, resultB);
			}
			function (status,statusText){
				console.log(`error B: `, result, `, `,  statusText);
			}
		)
	},
	function (status,statusText){
		console.log(`error A: `, result, `, `,  statusText);
	}
)

從以上例子可以看到請求B的request會在請求A的success閉包(Closure)中,這時已經產生多層嵌套的問題。

這個例子兩層嵌套已經對閱讀但來不好的體驗維護性也不好,若嵌套三層、四層、五層將會是非常糟糕的事情,這也稱為回呼地獄(Callback Hell)。

除了嵌套是個問題以外且不同開發者開發的函式其制定的回呼方式也不盡相同。

Promise/A+ 規範的產生約莫是在2015年之前,主要是來解決上面所提到的兩項問題"非同步請求處理方式不統一"及"回呼函式產生的回呼地獄,並且希望開發者都來遵守這套規範。

簡單來說只要一個物件有then函式就算是Promise。

const obj = {
	then: (resolve, reject) => {
	
	}
}

甚至是陣列中有then也算是Promise。

const arr = [];
arr.then = (resolve, reject) => {

}

在2015年ES6推出的Promise也遵循了Promise/A+規範。

const p1 = new Promise((resolve, reject) => {

})

因為ES6 Promise也遵循了Promise/A+規範,所以ES6 Promise可以跟自己實作的Promise進行互動相互使用的。

const obj = {
	then: function(resolve, reject){
		resolve(`custom promise`);
	}
}
const p1 = new Promise((resolve, reject) => {
	resolve(obj)
})
	.then(value => {
		console.log(value)
	})

// 輸出 "custom promise"

ES6 Promise除了遵循Promise/A+規範外,還加了不少方便的函式可以使用。

  • catch
  • finally
  • Promise.all
  • Promise.allSettled
  • Promise.any
  • Promise.race
  • Promise.reject
  • Promise.resolve
  • Promise.try
  • Promise.withResolvers

套用文章開頭的例子如下

  1. Pending (購買完商品等待商品配送)
  2. Fulfilled (配送成功)
  3. Rejected (配送失敗)

狀態的改變只有兩種

  • Pending => Fulfilled
  • Pending => Rejected
  • 狀態改變後無法再次更改狀態

執行resolve函式會導致狀態改變 Pending => Fulfilled,同時將傳入值帶入then函式中的第一個回呼函式表示執行成功。

new Promise((resolve, reject) => {
	resolve(`nicklabs.cc`)
})
	.then(
		(value) => {
			console.log(`success: `, value)
		},
		(reason) => {
			console.log(`error: `, reason)
		}
)

// 輸出 "success: nicklabs.cc"
// Promise {<fulfilled>}

執行reject函式會導致狀態改變 Pending => Rejected,同時將傳入值帶入then函式中的第二個回呼函式表示執行失敗。

new Promise((resolve, reject) => {
	reject(`nicklabs.cc`)
})
	.then(
		(value) => {
			console.log(`success: `, value)
		},
		(reason) => {
			console.log(`error: `, reason)
		}
)

// 輸出 "error: nicklabs.cc"
// Promise {<rejected>}

執行resolve函式後再執行reject函式後狀態會停留在fulfilled

new Promise((resolve, reject) => {
	resolve(`nicklabs.cc`);
	reject(`nicklabs.cc`);
})
	.then(
		(value) => {
			console.log(`success: `, value)
		},
		(reason) => {
			console.log(`error: `, reason)
		}
)

// 輸出 "success: nicklabs.cc"
// Promise {<fulfilled>}

可以再次將XMLHttpRequest封裝,這次是用Promise封裝

function request(url, data){
	return new Promise((resolve, reject) => {
		const xhr = new XMLHttpRequest();
		xhr.onreadystatechange = function(){
			// 當 readyState === 4 表示請求成功
			if(xhr.readyState !== 4){
				return;
			}
			if (xhr.status >= 200 && xhr.status < 300) {
				// 當成功時執行resolve並將ResponseText訊息傳入
				resolve(xhr.responseText);
			}else{
				// 執行reject將錯誤訊息傳入
				reject(xhr.status);
			}
		}
		xhr.open('POST', url, true);
		xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
		xhr.send(JSON.stringify(data));
	})
}

但如果只是這樣Promise還未解決回呼地獄 (Callback Hell) 的問題。

直接用範例來說明,程式碼如下:

new Promise((resolve, reject) => {
	resolve(`nicklabs.cc`)
})
	.then(
		(value) => {
			return value
		}
	)
	.then(
		(value) => {
			console.log(value)
		}
	)

//輸出 "nicklabs.cc"

從這個範例可以看到Promise是可以鏈式執行的,then函式返回的值也是一個Promise,可以讓回呼地獄變得更好掌控。

若上方程式碼第一時間無法理解,可以參考下方程式碼:

const p1 = new Promise((resolve, reject) => {
	resolve(`nicklabs.cc`)
})
const p2 = p1.then(
	(value) => {
		return value
	}
)
const p3 = p2.then(
	(value) => {
		console.log(value)
	}
)

//輸出 "nicklabs.cc"

Promise鏈式的執行避免了程式碼的層層嵌套,即使未來有多個Promise也只會讓程式碼往下新增而不是往右推擠,因此程式碼的可讀性及可維護性會大大提升。

在ECMA17中加入了兩個關鍵字 async、await是基於Promise上的語法糖可以讓非同步函式的操作變得更加簡單更好理解。

在非同步函式中我們可以不呼叫then函式而是使用await的語法,await會等待Promise完成後直接返回最終的結果。

首先將async關鍵字加到function上,將function標記為非同步函式,非同步函式的返回值就是一個Promise。

上面重新封裝了request並搭配使用語法糖再執行一次請求API A、B的例子:

async function run(){
	const resultA = await request(`xxxx.com/api/getA`);
	const resultB = await request(`xxxx.com/api/getB`, resultA);
}

run()

加上await關鍵字後看起來會像同步函式,在等待fetch完成返回的過程中Javascript依然可持續處理其他的程式邏輯,因為實際上還是使用Promise的機制在運作只是透過語法糖的包裝看起來像是同步函式。

作者頭像
Nick

是一位專業的網站開發者,不止擅長前端技術,也精通後端技術。