一篇文章帶你瞭解和使用Promise物件

Mr.M先生發表於2020-11-09

什麼是Promise

promise是解決非同步程式設計的一種方案,對比傳統的回撥函式更加的便捷和強大。

Promise的特點

  1. promise物件的狀態不受外界影響,共有三種狀態:pending(進行中)、fulfilled(已成功)、rejected(已失敗),且同一時間只能存在一種狀態
  2. 一旦狀態改變就不會被改變,且任何時候都可以得到這個結果;Promise物件的狀態改變,只有兩種可能:從pending變為fulfilled(後面簡稱resolved狀態)和從pending變為rejected(後面簡稱rejected狀態)。只要這兩種情況發生,狀態就凝固了,不會再變了。

Promise的基礎用法

Promise例項化

Promise實際是一個建構函式,用於生產promise例項

// promise一旦例項化就會立即執行
let promise = new Promise((resolve, reject) => {
	// 非同步函式執行成功時執行回撥,若resolve函式有帶引數,則該引數會傳遞給回撥函式
	resolve()
	// 非同步函式執行失敗時執行回撥,若reject函式有帶引數,則該引數會傳遞給回撥函式
	reject()
})

// 如果你希望這個promise是可控的
function controllPromise () {
	return new Promise(...)
}

示例
現在有一個非同步函式獲取資料,當該非同步函式執行完成後列印結果

let fn1 = function () {
	return new Promise((resolve, reject) => {
		// 定時器模擬非同步獲取資料
		setTimeout(() => {
			// 將獲取的結果傳入resolve函式中
			resolve('fn1')
		}, 1000)
	})
}
// promise狀態改變後會呼叫then方法繫結的函式,且該函式接收一個引數,該引數預設是undefined,或者是promise函式resolve、reject的值
fn1().then((res) => {
	console.log(res)
})
// 最終列印結果:fn1

resolve函式傳入的是promise

還有另外一種情況,若resolve或者reject函式傳入的引數是promise物件,那麼舊的promise函式物件的狀態由新的promise物件的狀態來決定,簡單的說就是,若resolve或者reject函式傳入的引數是promise物件需要等待該promise物件執行完成才會執行回撥。

let fn1 = function () {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve('fn1')
		}, 1000)
	})
}
let fn2 = function () {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve(fn1())
		}, 1000)
	})
}
fn2().then((res) => {
	console.log(res)
})
// 2s 後列印:fn1

Promise.prototype.then()

then方法是在Promise的原型上的,也就是說,then方法可以被所有Promise例項呼叫。then方法的第一個引數是resolved狀態的的回撥函式,第二個引數是rejected狀態的回撥函式(可選),而且then方法返回的是一個新的promise例項(不是原來的那個),所以then後面還可以繼續呼叫then方法等其他promise例項上的方法
示例

let fn1 = function () {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve('fn1')
		}, 1000)
	})
}
fn1().then((res) => {
	console.log(res)
}).then((res) => {
	console.log(res)
})
// 列印結果:fn1  undefined

then方法回撥函式的引數與返回值

我們從上面的示例可以知道,then方法回撥函式會接收一個變數res(自定義),這個變數實際就是前一個promise例項中resolve方法傳入的值,因此會列印 fn1,那麼為何後面會列印一個undefined呢,那是因為我們 then 方法沒有給返回值,第二個then方法的回撥函式接收的引數是上一個then方法回撥函式的返回值,我們來看示例
示例

let fn1 = function () {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve('fn1')
		}, 1000)
	})
}
fn1().then((res) => {
	// res 會接收上一個promise示例的resolve值
	console.log(res)
	return '我是第二個then方法回撥函式的引數'
}).then((res) => {
	// res 會接收上一個then方法的返回值
	console.log(res)
})
// 列印結果:fn1  我是第二個then方法回撥函式的引數

同樣的,如果回撥函式返回值是一個promise,那麼他會等待該promise物件執行完成在去執行下一個then方法的回撥函式
示例

let fn1 = function () {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve('fn1')
		}, 1000)
	})
}
let fn2 = function () {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve('fn2')
		}, 1000)
	})
}
fn1().then((res) => {
	console.log(res)
	return fn2()
}).then((res) => {
	console.log(res)
})
// 列印結果:fn1(1s後) fn2(1s後)

Promise.prototype.catch()

Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的別名,用於指定發生錯誤時的回撥函式。如果執行中丟擲錯誤,也會被catch()方法捕獲
示例

let fn1 = function () {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve('fn1')
		}, 1000)
	})
}

let fn2 = function () {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			reject('error test')
		}, 1000)
	})
}

// 接收reject丟擲的錯誤
fn1().then((res) => {
	console.log(res)
	return fn2()
}).then((res) => {
	console.log(res)
}).catch((err) => {
	console.log('error:' + err)
})
// 列印結果:
// fn1
// error:error test

// 接收常規執行丟擲的錯誤,如throw
fn1().then((res) => {
	console.log(res)
	throw('error normal')
}).then((res) => {
	console.log(res)
}).catch((err) => {
	console.log('error:' + err)
})
// 列印結果:
// fn1
// error:error normal

Promise 在resolve語句後面,再丟擲錯誤,不會被捕獲,等於沒有丟擲。因為 Promise 的狀態一旦改變,就永久保持該狀態,不會再變了。
示例

let fn1 = function () {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve('fn1')
			throw 'error'
		}, 1000)
	})
}
fn1().then((res) => {
	console.log(res)
}).catch((err) => {
	console.log(err)
})
// 列印結果
// fn1

Promise 物件的錯誤具有“冒泡”性質,會一直向後傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch語句捕獲。
示例

