本文目錄:
- 1.什么是回調(diào)函數(shù)?回調(diào)函數(shù)有什么缺點(diǎn)
- 2.Promise是什么,可以手寫實(shí)現(xiàn)一下嗎
- 3.手寫簡(jiǎn)單版的Promise
- 4.then,catch,finally
- 5.手寫promise.all
- 6.手寫promise.race
- 7.Promise里都是微任務(wù)嗎
- 8.async await 和 promise 的區(qū)別與聯(lián)系
- 9.Promise中用了什么設(shè)計(jì)模式
1.什么是回調(diào)函數(shù)?回調(diào)函數(shù)有什么缺點(diǎn)
回調(diào)函數(shù)是一段可執(zhí)行的代碼段,它作為一個(gè)參數(shù)傳遞給其他的代碼,其作用是在需要的時(shí)候方便調(diào)用這段(回調(diào)函數(shù))代碼。
在JavaScript中函數(shù)也是對(duì)象的一種,同樣對(duì)象可以作為參數(shù)傳遞給函數(shù),因此函數(shù)也可以作為參數(shù)傳遞給另外一個(gè)函數(shù),這個(gè)作為參數(shù)的函數(shù)就是回調(diào)函數(shù)。
const btnAdd = document.getElementById('btnAdd');
btnAdd.addEventListener('click', function clickCallback(e) {
// do something useless
});
在本例中,我們等待id為btnAdd的元素中的click事件,如果它被單擊,則執(zhí)行clickCallback函數(shù)?;卣{(diào)函數(shù)向某些數(shù)據(jù)或事件添加一些功能。
回調(diào)函數(shù)有一個(gè)致命的弱點(diǎn),就是容易寫出回調(diào)地獄(Callback hell)。假設(shè)多個(gè)事件存在依賴性:
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
},3000)
},2000)
},1000)
這就是典型的回調(diào)地獄,以上代碼看起來不利于閱讀和維護(hù),事件一旦多起來就更是亂糟糟,所以在es6中提出了Promise和async/await來解決回調(diào)地獄的問題。當(dāng)然,回調(diào)函數(shù)還存在著別的幾個(gè)缺點(diǎn),比如不能使用 try catch 捕獲錯(cuò)誤,不能直接 return。
2.Promise是什么,可以手寫實(shí)現(xiàn)一下嗎
Promise,翻譯過來是承諾,承諾它過一段時(shí)間會(huì)給你一個(gè)結(jié)果。從編程講Promise 是異步編程的一種解決方案。下面是Promise在MDN的相關(guān)說明:
Promise 對(duì)象是一個(gè)代理對(duì)象(代理一個(gè)值),被代理的值在Promise對(duì)象創(chuàng)建時(shí)可能是未知的。它允許你為異步操作的成功和失敗分別綁定相應(yīng)的處理方法(handlers)。這讓異步方法可以像同步方法那樣返回值,但并不是立即返回最終執(zhí)行結(jié)果,而是一個(gè)能代表未來出現(xiàn)的結(jié)果的promise對(duì)象。
一個(gè) Promise有以下幾種狀態(tài):
pending: 初始狀態(tài),既不是成功,也不是失敗狀態(tài)。
fulfilled: 意味著操作成功完成。
rejected: 意味著操作失敗。
這個(gè)承諾一旦從等待狀態(tài)變成為其他狀態(tài)就永遠(yuǎn)不能更改狀態(tài)了,也就是說一旦狀態(tài)變?yōu)?fulfilled/rejected 后,就不能再次改變??赡芄饪锤拍畲蠹也焕斫釶romise,我們舉個(gè)簡(jiǎn)單的栗子;
假如我有個(gè)女朋友,下周一是她生日,我答應(yīng)她生日給她一個(gè)驚喜,那么從現(xiàn)在開始這個(gè)承諾就進(jìn)入等待狀態(tài),等待下周一的到來,然后狀態(tài)改變。如果下周一我如約給了女朋友驚喜,那么這個(gè)承諾的狀態(tài)就會(huì)由pending切換為fulfilled,表示承諾成功兌現(xiàn),一旦是這個(gè)結(jié)果了,就不會(huì)再有其他結(jié)果,即狀態(tài)不會(huì)在發(fā)生改變;反之如果當(dāng)天我因?yàn)楣ぷ魈影?,把這事給忘了,說好的驚喜沒有兌現(xiàn),狀態(tài)就會(huì)由pending切換為rejected,時(shí)間不可倒流,所以狀態(tài)也不能再發(fā)生變化。
上一條我們說過Promise可以解決回調(diào)地獄的問題,沒錯(cuò),pending 狀態(tài)的 Promise 對(duì)象會(huì)觸發(fā) fulfilled/rejected 狀態(tài),一旦狀態(tài)改變,Promise 對(duì)象的 then 方法就會(huì)被調(diào)用;否則就會(huì)觸發(fā) catch。我們將上一條回調(diào)地獄的代碼改寫一下:
new Promise((resolve,reject) => {
setTimeout(() => {
console.log(1)
resolve()
},1000)
}).then((res) => {
setTimeout(() => {
console.log(2)
},2000)
}).then((res) => {
setTimeout(() => {
console.log(3)
},3000)
}).catch((err) => {
console.log(err)
})
其實(shí)Promise也是存在一些缺點(diǎn)的,比如無法取消 Promise,錯(cuò)誤需要通過回調(diào)函數(shù)捕獲。
3.手寫簡(jiǎn)單版的Promise
promise手寫實(shí)現(xiàn),面試夠用版:
function myPromise(executor) {
var _this = this;
this.onFulfilled = []; //成功的回調(diào)
this.onRejected = []; //失敗的回調(diào)
this.state = "PENDING"; //狀態(tài)
this.value = undefined; //成功結(jié)果
this.reason = undefined; //失敗原因
function resolve(value) {
if (_this.state === "PENDING") {
_this.state = "FULFILLED";
_this.value = value;
_this.onFulfilled.forEach((fn) => fn(value));
}
}
function reject(reason) {
if (_this.state === "PENDING") {
_this.state = "REJECTED";
_this.reason = reason;
_this.onRejected.forEach((fn) => fn(reason));
}
}
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
// 定義鏈?zhǔn)秸{(diào)用的then方法
myPromise.prototype.then = function (onFullfilled, onRejected) {
if (this.state === "FULFILLED") {
typeof onFulfilled === "function" && onFulfilled(this.value);
}
if (this.state === "REJECTED") {
typeof onRejected === "function" && onRejected(this.reason);
}
if (this.state === "PENDING") {
typeof onFulfilled === "function" &&
this.onFulfilled.push(onFulfilled);
typeof onRejected === "function" && this.onRejected.push(onRejected);
}
};
var myP = new myPromise((resolve, reject) => {
console.log("執(zhí)行");
setTimeout(() => {
reject(3);
}, 1000);
});
myP.then(
(res) => {
console.log(res);
},
(err) => {
console.log(err);
}
);
4.then,catch,finally
then 方法 的 第一個(gè)參數(shù)是 resolved 狀態(tài)對(duì)應(yīng)的回調(diào)函數(shù), 第二個(gè)參數(shù)(可選) 是 rejected 狀態(tài)對(duì)應(yīng)的回調(diào)函數(shù),then方法返回的是一個(gè)新的 Promise 實(shí)例.(注意, 不是原來那個(gè)Promise實(shí)例), 因此可以采用鏈?zhǔn)綄懛? 即 then 方法后面再調(diào)用另一個(gè) then 方法.
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
Promise.prototype.catch() 方法是 `.then(null,rejection) 或.then(undefined,rejection)的別名, 用于指定發(fā)生錯(cuò)誤時(shí)的回調(diào)函數(shù).
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 處理 getJSON 和 前一個(gè)回調(diào)函數(shù)運(yùn)行時(shí)發(fā)生的錯(cuò)誤
console.log('發(fā)生錯(cuò)誤!', error);
});
上面代碼中,getJSON()方法返回一個(gè) Promise 對(duì)象,如果該對(duì)象狀態(tài)變?yōu)閞esolved,則會(huì)調(diào)用then()方法指定的回調(diào)函數(shù);如果異步操作拋出錯(cuò)誤,狀態(tài)就會(huì)變?yōu)閞ejected,就會(huì)調(diào)用catch()方法指定的回調(diào)函數(shù),處理這個(gè)錯(cuò)誤。另外,then()方法指定的回調(diào)函數(shù),如果運(yùn)行中拋出錯(cuò)誤,也會(huì)被catch()方法捕獲。
finally()方法用于指定不管 Promise 對(duì)象最后狀態(tài)如何,都會(huì)執(zhí)行的操作。該方法是 ES2018 引入標(biāo)準(zhǔn)的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代碼中,不管promise最后的狀態(tài),在執(zhí)行完then或catch指定的回調(diào)函數(shù)以后,都會(huì)執(zhí)行finally方法指定的回調(diào)函數(shù)。
finally方法的回調(diào)函數(shù)不接受任何參數(shù),這意味著沒有辦法知道,前面的 Promise 狀態(tài)到底是fulfilled還是rejected。這表明,finally方法里面的操作,應(yīng)該是與狀態(tài)無關(guān)的,不依賴于 Promise 的執(zhí)行結(jié)果。
5.手寫promise.all
promise.all方法:當(dāng)參數(shù)中的promise有一個(gè)失敗了就直接返回失敗的結(jié)果,返回第一個(gè)失敗的結(jié)果,都成功返回所有的參數(shù)結(jié)果
思路:
返回一個(gè)新的promise,并遍歷調(diào)用傳入的promise組成的數(shù)組參數(shù),利用Promise.resolve()方法獲取成功執(zhí)行的所有promise的數(shù)量和數(shù)組參數(shù)的長(zhǎng)度進(jìn)行比對(duì),如果長(zhǎng)度小于參數(shù),則代表有失敗的結(jié)果,否則就是全部成功。
function promiseAll(promises){
// 返回一個(gè)promise實(shí)例
return new Promise((resolve, reject) => {
// 做一個(gè)判斷參數(shù)是否是數(shù)組
if(!Array.isArray(promises)){
return reject(new TypeError('arguments must be Array'))
}
let count = 0,
newValues = new Array(promise.length) // 接收新的結(jié)果參數(shù) 建立一個(gè)偽數(shù)組
for(let i = 0; i < promises.length; i++){
// 運(yùn)用promise特性 只會(huì)有一個(gè)狀態(tài)
Promise.resolve(promises[i])
.then(res = > {
count++
newValues[i] = res // 把每次返回成功的數(shù)據(jù)添加到數(shù)組中
if(count === promises.length){ // 數(shù)據(jù)接收完成
resolve(newValues)
}
}, rej = > reject(rej))
}
})
}
成功的時(shí)候返回的是一個(gè)結(jié)果數(shù)組,而失敗的時(shí)候則返回最先被reject失敗狀態(tài)的值。,需要注意的是,Promise.all獲得的成功結(jié)果的數(shù)組里面的數(shù)據(jù)順序和Promise.all接收到的數(shù)組順序是一致的。
6.手寫promise.race
Promse.race就是賽跑的意思,意思就是說,Promise.race([p1, p2, p3])里面哪個(gè)結(jié)果獲得的快,就返回那個(gè)結(jié)果,不管結(jié)果本身是成功狀態(tài)還是失敗狀態(tài)。
function promiseRace(arrays){
if(!Array.isArray(arrays))
return 'not Array'
return new Promise((resolve,reject)=>{
for(var i = 0; i < arrays.length; i++){
Promise.resolve(arrays[i]).then((value)=>{
resolve(value)
}, (err)=>{reject(err)})
}
})
}
7.Promise里都是微任務(wù)嗎
Promise 中只有涉及到狀態(tài)變更后才需要被執(zhí)行的回調(diào)才算是微任務(wù),比如說 then、 catch 、finally ,其他所有的代碼執(zhí)行都是宏任務(wù)(同步執(zhí)行)。
8.async await 和 promise 的區(qū)別與聯(lián)系
async await 和 promise 都是異步編程解決方案。
Promise的寫法只是回調(diào)函數(shù)的改進(jìn),使用then方法,只是讓異步任務(wù)的兩段執(zhí)行更清楚而已。Promise的最大問題是代碼冗余,請(qǐng)求任務(wù)多時(shí),一堆的then,也使得原來的語義變得很不清楚。
async搭配await是ES7提出的,它的實(shí)現(xiàn)是基于Promise,通過同步方式的寫法,使得代碼更容易閱讀。
async/await的優(yōu)勢(shì)在于處理 then 的調(diào)用鏈,能夠更清晰準(zhǔn)確的寫出代碼,并且也能優(yōu)雅地解決回調(diào)地獄問題。當(dāng)然也存在一些缺點(diǎn),因?yàn)?await 將異步代碼改造成了同步代碼,如果多個(gè)異步代碼沒有依賴性卻使用了 await 會(huì)導(dǎo)致性能上的降低。
9.Promise中用了什么設(shè)計(jì)模式
觀察者模式 在軟件設(shè)計(jì)中是一個(gè)對(duì)象,維護(hù)一個(gè)依賴列表,當(dāng)任何狀態(tài)發(fā)生改變自動(dòng)通知它們。
發(fā)布-訂閱模式是一種消息傳遞模式,消息的發(fā)布者(Publishers)一般將消息發(fā)布到特定消息中心,訂閱者(Subscriber)可以按照自己的需求從消息中心訂閱信息,跟消息隊(duì)列挺類似的。
觀察者設(shè)計(jì)模式的代碼實(shí)現(xiàn)
// 觀察者設(shè)計(jì)模式
class Observer {
constructor () {
this.observerList = [];
}
subscribe (observer) {
this.observerList.push(observer)
}
notifyAll (value) {
this.observerList.forEach(observe => observe(value))
}
}
發(fā)布-訂閱設(shè)計(jì)模式的代碼實(shí)現(xiàn)
// 發(fā)布訂閱
class EventEmitter {
constructor () {
this.eventChannel = {}; // 消息中心
}
// subscribe
on (event, callback) {
this.eventChannel[event] ? this.eventChannel[event].push(callback) : this.eventChannel[event] = [callback]
}
// publish
emit (event, ...args) {
this.eventChannel[event] && this.eventChannel[event].forEach(callback => callback(...args))
}
// remove event
remove (event) {
if (this.eventChannel[event]) {
delete this.eventChannel[event]
}
}
// once event
once (event, callback) {
this.on(event, (...args) => {
callback(...args);
this.remove(event)
})
}
}
從代碼中也能看出他們的區(qū)別,觀察者模式不對(duì)事件進(jìn)行分類,當(dāng)有事件時(shí),將通知所有觀察者。發(fā)布-訂閱設(shè)計(jì)模式對(duì)事件進(jìn)行了分類,觸發(fā)不同的事件,將通知不同的觀察者。所以可以認(rèn)為后者就是前者的一個(gè)升級(jí)版,對(duì)通知事件做了更細(xì)粒度的劃分。
發(fā)布-訂閱和觀察者在異步中的應(yīng)用
觀察者模式
const observer = new Observer();
observer.subscribe(value => {
console.log("第一個(gè)觀察者,接收到的值為:");
console.log(value)
});
observer.subscribe(value => {
console.log("第二個(gè)觀察者,接收到的值為");
console.log(value)
});
fs.readFile("h.js", (err, data) => {
observer.notifyAll(data.toString())
});
發(fā)布-訂閱模式
// 發(fā)布-訂閱
const event = new EventEmitter();
event.on("err", console.log);
event.on("data", data => {
// do something
console.log(data)
});
fs.readFile("h.js", (err, data) => {
if (err) event.emit("err", err);
event.emit("data", data.toString())
});
兩種設(shè)計(jì)模式在異步編程中,都是通過注冊(cè)全局觀察者或全局事件,然后在異步環(huán)境里通知所有觀察者或觸發(fā)特定事件來實(shí)現(xiàn)異步編程。
劣勢(shì)也很明顯,比如全局觀察者/事件過多難以維護(hù),事件名命沖突等等,因此Promise便誕生了,Promise在一定程度上繼承了觀察者和發(fā)布-訂閱設(shè)計(jì)模式的思想。