前言
雖然今年已經(jīng)18年窄潭,但是今天還是要繼續(xù)聊聊ES6的東西,ES6已經(jīng)過去幾年酵颁,可是我們對于ES6的語法究竟是掌握了什么程度嫉你,是了解?會用躏惋?還是精通幽污?相信大家和我一樣都對自己有著一個提升的心,對于新玩具可不能僅僅了解簿姨,對于其中的思想才是最吸引人的距误,所以接下來會通過一篇文章,來讓大家對于Promise
這個玩具做到精通的程度1馕弧W继丁!
此處打開一瓶冰闊落~~~
Promise
Promise
是異步編程的一種解決方案域仇,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件——更合理和更強(qiáng)大刑然。它由社區(qū)最早提出和實現(xiàn),ES6將其寫進(jìn)了語言標(biāo)準(zhǔn)暇务,統(tǒng)一了用法泼掠,原生提供了Promise
對象。
嗝~~~~~
首先垦细,我們通過字面可以看出來Pormise
是一種解決方案择镇,而且還有兩種傳統(tǒng)的解決方案·回調(diào)函數(shù)
和事件
,ok蝠检,那么我們就來先聊聊這兩種方案沐鼠。
回調(diào)函數(shù) Callback
回調(diào)函數(shù)想必大家都不陌生,就是我們常見的把一個函數(shù)當(dāng)做參數(shù)傳遞給另外一個函數(shù)叹谁,在滿足了一定的條件之后再去執(zhí)行回調(diào),比如我們想要實現(xiàn)一個在三秒后去計算1到5的和乘盖,那么:
// 求和函數(shù)
function sum () {
return eval([...arguments].join('+'))
}
// 三秒后執(zhí)行函數(shù)
function asycnGetSum (callback) {
setTimeout(function(){
var result = callback(1,2,3,4,5);
console.log(result)
},3000)
}
asyncGetSum(sum);
這樣的實現(xiàn)就是回調(diào)函數(shù)焰檩,但是如果我要實現(xiàn)在一段動畫,動畫的執(zhí)行過程是小球先向右移動100px订框,然后再向下移動100px析苫,在向左移動100px,每段動畫持續(xù)時間都是3s.
dom.animate({left:'100px'},3000,'linear',function(){
dom.animate({top:'100px'},3000,'linear',function(){
dom.animate({left:'0px'},3000,'linear',function(){
console.log('動畫 done')
})
})
})
這樣就會看到形成了一個回調(diào)嵌套,也就是我們常說的回調(diào)地獄
衩侥,導(dǎo)致代碼可讀性十分差国旷。
事件
事件處理就是jQuery
中的on
綁定事件和trigger
觸發(fā)事件,其實就是我們常見的發(fā)布訂閱模式茫死,當(dāng)我訂閱了一個事件跪但,那么我就是訂閱者,如果發(fā)布者發(fā)布了數(shù)據(jù)之后峦萎,那么我就要收到相應(yīng)的通知屡久。
// 定義一個發(fā)布中心
let publishCenter = {
subscribeArrays:{}, // 定義一個訂閱者回調(diào)函數(shù)callback
subscribe:function(key,callback){
// 增加訂閱者
if(!this.subscribeArrays[key]){
this.subscribeArrays[key] = [];
}
this.subscribeArrays[key].push(callback)
},
publish:function(){
//發(fā)布 第一個參數(shù)是key
let params = [...arguments];
let key = params.shift();
let callbacks = this.subscribeArrays[key];
if(!callbacks || callbacks.length === 0){
// 如果沒人訂閱 那么就返回
return false
}
for( let i = 0 ; i < callbacks.length; i++ ){
callbacks[i].apply( this, params );
}
}
};
// 訂閱 一個wantWatermelon事件
publishCenter.subscribe('wantWatermelon',function(){console.log('恰西瓜咯~~')})
//觸發(fā)wantWatermelon事件 好咯 可以看到 恰西瓜咯
publishCenter.publish('wantWatermelon')
恰西瓜中~~~
Promise A+
嗝~ok,吃完我們進(jìn)入正題爱榔,看到上面異步編程如此如此如此麻煩被环,對于我這種頭大用戶,當(dāng)然是拒絕的啊详幽,還好我們有Pormise
(Pormise
大法好),下面我們就來通過實現(xiàn)一個Promise
去更深的了解Promise
的原理筛欢,首先我們了解一下PromiseA+
,它是一種規(guī)范唇聘,用來約束大家寫的Promise
方法的版姑,為了讓大家寫的Promise
杜絕一些錯誤,按照我們所期望的流程來走雳灾,因此就出現(xiàn)了PromiseA+
規(guī)范漠酿。
Promise特點
我們根據(jù)PromiseA+
文檔來一步一步的看Promise
有什么特點。
首先我們看文檔的2.1節(jié)谎亩,題目是Promise states炒嘲,也就是說講的是Promise
的狀態(tài),那么都說了些什么呢匈庭,我們來看一哈:
- 一個promise只有三種狀態(tài)夫凸,pending態(tài),fulfilled態(tài)(完成態(tài)),rejected(拒絕態(tài))
- 當(dāng)promise處于pending態(tài)時阱持,可能轉(zhuǎn)化成fulfilled或者rejected
- 一旦promise的狀態(tài)改成了fulfilled后夭拌,狀態(tài)就不能再改變了,并且需要提供一個不可變的value
- 一旦promise的狀態(tài)改成了rejected后衷咽,狀態(tài)就不能再改變了鸽扁,并且需要提供一個不可變的reason
ok,那么我們就開始寫我們自己的Promise
,我們先看看一段正常Promise
的寫法
// 成功或者失敗是需要提供一個value或者reason
let promise1 = new Promise((resolve,rejected)=>{
// 可以發(fā)現(xiàn) 當(dāng)我們new Promise的時候這句話是同步執(zhí)行的 也就是說當(dāng)我們初始化一個promise的時候 內(nèi)部的回調(diào)函數(shù)(通常我們叫做執(zhí)行器executor)會立即執(zhí)行
console.log('hahahha');
// promise內(nèi)部支持異步
setTimeout(function(){
resolve(123);
},100)
// throw new Error('error') 我們也可以在執(zhí)行器內(nèi)部直接拋出一個錯誤 這時promise會直接變成rejected態(tài)
})
根據(jù)我們上面的代碼還有PromiseA+規(guī)范中的狀態(tài)說明镶骗,我們可以知道Promise
已經(jīng)有了下面幾個特點
-
promise
有三種狀態(tài) 默認(rèn)pending
態(tài)pending
可以變成fulfilled
(成功態(tài))或者rejected
(失敗態(tài))桶现,而一旦轉(zhuǎn)變之后就不能在變成其他值了 -
promise
內(nèi)部有一個value
用來存儲成功態(tài)的結(jié)果 -
promise
內(nèi)部有一個reason
用來存儲失敗態(tài)的原因 -
promise
接受一個executor
函數(shù),這個函數(shù)有兩個參數(shù)鼎姊,一個是resolve
方法骡和,一個是reject
方法相赁,當(dāng)執(zhí)行resolve
時,promise
狀態(tài)改變?yōu)?code>fulfilled慰于,執(zhí)行reject
時钮科,promise
狀態(tài)改變?yōu)?code>rejected - 默認(rèn)
new Promise
執(zhí)行的時候內(nèi)部的executor
函數(shù)執(zhí)行 -
promise
內(nèi)部支持異步改變狀態(tài) -
promise
內(nèi)部支持拋出異常,那么該promise
的狀態(tài)直接改成rejected
我們接下來繼續(xù)看PromiseA+文檔:
promise
必須要有一個then
方法婆赠,用來訪問它當(dāng)前的value
或者是reason
- 該方法接受兩個參數(shù)
onFulfilled
(成功回掉函數(shù))绵脯,onRejected
(失敗回調(diào)函數(shù))promise.then(onFulfilled, onRejected)
- 這兩個參數(shù)都是可選參數(shù),如果發(fā)現(xiàn)這兩個參數(shù)不是函數(shù)類型的話页藻,那么就忽略 比如
promise.then().then(data=>console.log(data),err=>console.log(err))
就可以形成一個值穿透onFulfilled
必須在promise
狀態(tài)改成fulfilled
之后改成調(diào)用桨嫁,并且呢promise
內(nèi)部的value
值是這個函數(shù)的參數(shù),而且這個函數(shù)不能重復(fù)調(diào)用onRejected
必須在promise
狀態(tài)改成rejected
之后改成調(diào)用份帐,并且呢promise
內(nèi)部的reason
值是這個函數(shù)的參數(shù)璃吧,而且這個函數(shù)不能重復(fù)調(diào)用onFulfilled
和onRejected
這兩個方法必須要在當(dāng)前執(zhí)行棧的上下文執(zhí)行完畢后再調(diào)用,其實就是事件循環(huán)中的微任務(wù)(setTimeout
是宏任務(wù)废境,有一定的差異)onFulfilled
和onRejected
這兩個方法必須通過函數(shù)調(diào)用畜挨,也就是說 他們倆不是通過this.onFulfilled()
或者this.onRejected()
調(diào)用,直接onFulfilled()
或者onRejected()
then
方法可以在一個promise
上多次調(diào)用噩凹,也就是我們常見的鏈?zhǔn)秸{(diào)用- 如果當(dāng)前
promise
的狀態(tài)改成了fulfilled
那么就要按照順序依次執(zhí)行then
方法中的onFulfilled
回調(diào)- 如果當(dāng)前
promise
的狀態(tài)改成了rejected
那么就要按照順序依次執(zhí)行then
方法中的onRejected
回調(diào)then
方法必須返回一個promise
(接下來我們會把這個promise
稱做promise2
)巴元,類似于promise2 = promise1.then(onFulfilled, onRejected);
- 如果呢
onFulfilled()
或者onRejected()
任一一個返回一個值x
,那么就要去執(zhí)行resolvePromise
這個函數(shù)中去(這個函數(shù)是用來處理返回值x
遇到的各種值驮宴,然后根據(jù)這些值去決定我們剛剛then
方法中onFulfilled()
或者onRejected()
這兩個回調(diào)返回的promise2
的狀態(tài))- 如果我們在
then
中執(zhí)行onFulfilled()
或者onRejected()
方法時產(chǎn)生了異常逮刨,那么就將promise2
用異常的原因e
去reject
- 如果
onFulfilled
或者onRejected
不是函數(shù),并且promise
的狀態(tài)已經(jīng)改成了fulfilled
或者rejected
堵泽,那么就用同樣的value
或者reason
去更新promise2
的狀態(tài)(其實這一條和第三條一個道理修己,也就是值得穿透問題)
好吧,我們總結(jié)了這么多規(guī)范特點迎罗,那么我們就用這些先來練練手
/**
* 實現(xiàn)一個PromiseA+
* @description 實現(xiàn)一個簡要的promise
* @param {Function} executor 執(zhí)行器
* @author Leslie
*/
function Promise(executor){
let self = this;
self.status = 'pending'; // 存儲promise狀態(tài) pending fulfilled rejected.
self.value = undefined; // 存儲成功后的值
self.reason = undefined; // 記錄失敗的原因
self.onfulfilledCallbacks = []; // 異步時候收集成功回調(diào)
self.onrejectedCallbacks = []; // 異步時候收集失敗回調(diào)
function resolve(value){
if(self.status === 'pending'){
self.status = 'fulfilled';// resolve的時候改變promise的狀態(tài)
self.value = value;//修改成功的值
// 異步執(zhí)行后 調(diào)用resolve 再把存儲的then中的成功回調(diào)函數(shù)執(zhí)行一遍
self.onfulfilledCallbacks.forEach(element => {
element()
});
}
}
function reject(reason){
if(self.status === 'pending'){
self.status = 'rejected';// reject的時候改變promise的狀態(tài)
self.reason = reason; // 修改失敗的原因
// 異步執(zhí)行后 調(diào)用reject 再把存儲的then中的失敗回調(diào)函數(shù)執(zhí)行一遍
self.onrejectedCallbacks.forEach(element => {
element()
});
}
}
// 如果執(zhí)行器中拋出異常 那么就把promise的狀態(tài)用這個異常reject掉
try {
//執(zhí)行 執(zhí)行器
executor(resolve,reject);
} catch (error) {
reject(error)
}
}
Promise.prototype.then = function(onfulfilled,onrejected){
// onfulfilled then方法中的成功回調(diào)
// onrejected then方法中的失敗回調(diào)
let self = this;
// 如果onfulfilled不是函數(shù) 那么就用默認(rèn)的函數(shù)替代 以便達(dá)到值穿透
onfulfilled = typeof onfulfilled === 'function'?onfulfilled:val=>val;
// 如果onrejected不是函數(shù) 那么就用默認(rèn)的函數(shù)替代 以便達(dá)到值穿透
onrejected = typeof onrejected === 'function'?onrejected: err=>{throw err}
let promise2 = new Promise((resolve,reject)=>{
if(self.status === 'fulfilled'){
// 加入setTimeout 模擬異步
// 如果調(diào)用then的時候promise 的狀態(tài)已經(jīng)變成了fulfilled 那么就調(diào)用成功回調(diào) 并且傳遞參數(shù)為 成功的value
setTimeout(function(){
// 如果執(zhí)行回調(diào)發(fā)生了異常 那么就用這個異常作為promise2的失敗原因
try {
// x 是執(zhí)行成功回調(diào)的結(jié)果
let x = onfulfilled(self.value);
// 調(diào)用resolvePromise函數(shù) 根據(jù)x的值 來決定promise2的狀態(tài)
resolvePromise(promise2,x,resolve,reject);
} catch (error) {
reject(error)
}
},0)
}
if(self.status === 'rejected'){
// 加入setTimeout 模擬異步
// 如果調(diào)用then的時候promise 的狀態(tài)已經(jīng)變成了rejected 那么就調(diào)用失敗回調(diào) 并且傳遞參數(shù)為 失敗的reason
setTimeout(function(){
// 如果執(zhí)行回調(diào)發(fā)生了異常 那么就用這個異常作為promise2的失敗原因
try {
// x 是執(zhí)行失敗回調(diào)的結(jié)果
let x = onrejected(self.reason);
// 調(diào)用resolvePromise函數(shù) 根據(jù)x的值 來決定promise2的狀態(tài)
resolvePromise(promise2,x,resolve,reject);
} catch (error) {
reject(error)
}
},0)
}
if(self.status === 'pending'){
//如果調(diào)用then的時候promise的狀態(tài)還是pending睬愤,說明promsie執(zhí)行器內(nèi)部的resolve或者reject是異步執(zhí)行的,那么就需要先把then方法中的成功回調(diào)和失敗回調(diào)存儲襲來纹安,等待promise的狀態(tài)改成fulfilled或者rejected時候再按順序執(zhí)行相關(guān)回調(diào)
self.onfulfilledCallbacks.push(()=>{
//setTimeout模擬異步
setTimeout(function(){
// 如果執(zhí)行回調(diào)發(fā)生了異常 那么就用這個異常作為promise2的失敗原因
try {
// x 是執(zhí)行成功回調(diào)的結(jié)果
let x = onfulfilled(self.value)
// 調(diào)用resolvePromise函數(shù) 根據(jù)x的值 來決定promise2的狀態(tài)
resolvePromise(promise2,x,resolve,reject);
} catch (error) {
reject(error)
}
},0)
})
self.onrejectedCallbacks.push(()=>{
//setTimeout模擬異步
setTimeout(function(){
// 如果執(zhí)行回調(diào)發(fā)生了異常 那么就用這個異常作為promise2的失敗原因
try {
// x 是執(zhí)行失敗回調(diào)的結(jié)果
let x = onrejected(self.reason)
// 調(diào)用resolvePromise函數(shù) 根據(jù)x的值 來決定promise2的狀態(tài)
resolvePromise(promise2,x,resolve,reject);
} catch (error) {
reject(error)
}
},0)
})
}
})
return promise2;
}
一氣呵成尤辱,是不是覺得之前總結(jié)出的特點十分有效,對著特點十分順暢的就擼完了代碼~
那么就讓我們接著來看看promiseA+文檔里還有些什么內(nèi)容吧
resolvePromise
這個函數(shù)呢會決定promise2
用什么樣的狀態(tài)厢岂,如果x
是一個普通值光督,那么就直接采用x
,如果x
是一個promise
那么就將這個promise
的狀態(tài)當(dāng)成是promise2
的狀態(tài)- 判斷如果
x
和promise2
是一個對象塔粒,即promise2 === x
可帽,那么就陷入了循環(huán)調(diào)用,這時候promise2
就會以一個TypeError
為reason
轉(zhuǎn)化為rejected
- 如果
x
是一個promise
窗怒,那么promise2
就采用x
的狀態(tài)映跟,用和x
相同的value
去resolve
,或者用和x
相同的reason
去reject
- 如果
x
是一個對象或者是函數(shù) 那么就先執(zhí)行let then = x.then
- 如果
x
不是一個對象或者函數(shù) 那么就resolve
這個x
- 如果在執(zhí)行上面的語句中報錯了扬虚,那么就用這個錯誤原因去
reject
promise2
- 如果
then
是一個函數(shù)努隙,那么就執(zhí)行then.call(x,resolveCallback,rejectCallback)
- 如果
then
不是一個函數(shù),那么就resolve
這個x
- 如果
x
是fulfilled
態(tài) 那么就會走resolveCallback
這個函數(shù)辜昵,這時候就默認(rèn)把成功的value
作為參數(shù)y
傳遞給resolveCallback
,即y=>resolvePromise(promise2,y)
,繼續(xù)調(diào)用resolvePromise
這個函數(shù) 確保 返回值是一個普通值而不是promise
- 如果
x
是rejected
態(tài) 那么就把這個失敗的原因reason
作為promise2
的失敗原因reject
出去- 如果
resolveCallback
,rejectCallback
這兩個函數(shù)已經(jīng)被調(diào)用了荸镊,或者多次被相同的參數(shù)調(diào)用,那么就確保只調(diào)第一次堪置,剩下的都忽略掉- 如果調(diào)用
then
拋出異常了躬存,并且如果resolveCallback
,rejectCallback
這兩個函數(shù)已經(jīng)被調(diào)用了,那么就忽略這個異常舀锨,否則就用這個異常作為promise2
的reject
原因
我們又又又又又又總結(jié)了這么多岭洲,好吧不說了總結(jié)多少就開擼吧。
/**
* 用來處理then方法返回結(jié)果包裝成promise 方便鏈?zhǔn)秸{(diào)用
* @param {*} promise2 then方法執(zhí)行產(chǎn)生的promise 方便鏈?zhǔn)秸{(diào)用
* @param {*} x then方法執(zhí)行完成功回調(diào)或者失敗回調(diào)后的result
* @param {*} resolve 返回的promise的resolve方法 用來更改promise最后的狀態(tài)
* @param {*} reject 返回的promise的reject方法 用來更改promise最后的狀態(tài)
*/
function resolvePromise(promise2,x,resolve,reject){
// 首先判斷x和promise2是否是同一引用 如果是 那么就用一個類型錯誤作為Promise2的失敗原因reject
if( promise2 === x) return reject(new TypeError('typeError:大佬坎匿,你循環(huán)引用了!'));
// called 用來記錄promise2的狀態(tài)改變盾剩,一旦發(fā)生改變了 就不允許 再改成其他狀態(tài)
let called;
if( x !== null && ( typeof x === 'object' || typeof x === 'function')){
// 如果x是一個對象或者函數(shù) 那么他就有可能是promise 需要注意 null typeof也是 object 所以需要排除掉
//先獲得x中的then 如果這一步發(fā)生異常了,那么就直接把異常原因reject掉
try {
let then = x.then;//防止別人瞎寫報錯
if(typeof then === 'function'){
//如果then是個函數(shù) 那么就調(diào)用then 并且把成功回調(diào)和失敗回調(diào)傳進(jìn)去替蔬,如果x是一個promise 并且最終狀態(tài)時成功告私,那么就會執(zhí)行成功的回調(diào),如果失敗就會執(zhí)行失敗的回調(diào)如果失敗了承桥,就把失敗的原因reject出去驻粟,做為promise2的失敗原因,如果成功了那么成功的value時y凶异,這個y有可能仍然是promise蜀撑,所以需要遞歸調(diào)用resolvePromise這個方法 直達(dá)返回值不是一個promise
then.call(x,y => {
if(called) return;
called = true;
resolvePromise(promise2,y,resolve,reject)
}, error=>{
if(called) return
called = true;
reject(error)
})
}else{
resolve(x)
}
} catch (error) {
if(called) return
called = true;
reject(error)
}
}else{
// 如果是一個普通值 那么就直接把x作為promise2的成功value resolve掉
resolve(x)
}
}
finnnnnnnnnally,我們終于通過我們的不懈努力實現(xiàn)了一個基于PromiseA+規(guī)范的Promise
!
最后呢為了完美唠帝,我們還要在這個promise
上實現(xiàn)Promise.resolve
,Promise.reject
,以及catch
屯掖,Promise.all
和Promise.race
這些方法。
Promise的一些方法
Promise.resolve = function(value){
return new Promise((resolve,reject)=>{
resolve(value)
})
}
Promise.reject = function(reason){
return new Promise((resolve,reject)=>{
reject(reason)
})
}
Promise.prototype.catch = function(onRejected){
return this.then(null,onRejected)
}
Promise.all = function(promises){
return new Promise((resolve,reject)=>{
let arr = [];
let i = 0;
function getResult(index,value){
arr[index] = value;
if(++i == promises.length) {
resolve(arr)
}
}
for(let i = 0;i<promises.length;i++){
promises[i].then(data=>{
getResult(i,data)
},reject)
}
})
}
Promise.race = function(promises){
return new Promise((resolve,reject)=>{
for(let i = 0 ; i < promises.length ; i++){
promises[i].then(resolve,reject)
}
})
}
Promise 語法糖
恰完西瓜來口糖襟衰,語法糖是為了讓我們書寫promise的時候能夠更加的快速贴铜,所以做了一層改變,我們來看一個例子瀑晒,比如當(dāng)我們封裝一個異步讀取圖片的寬高函數(shù)
// 原來的方式
let getImgWidthHeight = function(imgUrl){
return new Promise((resolve,reject)=>{
let img = new Image();
img.onload = function(){
resolve(img.width+'-'+img.height)
}
img.onerror = function(e){
reject(e)
}
img.src = imgUrl;
})
}
是不是覺得怎么寫起來有點舒服但又有點不舒服绍坝,好像我每次都要去寫執(zhí)行器啊苔悦!為什么轩褐!好的,沒有為什么玖详,既然不舒服 我們就改!
// 實現(xiàn)一個promise的語法糖
Promise.defer = Promise.deferred = function (){
let dfd = {};
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject;
})
return dfd
}
有了上面的語法糖我們再看一下那個圖片的函數(shù)怎么寫
let newGetImgWidthHeight = function(imgUrl){
let dfd = Promise.defer();
let img = new Image();
img.onload = function(){
dfd.resolve(img.width+'-'+img.height)
}
img.onerror = function(e){
dfd.reject(e)
}
img.url = imgUrl;
return dfd.promise
}
是不是發(fā)現(xiàn)我們少了一層函數(shù)嵌套把介,呼~~ 得勁~~
最終檢測
npm install promises-aplus-tests -g
既然我們都說了我們是遵循promiseA+規(guī)范的勤讽,那至少要拿出點證據(jù)來是不是,不然是不是說服不了大家拗踢,那么我們就用promises-aplus-tests這個包來檢測我們寫的promise
究竟怎么樣呢脚牍!安裝完成之后來跑一下我們的promise
最終跑出來我們?nèi)客ㄟ^測試!酷巢墅!晚餐再加個雞腿~