// 假設fn1, fn2, fn3是三個獲取資料的非同步函式
fn1().then(() => {
	return fn2()
}).then(() => {
	return fn3()
}).catch((err) => {
	console.log(err)
})
// 若fn1, fn2, fn3其中一個非同步函式丟擲錯誤都會被catch語句捕獲

跟傳統的try/catch程式碼塊不同的是,如果沒有使用catch()方法指定錯誤處理的回撥函式,Promise 物件丟擲的錯誤不會傳遞到外層程式碼,這就是說,Promise 內部的錯誤不會影響到 Promise 外部的程式碼,通俗的說法就是“Promise 會吃掉錯誤”。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行會報錯,因為x沒有宣告
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  console.log('everything is great');
});

setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123

在鏈式呼叫中可以存在多個catch,但一般來說會執行第一個catch到的錯誤,catch只會捕獲到前面丟擲的錯誤,無法捕獲到後面函式丟擲的錯誤,所以建議將catch放在鏈式呼叫的最後一步

Promise.all()

Promise.all()方法用於將多個promise例項,包裝組合成新的Promise例項,Promise.all()方法接受一個陣列作為引數,p1、p2、p3都是 Promise 例項,如果不是,就會先呼叫Promise.resolve方法,將引數轉為 Promise 例項,再進一步處理。另外,Promise.all()方法的引數可以不是陣列,但必須具有 Iterator 介面,且返回的每個成員都是 Promise 例項。

const all = Promise.all([p1,p2,p3])

p的狀態由p1、p2、p3決定,分成兩種情況。

(1)只有p1、p2、p3的狀態都變成fulfilled,all的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個陣列,傳遞給p的回撥函式。

(2)只要p1、p2、p3之中有一個被rejected,all的狀態就變成rejected,此時第一個被reject的例項的返回值,會傳遞給p的回撥函式。

簡單來說就是,當p1、p2、p3全部成功的時候執行回撥,並返回陣列,若有失敗返回值則優先返回最先丟擲錯誤那個
示例

function p1() {
   return 1
 }

 function p2() {
   return new Promise((res, rej) => {
     setTimeout(() => {
       res('2')
     }, 4000)
   })
 }

 function p3() {
   return new Promise((res, rej) => {
     setTimeout(() => {
       res('3')
     }, 1000)
   })
 }

 function test_err() {
   return new Promise((res, rej) => {
     setTimeout(() => {
       rej('error')
     }, 3000)
   })
 }
 // 全部成功,返回一個陣列,陣列包含了返回的結果,且順序同傳入promise時的例項順序相同
 Promise.all([
   p1(),
   p2(),
   p3()
 ]).then((res) => {
   console.log(res)
 })
// 列印結果
// [1, "2", "3"]

Promise.all([
   p1(),
   p2(),
   p3(),
   test_err()
 ]).catch((res) => {
    console.log(res)
 })
 // 列印結果
 // error

Promise.race

Promise.race()方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。

const race = Promise.race([p1, p2, p3])

但和Promise.all()方法不同的是,當p1, p2, p3中有一個率先改變狀態,那麼race的狀態就會跟著改變,那個率先改變的Promise示例的返回值就會傳遞給race的回撥函式

我們用一個超時機制的例子來說明一下

// 假設fn1是獲取資料的函式,獲取資料所用時間未知當獲取時間超過3s我們就判斷它超時了
function fn1() {
	...
}
function overtime() {
	return new Promise((res ,rej) => {
		setTimeout(() => {
			res('overtime')
		}, 3000)
	})
}
const race = new Promise.race([fn1(), overtime()])
// 若超過3秒,fn1還未獲取到資料,就會返回overtime

Promise.resolve()

Promise.resolve()方法可以將一個現有的變數轉換為Promise物件

const jsp = Promise.resolve('1')

// 等價於
function jsp = new Promise((res ,rej) => {
	res('1')
})

resolve方法接收的引數可以分為4中情況

  • 引數是一個Promise例項

    如果引數是 Promise 例項,那麼Promise.resolve將不做任何修改、原封不動地返回這個例項。

  • 引數是一個thenable物件

    thenable物件指的是具有then方法的物件,比如下面這個物件。

    let thenable = {
    then: function(resolve, reject) {
    	resolve(42);
    }
    

    Promise.resolve()方法會將這個物件轉為 Promise 物件,然後就立即執行thenable物件的then()方法。

  • 引數不是具有then()方法的物件,或根本就不是物件

    如果引數是一個原始值,或者是一個不具有then()方法的物件,則Promise.resolve()方法返回一個新的 Promise 物件,狀態為resolved。

  • 不帶有任何引數

    Promise.resolve()方法允許呼叫時不帶引數,直接返回一個resolved狀態的 Promise 物件。

如果希望得到一個 Promise 物件,比較方便的方法就是直接呼叫Promise.resolve()方法。

Promise.reject()

Promise.reject(reason)方法也會返回一個新的 Promise 例項,該例項的狀態為rejected。

const p = Promise.reject('出錯了');
// 等同於
const p = new Promise((resolve, reject) => reject('出錯了'))

p.then(null, function (s) {
  console.log(s)
});
// 出錯了

上面程式碼生成一個 Promise 物件的例項p,狀態為rejected,回撥函式會立即執行。

Promise.reject()方法的引數,會原封不動地作為reject的理由,變成後續方法的引數。

Promise.reject('出錯了')
.catch(e => {
  console.log(e === '出錯了')
})
// true

上面程式碼中,Promise.reject()方法的引數是一個字串,後面catch()方法的引數e就是這個字串。

本文借鑑的阮一峰 promise入門,這篇文章講的很詳細,有興趣可以看看

相關文章