我是要成為海賊王的男人
悟空已成神,鳴人已成影,待路飛成王之時,便是我青春結束時!
悟空陪布瑪找尋龍珠,一路拳打比克、斬弗利薩,生個兒子戰沙魯,最後淨化布歐,只因承諾要保護地球。鳴人“有話直說,說到做到,這就是我的忍道”,一句會把佐助帶回來的承諾,斷臂踐行。路飛要湊齊10個船員,成為海賊王,我們相信路飛一定會成王,因為我們相信他的承諾。
我為什麼說承諾呢,今天主題不是Promise
嗎,因為⬇️
回撥地獄 Callback Hell
如果看這篇文章的你是有過專案經驗的,應該都遭遇過這慘絕人寰的“回撥地獄”。“回撥地獄”並不是JS或者程式語言中的一種形式,只是大家把這種程式設計中遇到的現象、問題預定俗稱的調侃成“回撥地獄”。因為只要陷進去,就很難出來。並且回撥地獄在程式碼層級上會越陷越深,邏輯看著會非常會亂,如下程式碼⬇️
// 我們用setTimeout模擬線上傳送請求等非同步執行的函式
setTimeout(() => {
console.log(1);
setTimeout(() => {
console.log(2);
setTimeout(() => {
console.log(3);
}, 1000)
}, 1000)
}, 1000);
複製程式碼
這是三個回撥函式巢狀,延遲一秒後輸出1,再過一秒輸出2,再過一秒輸出3。當然現實專案中,每個函式裡面處理的邏輯肯定不僅僅只是輸入一個數字這麼簡單,當我們回撥巢狀很多的時候,如果產品提出的一個需求我們需要更改執行順序,這個時候我們會發現巢狀邏輯複雜到難以簡單的更改順序,嚴重的只能重新寫這段的邏輯程式碼。並且回撥函式讓邏輯很不清晰。
後來就有人提出了Promise
概念,這個概念意在讓非同步程式碼變得非常乾淨和直觀。
Promise 這就是我的忍道
這個概念並不是ES2015首創的,在ES2015標準釋出之前,早已有
Promise/A
和Promise/A+
等概念的出現,ES2015中的Promise
標準便源自於Promise/A+
。Promise
最大的目的在於可以讓非同步函式變得竟然有序,就如我們需要在瀏覽器中訪問一個JSON座位返回格式的第三方API,在資料下載完成後進行JSON解碼,通過Promise
來包裝非同步流程可以使程式碼變得非常乾淨。———————摘自《實戰ES2015》
上面最重要的一句就是可以讓非同步函式變得竟然有序,可能有人會說await
和async
也可以讓非同步函式同步執行,但是await
操作符本來就是用於等待一個Promise物件的。
我們先來看一下Promise
是怎麼解決上面回撥地獄這樣的難題的⬇️
// 封裝一層函式
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000)
})
}
// 按回撥函式的邏輯執行
timeout().then(() => {
console.log(1);
return timeout()
}).then(() => {
console.log(2);
return timeout()
}).then(() => {
console.log(3);
});
複製程式碼
我們按照回撥函式的邏輯用Promise
重新寫了一遍,執行結果一樣,我們可以看出來,相比回撥函式的層級深入,使用Promise
以後函式的層級明顯減少了,邏輯清晰許多。
下面我們來從頭開始認識Promise
Promise基礎
想要給一個函式賦予Promise
的能力,就要先建立一個Promise
物件,並將其作為函式值返回。Promise
建構函式要求傳入一個函式,並帶有resovle
和reject
引數。一個成功回撥函式,一個失敗成功回撥函式。下面是Promise
物件的三個狀態:
- pending: 初始狀態,既不是成功,也不是失敗狀態。
- fulfilled: 意味著操作成功完成。
- rejected: 意味著操作失敗。
三個狀態的轉換關係是從pending -> fulfilled
或者pending -> rejected
,並且狀態改變以後就不會再變了。pending -> fulfilled
以後會去執行傳入Promise
物件的resovle
函式,對應的,pending -> rejected
以後會去執行傳入Promise
物件的reject
函式。
.then()
那resovle
函式和reject
函式是怎麼傳進去的呢,當然就是之前說的.then()
,.then()
可以接收兩個引數,.then(onFulfilled[, onRejected])
這是官方寫法,其實就是.then(resovle, reject)
,第一個引數是成功回撥,第二個引數就是失敗回撥。如下⬇️
function timeout(isSuccess) {
return new Promise((resolve, reject) => {
if (isSuccess) {
setTimeout(resolve, 1000)
} else {
reject()
}
})
}
timeout(true).then(() => {
console.log('成功')
}, () => {
console.log('失敗')
});
timeout(false).then(() => {
console.log('成功')
}, () => {
console.log('失敗')
});
複製程式碼
我用if語句模擬一下成功和失敗的場景,這就是.then()
的用法。
.catch()
剛才說了.then()
的第二個引數傳進去的是一個失敗回撥的函式,但是Promise
還有一個.catch()
的方法,也是用來處理失敗的,例子如下⬇️:
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000)
})
}
timeout().then(() => {
throw new Error('因為被凱多打敗了,所以沒當上海賊王')
}).catch((err) => {
console.log('失敗原因:', err)
});
複製程式碼
這時候也會輸出錯誤資訊。這時候你可能會問,那.then(resovle, reject)
的reject
和.catch(reject)
有什麼區別呢,下面是個人見解
.then(resovle, reject)
的reject
和.catch(reject)
有什麼區別
我個人認為,.then(resovle, reject)
的reject
按就近原則,只對最近的這個非同步函式進行錯誤處理,但是對以後的或者之前的非同步函式不做處理,而.catch(reject)
會捕獲到全域性所有鏈式上非同步函式的錯誤。鏈式呼叫下面會講到。總之就是.catch(reject)
管的範圍要大一些。
鏈式呼叫
Promise
有一個物件鏈,並且這個物件鏈式呈流水線的模式進行作業,是因為在Promise
物件對自身的onFulfilled
和onRejected
相應器的處理中,會對其中返回的Promise
物件進行處理。其中內部會將這個新的Promise
物件加入到Promise
物件鏈中,並將其暴露出來,使其繼續接受新的Promise
物件的加入。只有當Promise
物件鏈中的上一個Promise
物件進入成功或者失敗階段,下一個Promise
物件菜戶被啟用,這就形成了流水線的作業模式。
這就好比一開始使用Promise
改造回撥地獄函式時候的樣子⬇️
// 封裝一層函式
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000)
})
}
// 按回撥函式的邏輯執行
timeout().then(() => {
console.log(1);
return timeout()
}).then(() => {
console.log(2);
return timeout()
}).then(() => {
console.log(3);
});
複製程式碼
可以一層一層的傳一下去,這也是厲害的地方。當鏈式呼叫中用.catch()
捕獲錯誤的時候是這樣的⬇️
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000)
})
}
timeout()
.then(() => {
console.log(1);
return timeout(err)
})
.then(() => {
throw new Error('發生錯誤了')
return timeout(2)
})
.catch((err) => {
console.log('123',err)
})
.then(() => {
console.log(3);
});
複製程式碼
這種情況,.catch()
緊跟在丟擲錯誤的一步函式後面,會丟擲錯誤,然後繼續往下執行,但是如果.catch()
是在最後,結果就完全不一樣了⬇️
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000)
})
}
timeout()
.then(() => {
console.log(1);
return timeout(err)
})
.then(() => {
throw new Error('發生錯誤了')
return timeout(2)
})
.then(() => {
console.log(3);
})
.catch((err) => {
console.log('123',err)
});
複製程式碼
如果是這樣,前面說了.catch()
會捕獲全域性錯誤,但是,.catch()
寫在最後,丟擲錯誤以後,函式會直接跳到.catch()
然後繼續往下執行,就像下面程式碼⬇️
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000)
})
}
timeout()
.then(() => {
console.log(1);
return timeout()
})
.then(() => {
console.log(11);
throw new Error('發生錯誤了')
return timeout()
})
.then(() => {
return timeout(2)
})
.catch((err) => {
console.log('2',err)
})
.then(() => {
throw new Error('發生錯誤了2')
console.log(3);
})
.catch((err) => {
console.log('3',err)
});
複製程式碼
上面這段程式碼就會直接跳過輸出2的非同步函式,直接走到第一個.catch()
,然後再往下執行。
Promise高階
Promise.all()
這個方法真的太實用了,比如你進入首頁,需要同時請求各種分類,使用者資訊等等資訊,我們們可能需要在所有的請求都回來以後再展示頁面,因為我們不能確定每個請求都要多久才能請求回來,所以這個問題一度很難解決。現在有了Promise.all()
這個方法,真的太方便了,下面就是例子⬇️
// Promise.all()需要傳入的就是一個陣列,每一項就是每一個非同步函式
function timeout(delay) {
return new Promise((resolve, reject) => {
setTimeout(resolve, delay * 1000)
})
}
Promise.all([
timeout(1),
timeout(3),
timeout(5),
]).then(() => {
console.log('都請求完畢了!')
});
複製程式碼
上面程式碼會在最大延遲的5秒後然後在執行.then()
的方法,當然還有一個差不多的函式,往下看
Promise.race()
Promise.race()
會監聽所有的Promise
物件,在等待其中的第一個進入完成狀態的Promise
物件。一旦有第一個Promise
物件進入了完成狀態,該方法返回的Promise
物件便會根據這第一個完成的Promise
物件的狀態而改變,如下⬇️
function timeout(delay) {
return new Promise((resolve, reject) => {
setTimeout(resolve, delay * 1000)
})
}
Promise.race([
timeout(1),
timeout(3),
timeout(5),
]).then(() => {
console.log('有一個請求已經結束!')
});
複製程式碼
上面程式碼在執行1秒後就會執行.then()
的方法,然後剩下的兩個請求繼續等待返回。
反正我也沒遇到過什麼使用場景,知道有這個方法就行了
只管把目標定在高峰,人家要笑就讓他去笑!
寫到後面有點太官方的感覺,但是又覺得很不好解釋,只能堆例子來解釋了,跟大佬的差距還是有一定的差距,這只是基於我現在的水平到目前為止對Promise
的理解。
一句承諾,就要努力去兌現。自己選擇的路,跪著也要走完。
我是前端戰五渣,一個前端界的小學生。