前言
這篇文章我們一起來學習如何使用Promise
,以及如何實現一個自己的Promise
,講解非常清楚,全程一步一步往後實現,附帶詳細註釋與原理講解。
如果你覺的這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖 第一時間獲取最新的文章,回覆進群,拉你進入前端開發交流群,回覆資料,獲取海量前端電子書及前端學習影片~
promise是什麼?主要用來解決什麼問題?
Promise是非同步程式設計的一種解決方案,比傳統解決方案--回撥函式和事件--更合理更強大。
Promise
特點:
(1)物件的狀態不受外界影響。Promise
物件代表一個非同步操作,有三種狀態:pending
(進行中),fulfilled
(已成功)和reject
(已失敗)。只有非同步操作的結果,可以決定當前是哪一種狀態,任何其它操作都無法改變這個狀態。這也是Promise
(承諾)這個名字的由來。
(2)一旦狀態改變,就不會再變,任何時候都可以得到這個結果。Promise
物件的狀態改變,只有兩種可能:從pending
變為fulfilled
和從pending
變為rejected
。
promise主要用來解決:
- 回撥地獄
- 併發請求
- 非同步方案最佳化(但它本身不是非同步的,new Promise()後,它會立即執行)
promise基本用法
ES6規定,Promise
物件是一個建構函式,用來生成Promise
例項
下面程式碼創造了一個Promise
例項
const promise = new Promise(function(resolve,reject){
//...
if(/*非同步操作成功*/){
resolve(value)
}else{
//非同步操作失敗
reject(error)
}
})
Promise
建構函式接受一個函式作為引數,該函式的兩個引數分別是resolve
和reject
.他們是兩個函式,由JavaScript引擎提供,不用自己部署。
resolve
函式的作用是,將Promise
物件的狀態從“未完成”變成“成功”(即從pending變為resolve),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;reject
函式的作用是,將Promise
物件的狀態從“未完成”變成“失敗”(即從pending變為rejected),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
Promise
例項生成以後,可以用then
方法分別指定resolved
狀態和rejected
狀態的回撥函式,或用catch
方法指定rejected
狀態的回撥函式。
promise.then(res=>{
//success
},error=>{
//error
}).catch(err=>{})
then
方法可以接受兩個回撥函式作為引數,第一個回撥函式是Promise
物件的狀態變為resolved
時呼叫,第二個回撥函式是Promise
物件的狀態變為rejected
時呼叫。其中,第二個函式是可選的,不一定要提供。這兩個函式都接收Promise
物件傳出的值作為引數。
Ok,透過上面對promise基本用法的描述,我們大概知道了一個promise類裡面都應該包含哪些內容了:
- promise狀態:pending,fulfilled,rejected
- promise返回值
- 執行器:promise執行入口(也就是你傳入的那個函式)
- resolve:改變promise狀態為fulfilled
- reject:改變promise狀態為rejected
- then:接收兩個回撥,onFulfilled, onRejected。分別在promise狀態變為fulfiled或rejected後執行
- catch:接受一個回撥,在promise狀態變為rejected後執行
簡單實現一個promise
我們知道了一個promise內容至少包含以上那些內容,所以一個簡單的promise內部至少是這樣的
class myPromise {
static PENDING = 'pending'
static FULFILLEd = 'fulfilled'
static REJECTED = 'rejected'
constructor(init){
this.state = myPromise.PENDING // promise狀態
this.promiseRes = null // promise返回值
const resolve = result=>{
//...
}
const reject = result=>{
//...
}
try{
init(resolve,reject) // init就是初始化執行器
}catch(err){
reject(err)
}
}
then(onFulfilled,onRejected){
//...
}
catch(onRejected){
//...
}
}
OK,大概瞭解之後,我們再來一個一個的看裡面每個部分的實現以及作用
Promise的執行器
它其實是我們在new Promise
時傳入的一個回撥函式,這個函式本身是同步的,也就是說在new Promise
時它就會執行,這也是我們操作promise的入口。
class myPromise{
//...
constructor(init){
try{
init(resolve,reject) // init就是初始化執行器
}catch(err){
reject(err) //這裡主要是在init執行器函式出錯時,用以讓promise狀態變為rejected
}
}
//...
}
該函式接受兩個回撥函式(resolve,reject)作為引數,用以改變Promise的狀態
resolve與reject方法
這兩個函式作為引數傳到執行器函式中,用以後續改變Promise狀態
class myPromise {
static PENDING = 'pending'
static FULFILLEd = 'fulfilled'
static REJECTED = 'rejected'
constructor(init){
this.state = myPromise.PENDING // promise狀態
this.promiseRes = null // promise返回值
this.resolveCallback = [] //成功回撥集合
this.rejectCallback = [] //失敗回撥集合
const resolve = result=>{
// 只有當狀態為pending時才改變,保證狀態一旦改變就不會再變
if(this.state === myPromise.PENDING){
this.state = myPromise.FULFILLEd //改變狀態
this.promiseRes = result //返回值
//依次呼叫成功回撥
this.resolveCallback.forEach(fn=>fn())
}
}
const reject = result=>{
// 只有當狀態為pending時才改變,保證狀態一旦改變就不會再變
if(this.state === myPromise.PENDING){
this.state = myPromise.REJECTED //改變狀態
this.promiseRes = result //返回值
// 依次呼叫失敗回撥
this.rejectCallback.forEach(fn=>fn())
}
}
try{
init(resolve,reject) // 注意this指向
}catch(err){
reject(err)
}
}
}
初步then方法
class myPromise {
static PENDING = 'pending'
static FULFILLEd = 'fulfilled'
static REJECTED = 'rejected'
constructor(init){
this.state = myPromise.PENDING // promise狀態
this.promiseRes = null // promise返回值
this.resolveCallback = [] //成功回撥集合
this.rejectCallback = [] //失敗回撥集合
const resolve = result=>{
// 只有當狀態為pending時才改變,保證狀態一旦改變就不會再變
if(this.state === myPromise.PENDING){
this.state = myPromise.FULFILLEd //改變狀態
this.promiseRes = result //返回值
//依次呼叫成功回撥
this.resolveCallback.forEach(fn=>fn())
}
}
const reject = result=>{
// 只有當狀態為pending時才改變,保證狀態一旦改變就不會再變
if(this.state === myPromise.PENDING){
this.state = myPromise.REJECTED //改變狀態
this.promiseRes = result //返回值
// 依次呼叫失敗回撥
this.rejectCallback.forEach(fn=>fn())
}
}
try{
init(resolve,reject) // 注意this指向
}catch(err){
reject(err)
}
}
then(onFulfilled,onRejected){
if(this.state === myPromise.FULFILLEd && typeof onFulfilled === 'function') {
onFulfilled(this.promiseRes)
}
if(this.state === myPromise.REJECTED && typeof onRejected === 'function') {
onRejected(this.promiseRes)
}
}
}
寫到這裡,我們的promise已經初步成型了,我們可以來測試一下:
const res1 = new myPromise((res,rej)=>{
res('成功啦~')
rej('失敗啦~')
})
res1.then((res)=>{
console.log(res)
},err=>{
console.log(err)
})
// 按照預期,這裡應該是隻會列印出成功啦~
從上圖看我們,是不是符合我們的預期,並且myPromise
內部與原生的Promise
也是非常相似的。你們是不是覺得這裡已經沒問題了,上面我們只是測了一下同步方法的執行,但別忘了,Promise主要是來解決非同步問題的,我們再來試一下里面執行非同步方法還符不符合我們的預期?
const res1 = new myPromise((res,rej)=>{
setTimeout(()=>res('成功啦~'),1000)
// rej('失敗啦~')
})
res1.then((res)=>{
console.log(res)
})
這裡我們預期本來是一秒之後列印成功啦,但它並沒有如我們所願,反而是什麼也沒列印出來,這是因為在setTimeout執行之前(pen ding)這個then方法已經執行過了,1s後狀態變成fulfilled時,then也不會再執行了。
所以我們需要保證then方法的回撥函式在promise狀態變成fulfilled
或rejected
時再執行,那麼當promise狀態為pending
時我們先要把回撥存在對應的佇列中,等後續狀態改變後再執行
較完整then方法
OK,這裡我們修改一下我們的then方法,讓其保證非同步程式碼執行的正確性 (具體實現微任務我們可以用 mutationObserver,這裡我們就用setTimeout來模擬一下)
class myPromise {
static PENDING = 'pending'
static FULFILLEd = 'fulfilled'
static REJECTED = 'rejected'
constructor(init){
this.state = myPromise.PENDING // promise狀態
this.promiseRes = null // promise返回值
this.resolveCallback = [] //成功回撥集合
this.rejectCallback = [] //失敗回撥集合
const resolve = result=>{
// 只有當狀態為pending時才改變,保證狀態一旦改變就不會再變
if(this.state === myPromise.PENDING){
this.state = myPromise.FULFILLEd //改變狀態
this.promiseRes = result //返回值
//依次呼叫成功回撥
this.resolveCallback.forEach(fn=>fn())
}
}
const reject = result=>{
// 只有當狀態為pending時才改變,保證狀態一旦改變就不會再變
if(this.state === myPromise.PENDING){
this.state = myPromise.REJECTED //改變狀態
this.promiseRes = result //返回值
// 依次呼叫失敗回撥
this.rejectCallback.forEach(fn=>fn())
}
}
try{
init(resolve,reject) // 注意this指向
}catch(err){
reject(err)
}
}
then(onFulfilled,onRejected){
if(this.state === myPromise.FULFILLEd && typeof onFulfilled === 'function') {
onFulfilled(this.promiseRes)
}
if(this.state === myPromise.REJECTED && typeof onRejected === 'function') {
onRejected(this.promiseRes)
}
if(this.state === myPromise.PENDING){
if(onFulfilled && typeof onFulfilled === 'function'){
this.resolveCallback.push(()=>
// 這裡我們用setTimeout來模擬實現then的微任務
setTimeout(()=>{
onFulfilled(this.promiseRes)
},0)
)
}
if(onRejected && typeof onRejected === 'function'){
this.rejectCallback.push(()=>
// 這裡我們用setTimeout來模擬實現then的微任務
setTimeout(()=>{
onRejected(this.promiseRes)
},0)
)
}
}
}
}
這裡我們可以再測試一下上面那個非同步函式的測試用例,發現它能夠正確列印,OK,一個較完整的then方法就算實現了~
then的鏈式呼叫
then
方法會返回一個新的Promise
(⚠️注意:不是原來的那個Promise
)所以可以採用鏈式呼叫
採用鏈式的then
,可以指定一組按照次序呼叫的回撥函式。這時,前一個回撥函式,有可能返回的還是一個Promise
物件(即有非同步操作),這時後一個回撥函式,就會等待該Promise
物件的狀態發生變化,才會被呼叫。
then(onFulfilled,onRejected){
const {promiseRes,state} = this
let promise = new myPromise((reso,reje)=>{
const resolveMyPromise = promiseRes => {
try{
if(typeof onFulfilled !== 'function'){
// 如果then的第一個回撥不是一個函式,直接忽略,返回一個新的promise
reso(promiseRes)
}else{
// 獲取第一個回撥的執行結果
const res = onFulfilled(promiseRes)
// 看該執行結果是否是一個promise
if(res instanceof myPromise){
// 是一個promise,等它狀態改變後再改變then返回的promise狀態
res.then(reso,rej)
}else{
// 不是一個promise,將它作為新的promise的resolve
reso(res)
}
}
}catch(err){
//異常,直接將新的promise狀態置為rejected
reje(err)
}
}
const rejectMyPromise = promiseRes => {
try{
if(typeof onRejected !== 'function'){
// 如果then的第二個回撥不是一個函式,直接忽略,返回一個新的promise
reje(promiseRes)
}else{
// 獲取第二個回撥的執行結果
const res = onRejected(promiseRes)
// 看該執行結果是否是一個promise
if(res instanceof myPromise){
// 是一個promise,等它狀態改變後再改變then返回的promise狀態
res.then(reso,rej)
}else{
// 不是一個promise,將它作為新的promise的resolve
reje(res)
}
}
}catch(err){
//異常,直接將新的promise狀態置為rejected
reje(err)
}
}
if(state === myPromise.FULFILLEd) {
resolveMyPromise(promiseRes)
}
if(state === myPromise.REJECTED) {
rejectMyPromise(promiseRes)
}
if(state === myPromise.PENDING){
if(onFulfilled && typeof onFulfilled === 'function'){
this.resolveCallback.push(()=>
// 這裡我們用setTimeout來模擬實現then的微任務
setTimeout(()=>{
resolveMyPromise(this.promiseRes)
},0)
)
}
if(onRejected && typeof onRejected === 'function'){
this.rejectCallback.push(()=>
// 這裡我們用setTimeout來模擬實現then的微任務
setTimeout(()=>{
rejectMyPromise(this.promiseRes)
},0)
)
}
}
})
return promise
}
catch方法
我們知道then的第二個回撥其實與catch方法是一樣的,所以catch方法我們可以這樣實現
catch(onRejected) {
return this.then(undefined,onRejected)
}
Promise.resolve
將物件轉為一個promise物件,根據引數不通可分為四種情況
- 引數是一個Promise例項,直接返回該例項
- 引數是一個
thenable
物件,將該物件轉為Promise物件後,執行該物件的then
方法 - 沒有引數,也是返回一個狀態為
resolved
的新的Promise物件 - 引數是一個一個原始值,返回一個新的Promise物件,狀態為
resolved
手動實現:
static resolve(v){
//1.引數是一個Promise例項,直接返回
if(v instanceof myPromise){
return v
}
//2.引數是一個thenable物件,轉為Promise後執行該物件的then方法
if(typeof v === 'object' && typeof v.then === 'function'){
return new myPromise((res,rej)=>{
v.then(res,rej)
})
}
//3.沒有引數,直接返回一個resolved狀態的promise
if(!v){
return new myPromise(res=>{
res()
})
}
//4.引數是一個原始值,返回一個新的Promise,狀態為resolved
return new myPromise(res=>{
res(v)
})
}
Promise.reject
返回一個新的Promise物件,狀態為rejected
static reject(v){
return new myPromise((res,rej)=>{
rej(v)
})
}
Promise.all
該方法用於將多個Promise例項包裝成一個新的Promise例項,如果有不是Promise的項,則讓該項直接成功
用法:
const p = Promise.all([p1,p2,p3])
p
的狀態由p1
、p2
、p3
決定,分成兩種情況。
(1)只有p1
、p2
、p3
的狀態都變成fulfilled
,p
的狀態才會變成fulfilled
,此時p1
、p2
、p3
的返回值組成一個陣列,傳遞給p
的回撥函式。
(2)只要p1
、p2
、p3
之中有一個被rejected
,p
的狀態就變成rejected
,此時第一個被reject
的例項的返回值,會傳遞給p
的回撥函式。
Ok,瞭解完Promise.all
我們動手來實現一遍
手動實現:
static all (promises){
return new myPromise((res,rej)=>{
let count = 0
const result = [];
function addFun(index,resf) {
result[index]=resf // 這裡用索引別用push,保證返回的順序
count++
if(count==promises.length) {
res(result)
}
}
[].forEach.call(promises,(promise,index)=>{
if(promise instanceof myPromise) {
promise.then(success=>{
// count ++
// result.push(success)
addFun(index,success)
},err=>{
rej(err)
})
}else{
addFun(index,promise)
}
})
})
}
Promise.race
Promise.race()
方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。
用法:
const p = Promise.race([p1, p2, p3]);
上面程式碼中,只要p1
、p2
、p3
之中有一個例項率先改變狀態,p
的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給p
的回撥函式。
手動實現:
static race(promises) {
return new myPromise((res,rej)=>{
[].forEach.call(promises,promise=>{
if(promise instanceof myPromise){
promise.then(success=>{
res(success)
},error=>{
rej(error)
})
}else{
res(promise)
}
})
})
}
完整程式碼
class myPromise {
static PENDING = 'pending'
static FULFILLEd = 'fulfilled'
static REJECTED = 'rejected'
constructor(init){
this.state = myPromise.PENDING // promise狀態
this.promiseRes = null // promise返回值
this.resolveCallback = [] //成功回撥集合
this.rejectCallback = [] //失敗回撥集合
const resolve = result=>{
// 只有當狀態為pending時才改變,保證狀態一旦改變就不會再變
if(this.state === myPromise.PENDING){
this.state = myPromise.FULFILLEd //改變狀態
this.promiseRes = result //返回值
//依次呼叫成功回撥
this.resolveCallback.forEach(fn=>fn())
}
}
const reject = result=>{
// 只有當狀態為pending時才改變,保證狀態一旦改變就不會再變
if(this.state === myPromise.PENDING){
this.state = myPromise.REJECTED //改變狀態
this.promiseRes = result //返回值
// 依次呼叫失敗回撥
this.rejectCallback.forEach(fn=>fn())
}
}
try{
init(resolve,reject) // 注意this指向
}catch(err){
reject(err)
}
}
then(onFulfilled,onRejected){
const {promiseRes,state} = this
let promise = new myPromise((reso,reje)=>{
const resolveMyPromise = promiseRes => {
try{
if(typeof onFulfilled !== 'function'){
// 如果then的第一個回撥不是一個函式,直接忽略,返回一個新的promise
reso(promiseRes)
}else{
// 獲取第一個回撥的執行結果
const res = onFulfilled(promiseRes)
// 看該執行結果是否是一個promise
if(res instanceof myPromise){
// 是一個promise,等它狀態改變後再改變then返回的promise狀態
res.then(reso,rej)
}else{
// 不是一個promise,將它作為新的promise的resolve
reso(res)
}
}
}catch(err){
//異常,直接將新的promise狀態置為rejected
reje(err)
}
}
const rejectMyPromise = promiseRes => {
try{
if(typeof onRejected !== 'function'){
// 如果then的第二個回撥不是一個函式,直接忽略,返回一個新的promise
reje(promiseRes)
}else{
// 獲取第二個回撥的執行結果
const res = onRejected(promiseRes)
// 看該執行結果是否是一個promise
if(res instanceof myPromise){
// 是一個promise,等它狀態改變後再改變then返回的promise狀態
res.then(reso,rej)
}else{
// 不是一個promise,將它作為新的promise的resolve
reje(res)
}
}
}catch(err){
//異常,直接將新的promise狀態置為rejected
reje(err)
}
}
if(state === myPromise.FULFILLEd) {
resolveMyPromise(promiseRes)
}
if(state === myPromise.REJECTED) {
rejectMyPromise(promiseRes)
}
if(state === myPromise.PENDING){
if(onFulfilled && typeof onFulfilled === 'function'){
this.resolveCallback.push(()=>
// 這裡我們用setTimeout來模擬實現then的微任務
setTimeout(()=>{
resolveMyPromise(this.promiseRes)
},0)
)
}
if(onRejected && typeof onRejected === 'function'){
this.rejectCallback.push(()=>
// 這裡我們用setTimeout來模擬實現then的微任務
setTimeout(()=>{
rejectMyPromise(this.promiseRes)
},0)
)
}
}
})
return promise
}
catch(onRejected) {
return this.then(undefined,onRejected)
}
static all (promises){
return new myPromise((res,rej)=>{
let count = 0
const result = [];
function addFun(index,resf) {
result[index]=resf // 這裡用索引別用push,保證返回的順序
count++
if(count==promises.length) {
res(result)
}
}
[].forEach.call(promises,(promise,index)=>{
if(promise instanceof myPromise) {
promise.then(success=>{
addFun(index,success)
},err=>{
rej(err)
})
}else{
addFun(index,promise)
}
})
})
}
static race(promises) {
return new myPromise((res,rej)=>{
[].forEach.call(promises,promise=>{
if(promise instanceof myPromise){
promise.then(success=>{
res(success)
},error=>{
rej(error)
})
}else{
res(promise)
}
})
})
}
static resolve(v){
//1.引數是一個Promise例項,直接返回
if(v instanceof myPromise){
return v
}
//2.引數是一個thenable物件,轉為Promise後執行該物件的then方法
if(typeof v === 'object' && typeof v.then === 'function'){
return new myPromise((res,rej)=>{
v.then(res,rej)
})
}
//3.沒有引數,直接返回一個resolved狀態的promise
if(!v){
return new myPromise(res=>{
res()
})
}
//4.引數是一個原始值,返回一個新的Promise,狀態為resolved
return new myPromise(res=>{
res(v)
})
}
static reject(v){
return new myPromise((res,rej)=>{
rej(v)
})
}
}
推薦閱讀
介紹迴流與重繪(Reflow & Repaint),以及如何進行最佳化?
這些瀏覽器面試題,看看你能回答幾個?
這一次帶你徹底瞭解前端本地儲存
面試官:說一說前端路由與後端路由的區別
JavaScript之原型與原型鏈
Javascript深入之作用域與閉包
this指向與call,apply,bind
總結
OK,上面跟大家一起過了一遍Promise
的用法以及自己動手實現了一遍Promise
,想必看完這篇文章,大家對Promise
會有一個更加清晰的認識。
我是南玖
,感謝各位的:「點贊和關注」,我們下期見!