事件和回調(diào)函數(shù)的缺陷
我們習(xí)慣于使用傳統(tǒng)的回調(diào)或者事件處理來解決異步問題
事件: 某個對象的屬性是一個函數(shù), 當(dāng)發(fā)生一件事時, 運行該函數(shù)
dom.onclick = function() {
// ...執(zhí)行的代碼
}
回調(diào):運行某個函數(shù)以實現(xiàn)某個功能的時候, 傳入一個函數(shù)作為參數(shù), 當(dāng)某個特定的條件下的時候, 就會觸發(fā)該函數(shù)
dom.addEventListner('click', callback);
本質(zhì)上,事件和回調(diào)沒有本質(zhì)上的區(qū)別, 只是把函數(shù)放置的位置不同而已, 一直以來這個模式都運作良好,直到前端工作越來越復(fù)雜,目前該模式主要面臨以下兩個問題
- 回調(diào)地獄: 某個異步操作需要等待之前的異步操作完成, 無論是回調(diào)還是事件都會陷入不斷的嵌套,
ajax({
url, method, data,
success: res => { // 當(dāng)某個請求數(shù)據(jù)的接口需要用到上一次請求回來的數(shù)據(jù)
if(res.data) { // 的時候, 我們就會生成回調(diào)地獄, 代碼非常的難看
ajax({
url, method, res.data.data,
success: res => {
// ajax....
}
})
}
}
})
- 異步之間的聯(lián)系: 某個異步操作需要等待多個異步操作的結(jié)果, 對這種聯(lián)系的處理, 會讓代碼復(fù)雜度增加
// 小明像10個女神表白, 每個女神會思考部分時間以后再進(jìn)行回復(fù), 當(dāng)所有女神回復(fù)完畢以后, 小明統(tǒng)計日志
for(let i = 0, len = girArr.length; i < len; i++) {
getResponse() // ..表白函數(shù)執(zhí)行, 每個表白函數(shù)中有一個setTimeout的異步來決定女神的思考時間,思考完成以后會推入result數(shù)組
}
if(reslut.length === 10) {
// ...開始記錄日志 這樣會顯得相當(dāng)麻煩, 且不符合設(shè)計模式的單一原則
}
Promise
為了解決上述的問題, ES6引入了Promise, 這一塊概念文字較多(建議多讀幾遍至少對es6處理異步的通用模型有一定的基礎(chǔ)認(rèn)識,如果時間緊迫不想閱讀可以直接劃到下面看promise的實例講解)
我們先來看看ES6對異步處理的通用模型
ES6官方參考了大量的異步場景, 總結(jié)出了一套異步的通用模型, 該模型幾乎可以覆蓋所有的異步場景, 甚至是某些同步場景
值得注意的是, 為了兼容舊系統(tǒng), ES6并不打算拋掉過去的做法, 只是基于該模型推出了一個全新的api,使用該api會讓異步處理更加的簡潔和優(yōu)雅
理解該API, 最重要的是理解他的異步模型,
- ES6將某一件可能發(fā)生異步操作的事情, 分成兩個階段: unsettled和settled
- unsettled: 未決階段, 表示事情還在進(jìn)行前期的處理, 并沒有發(fā)生通向結(jié)果的那件事(比如監(jiān)聽一個dom的點擊事件, 這時候用戶還未點擊)
- settled: 已決階段, 事情已經(jīng)有了一個結(jié)果, 不管這個結(jié)果是好是壞, 整件事情無法逆轉(zhuǎn)(比如用戶觸發(fā)點擊事件已經(jīng)產(chǎn)生了結(jié)果)
事件總是從未決階段逐步發(fā)展到已決階段的, 未決階段擁有控制何時通向已決階段的能力
- 同時ES6將事情劃分為三種狀態(tài): padding, resolved, rejected
- padding: 掛起, 處于未決階段, 最終結(jié)果還沒有出來
- resolved: 已處理, 已決階段的一種狀態(tài), 表示整件事情已經(jīng)產(chǎn)生結(jié)果, 并且是一個可以按照正常邏輯進(jìn)行下去的結(jié)果。
- rejected: 已拒絕, 已決階段的一種狀態(tài), 表示整件事情已經(jīng)產(chǎn)生結(jié)果, 并且是一個無法按照正常邏輯走下去的結(jié)果, 通常用于表示有錯誤
因為未決階段有能力決定事情的走向, 所以未決階段可以決定事情的最終走向
- 我們將把事情變?yōu)閞esolved狀態(tài)的過程叫做resolve, 推向該狀態(tài)時, 可以傳遞一些數(shù)據(jù)
- 我們把事情變?yōu)閞ejected狀態(tài)的過程叫做reject, 推向該狀態(tài)時, 通常傳遞一些錯誤
始終記住, 無論是階段還是狀態(tài)都是不可逆轉(zhuǎn)的
- 當(dāng)事情發(fā)展到已決階段以后, 通常需要進(jìn)行后續(xù)處理, 不同的已決狀態(tài), 決定了不同的后續(xù)處理
- resolved: 后續(xù)處理為thenable
- rejected: 后續(xù)處理為catchable
后續(xù)處理可能會有很多個, 因此會形成作業(yè)隊列, 這些后續(xù)處理會按照順序, 當(dāng)狀態(tài)到達(dá)后依次執(zhí)行
ES6把上述一系列的概念和操作, 取了一個很優(yōu)雅的名字叫做Promise(承諾)
Pormise的基本使用
Pormise是一個構(gòu)造類, 他接受一個函數(shù)作為參數(shù), 如下
const promise = new Promise((resolve, rejected) => {
/*
未決處理的階段
ajax請求等異步操作可以放在這兒
通過調(diào)用resolve函數(shù)將Promise推向已決的resolved狀態(tài)
通過調(diào)用rejected將Promise推向已決的rejected狀態(tài)
resolve和reject都可以傳遞最多一個參數(shù), 表示推向狀態(tài)的數(shù)據(jù)
*/
})
promise.then(result => {
/*
這是thenable函數(shù), 如果當(dāng)前的Promise已經(jīng)是resolved狀態(tài), 該函數(shù)會立即執(zhí)行,如果當(dāng)前是未決狀態(tài), 則會加入作業(yè)隊列, 等待Promise狀態(tài)變?yōu)閞esolved會立即執(zhí)行
*/
}, err => {
/*
這是catchable函數(shù), 如果當(dāng)前的Promise已經(jīng)是rejected狀態(tài), 該函數(shù)會立即執(zhí)行, 如果當(dāng)前是未決狀態(tài), 則會加入作業(yè)隊列, 等待Promise狀態(tài)變?yōu)閞ejected會立即執(zhí)行
*/
})
同時Promise可以有多個then和catch并列進(jìn)行,適用于對同一個Promise的多種處理
promise.then(res => {
console.log('這個res我要存入緩存')
})
promise.then(res => {
console.log('這個res我要用來獲取新一輪的數(shù)據(jù)');
})
Promise的細(xì)節(jié)
- 未決階段的處理函數(shù)是同步的, 會立即執(zhí)行
const promise = new Promise((resolve, reject) => {
console.log('helloworld, 我是未決階段的處理函數(shù)');
/* 這個函數(shù)是同步的會立即執(zhí)行 */
})
- thenable和catchable函數(shù)是異步的, 就算是立即執(zhí)行, 也會放到event queue中等待執(zhí)行, 并且加入的是微任務(wù)
const promise = new Promise((resolve, reject) => {
console.log('padding狀態(tài), 但是我要立馬將他推入resolve');
resolve('hello');
})
setTimeout(() => {
console.log('我是宏任務(wù)的計時器')
},0)
promise.then(res => {
console.log(res);
})
console.log('我是callstack的執(zhí)行棧任務(wù)');
/*
執(zhí)行結(jié)果肯定是
padding狀態(tài), 但是我要立馬將他推入resolve
我是callstack的執(zhí)行棧任務(wù)
hello
我是宏任務(wù)的計時器
*/
如果對js執(zhí)行機制還不是很清楚的話, 可以查看我寫的js執(zhí)行機制和ui多線程的博客
- promise.then可以只添加thenable函數(shù), promise.catch也可以只添加catchable函數(shù)
promise.then(resulte => {
/* thenable */
}).catch(error => {
/* catchable */
})
- 在未決階段的處理函數(shù)中, 如果發(fā)生未捕獲的錯誤, 會將狀態(tài)推向rejected, 并會被catchable捕獲,(比如請求數(shù)據(jù)的時候網(wǎng)斷了)
const promise = new Promise((resolve, reject) => {
throw new Error('xxx'); // 會導(dǎo)致promise 直接觸發(fā)reject狀態(tài)
})
- 一旦狀態(tài)推向了已決階段, 無法再對狀態(tài)做任何更改
const promise = new Promise((resolve, reject) => {
resolve();
resolve(); //這個是無效的, 因為上面的結(jié)果已經(jīng)確定了,結(jié)果是不可逆轉(zhuǎn)的
})
Promise并沒有消除回調(diào), 只是讓回調(diào)變得可控
下面我們來看幾個Promise的實例:
- 小明表白女神事件, 小明發(fā)出表白申請,因為要看看小明是不是自己心儀的男生, 所以女神會思考三秒鐘以后才會告訴小明答應(yīng)還是不答應(yīng)
/* 小明向女神表白, 如果隨機數(shù)大于0.1 則表白失敗, 反之表白成功 */
const promise = new Promise((resolve, reject) => {
console.log('此時為padding狀態(tài),該函數(shù)是同步會立即執(zhí)行');
setTimeout(() => {
if(Math.random() < 0.1) {
/* 表白成功啦, 同時代表事件已經(jīng)處理, 從未決階段變成已決階段, 整件事情已經(jīng)產(chǎn)生結(jié)果(小明獲得女神芳心), 并且這個結(jié)果是按照正常邏輯走下去的結(jié)果, 所以我們需要用到resolve將Promise狀態(tài)推向resolve狀態(tài)*/
resolve(true);
}else {
/* 表白失敗, 同樣代表事件已經(jīng)處理, 從未決階段變成已決階段, 整件事情已經(jīng)產(chǎn)生結(jié)果(小明心碎了), 這個結(jié)果也是按照正常邏輯走下去的結(jié)果(女神本來就可能同意也可能拒絕), 所以我們同樣需要用到resolve將Promise狀態(tài)推向resolve狀態(tài), 但是根據(jù)需求我們可能傳遞的參數(shù)不一樣*/
resolve(false);
}
}, 3000)
})
promise.then(result => {
/*當(dāng)事件處于resolved狀態(tài)就會進(jìn)入thenable, 我們看到無論女神答應(yīng)與否其實這個都是按照正常邏輯走下去的狀態(tài), 我們通過then接受到這個狀態(tài), 同時根據(jù)不同的參數(shù)處理不同的結(jié)果*/
console.log(result);
if(result) {
console.log('小明表白成功, 恭喜恭喜')
}else {
console.log('今天的我你愛理不理, 明天的我你高攀不起')
}
}).catch(err => {
/*當(dāng)事情進(jìn)入了rejected狀態(tài)的話會觸發(fā)catchable, 代表發(fā)生了一些錯誤, 比如網(wǎng)絡(luò)請求失敗*/
console.log(err);
})
- 在網(wǎng)絡(luò)請求中使用Promise來請求數(shù)據(jù)
/* 在網(wǎng)絡(luò)請求中使用Promise */
const ajaxPromise = new Promise((resolve, reject) => {
/* 開啟ajax請求 */
ajax({
url: 'www.xxx.com',
method: 'POST',
data: {
name: 'tommy'
},
/* 在成功的回調(diào)函數(shù)中, 不管返回的結(jié)果是什么我們都會用resolve將Promise的狀態(tài)推入resolved狀態(tài) */
success: function(data) {
resolve(data);
},
fail: function(err) {
/* 而如果觸發(fā)了失敗的回調(diào)函數(shù), 那么可能是網(wǎng)絡(luò)或者服務(wù)器或者自身出問題了,我們應(yīng)該將狀態(tài)推入rejected狀態(tài) */
rejected(err);
}
})
})
/* 只要Promise狀態(tài)變?yōu)閞esolved狀態(tài)就會立馬觸發(fā)thenabale的函數(shù), 只要變?yōu)閞ejected狀態(tài)馬上就會觸發(fā)catchable函數(shù) */
ajaxPromise.then(data => {
console.log(data);
/* 拿到data做一系列處理 */
}).catch(err => {
console.log(err);
/* 查看捕獲的錯誤 */
})
Promise的串聯(lián)
當(dāng)后續(xù)的Promise需要用到之前的promise的處理結(jié)果時, 就需要用到promise的串聯(lián)
在promise對象中, 無論是then方法還是catch方法, 都是有返回值的,返回的是一個全新的promise對象, 他的狀態(tài)滿足下面的規(guī)則
- 如果當(dāng)前的Promise是未決(padding)狀態(tài), 那么得到的新的Promise也是未決狀態(tài)
- 如果當(dāng)前的Promise是已決狀態(tài), 那么會運行相應(yīng)的后續(xù)處理函數(shù), 并將后續(xù)處理函數(shù)的結(jié)果(返回值)作為resolved的狀態(tài)數(shù)據(jù), 應(yīng)用到新的Promise中, 如果后續(xù)處理函數(shù)發(fā)生錯誤, 則把返回值作為rejected的狀態(tài)數(shù)據(jù), 應(yīng)用到新的Promise中.
后續(xù)的Promise一定會等到前面的Promise有了后續(xù)處理結(jié)果后才會變成已決狀態(tài)
const promise = new Promise((resolve, reject) => {
resolve(1);
})
const promise2 = promise.then(res => res * 2);
promise2.then(res => {
console.log(res);
})
/* 直接輸出promise2拿到的是padding狀態(tài)的promise對象, 因為then和catch都是異步的, 同時promise被resolve推向了resolved狀態(tài), 那么勢必會執(zhí)行then后面的函數(shù), promise2也會變?yōu)閞esolved狀態(tài), 同時promise的then的返回結(jié)果會作為promise2的resolved處理函數(shù)的參數(shù)被放進(jìn)then中, 也就是上面的res */
如果返回的是一個Promise對象, 那么會把這個Promise對象拆解開并且把狀態(tài)數(shù)據(jù)傳遞給新的Promise對象
const promise = new Promise((resolve, reject) => {
resolve(1);
})
const promise2 = promise.then(res => {
return new Promise((resolve, reject) => {
resolve(2);
})
})
promise2.then(res => {
console.log(res); // 輸出2
})
Promise的其他api
原型成員(實例成員
- then: 注冊一個后續(xù)處理函數(shù), 當(dāng)Promise為resolved狀態(tài)時運行該函數(shù),
- catch: 注冊一個后續(xù)處理函數(shù), 當(dāng)Promise為rejected狀態(tài)時運行該函數(shù)
- finally: [ES2018]注冊一個后續(xù)處理參數(shù)(無參), 當(dāng)Promise為已決時運行該函數(shù)
const promise = Promise((resolve, reject) => {
resolve();
})
promise.finally(() => {
console.log('helloworld');
})
構(gòu)造函數(shù)成員(靜態(tài)成員)
- resolve(數(shù)據(jù)): 該方法返回一個resolved狀態(tài)的Promise, 傳遞的數(shù)據(jù)作為狀態(tài)數(shù)據(jù)丈咐。
特殊情況: 如果傳遞的數(shù)據(jù)是Promise, 則直接返回傳遞的Promise對象
Promise.resolve(1); //
- reject(數(shù)據(jù)): 該方法返回一個rejected狀態(tài)的Promise, 傳遞的數(shù)據(jù)為狀態(tài)數(shù)據(jù)
Promise.reject(2); //
- all(iterable): 這個方法返回一個新的Promise對象, 該promise對象在iterable參數(shù)對象里所有的promise對象都成功的時候才會被觸發(fā)成功, 一旦有任何一個在iterable里的promise對象失敗則立馬觸發(fā)該promise對象的失敗, 這個新的promise對象在觸發(fā)成功狀態(tài)以后, 會把一個包含iterable里所有promise返回值的數(shù)組作為成功回調(diào)的返回值, 順序跟iterable保持一致,如果這個新的promise對象觸發(fā)了失敗狀態(tài), 那么他會把iterable里第一個觸發(fā)失敗的promise對象的失敗信息作為他的失敗錯誤信息, Promise.all方法常被用于處理多個promise對象的狀態(tài)集合。
let proms = [];
for(let i = 0; i < 10; i ++) {
proms.push(new Promise((resolve, reject) => {
setTimeout(() => {
resolve(i);
}, 3000)
}))
}
console.log(proms);
Promise.all(proms).then(res => {
console.log('全部請求完成',res);
}).catch(err => {
console.log(err);
})
- race(iterable): 當(dāng)iterable參數(shù)里的任意一個子promise被成功或者失敗以后, 父類promise馬上也會用子promise的成功返回值或失敗詳情作為參數(shù)調(diào)用父類promise綁定的相對應(yīng)的句柄, 并返回該promise對象
let proms = [];
for(let i = 0; i < 10; i++) {
proms.push(new Promise((resolve, reject) => {
reject('我是第一個error' + i);
}))
}
Promise.race(proms).then(res => {
console.log(res);
}).catch(err => {
console.log(err);
})