1. Promise簡介 promise是非同步編程的一種解決方案,它出現的初衷是為瞭解決回調地獄的問題。 打個比方,我需要: --(延遲1s)--> 輸出1 --(延遲2s)--> 輸出2 --(延遲3s)--> 輸出3 通常寫法: setTimeout(()=> { console.log('1' ...
1. Promise簡介
promise是非同步編程的一種解決方案,它出現的初衷是為瞭解決回調地獄的問題。
打個比方,我需要:
--(延遲1s)--> 輸出1 --(延遲2s)--> 輸出2 --(延遲3s)--> 輸出3
通常寫法:
setTimeout(()=> {
console.log('1');
setTimeout(()=> {
console.log('2');
setTimeout(()=> {
console.log('3');
}, 3000)
}, 2000)
}, 1000)
這樣的多重的嵌套的回調被稱為回調地獄,這樣的代碼可讀性很差,不利於理解。
如果用promise的話畫風一轉
function delay(time, num) {
return new Promise((res, rej)=> {
setTimeout(()=> {
console.log(num);
res();
}, time*1000)
});
}
delay(1, 1).then(()=> {
return delay(2, 2);
}).then(()=> {
delay(3, 3);
})
使用了promise的鏈式調用,代碼結構更清晰。
是不是很棒?那還不趕快get起來~
2. Promise的使用
調用方式如下:
new Promise((resolve, reject)=> {
if('some option') {
resolve('some value');
} else {
reject('some error');
}
}).then(
val=> {
// ...
},
error=> {
// ...
}
)
Promise構造函數接收一個函數型參數fn,fn有兩個參數,分別是:resolve、reject,Promise還有一個Promise.prototype.then方法,該方法接收兩個參數,分別是成功的回調函數succ和失敗的回調函數error。
在fn中調用resolve會觸發then中的succ回調,調用reject會觸發error回調。
2.1 參數傳遞
- 在fn內部調用resolve/reject傳入的參數會作為相應參數傳入相應的回調函數
new Promise((res, rej)=> { res('happy') }).then(val=> { console.log(val); // happy }); new Promise((res, rej)=> { rej('error!'); }).then(val=> {}, err=> { console.log(err); // error! });
- 鏈式調用時若上一級沒有傳遞值則預設為undefined
new Promise((res, rej)=> { res('a'); }).then(val=> { return 'b' }).then(val=> { console.log(val); // 'b' }).then((val)=> { console.log(val); // 'undefined' });
- 若上一級的then中傳遞的並非函數,則忽略該級
new Promise((res, rej)=> { res('a'); }).then(val=> { return 'b'; }).then(val=> { console.log(val); // 'b' return 'c'; }).then({ // 並非函數 name: 'lan' }).then((val)=> { console.log(val); // 'c' });
2.2 參數傳遞例題
let doSomething = function() {
return new Promise((resolve, reject) => {
resolve('返回值');
});
};
let doSomethingElse = function() {
return '新的值';
}
doSomething().then(function () {
return doSomethingElse();
}).then(resp => {
console.warn(resp);
console.warn('1 =========<');
});
doSomething().then(function () {
doSomethingElse();
}).then(resp => {
console.warn(resp);
console.warn('2 =========<');
});
doSomething().then(doSomethingElse()).then(resp => {
console.warn(resp);
console.warn('3 =========<');
});
doSomething().then(doSomethingElse).then(resp => {
console.warn(resp);
console.warn('4 =========<');
});
結合上面的講解想一想會輸出什麼?(答案及解析)
3. Promise.prototype.then
當Promise中的狀態(pending ---> resolved or rejected)發生變化時才會執行then方法。
- 調用then返回的依舊是一個Promise實例 ( 所以才可以鏈式調用... )
new Promise((res, rej)=> {
res('a');
}).then(val=> {
return 'b';
});
// 等同於
new Promise((res, rej)=> {
res('a');
}).then(val=> {
return new Promise((res, rej)=> {
res('b');
});
});
- then中的回調總會非同步執行
new Promise((res, rej)=> {
console.log('a');
res('');
}).then(()=> {
console.log('b');
});
console.log('c');
// a c b
- 如果你不在Promise的參數函數中調用resolve或者reject那麼then方法永遠不會被觸發
new Promise((res, rej)=> {
console.log('a');
}).then(()=> {
console.log('b');
});
console.log('c');
// a c
4. Promise的靜態方法
Promise還有四個靜態方法,分別是resolve、reject、all、race,下麵我們一一介紹一下。
4.1 Promise.resolve()
除了通過new Promise()的方式,我們還有兩種創建Promise對象的方法,Promise.resolve()相當於創建了一個立即resolve的對象。如下兩段代碼作用相同:
Promise.resolve('a');
new Promise((res, rej)=> {
res('a');
});
當然根據傳入的參數不同,Promise.resolve()也會做出不同的操作。
- 參數是一個 Promise 實例
如果參數是 Promise 實例,那麼Promise.resolve將不做任何修改、原封不動地返回這個實例。
- 參數是一個thenable對象
thenable對象指的是具有then方法的對象,比如下麵這個對象。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
Promise.resolve方法會將這個對象轉為 Promise對象,然後就立即執行thenable對象的then方法。
- 參數不是具有then方法的對象,或根本就不是對象
如果參數是一個原始值,或者是一個不具有then方法的對象,則Promise.resolve方法返回一個新的 Promise 對象,狀態為resolved。
- 不帶有任何參數
Promise.resolve方法允許調用時不帶參數,直接返回一個resolved狀態的 Promise 對象。
值得註意的一點是該靜態方法是在本次事件輪詢結束前調用,而不是在下一次事件輪詢開始時調用。關於事件輪詢可以看這裡——>JavaScript 運行機制詳解:再談Event Loop
4.2 Promise.reject()
和Promise.resolve()類似,只不過一個是觸發成功的回調,一個是觸發失敗的回調
4.3 Promise.all()
Promise的all方法提供了並行執行非同步操作的能力,並且在所有非同步操作執行完後才執行回調。
function asyncFun1() {
return new Promise((res, rej)=> {
setTimeout(()=> {
res('a');
}, 1000);
});
}
function asyncFun2() {
return new Promise((res, rej)=> {
setTimeout(()=> {
res('b');
}, 1000);
});
}
function asyncFun3() {
return new Promise((res, rej)=> {
setTimeout(()=> {
res('c');
}, 1000);
});
}
Promise.all([asyncFun1(), asyncFun2(), asyncFun3()]).then((val)=> {
console.log(val);
});
Promise.all([asyncFun1(), asyncFun2(), asyncFun3()]).then((val)=> {
console.log(val); // ['a', 'b', 'c']
});
用Promise.all來執行,all接收一個數組參數,裡面的值最終都算返回Promise對象。這樣,三個非同步操作的並行執行的,等到它們都執行完後才會進到then裡面。有了all,你就可以並行執行多個非同步操作,並且在一個回調中處理所有的返回數據。
適用場景:打開網頁時,預先載入需要用到的各種資源如圖片、flash以及各種靜態文件。所有的都載入完後,我們再進行頁面的初始化。
4.4 Promise.race()
race()和all相反,all()是數組中所有Promise都執行完畢就執行then,而race()是一旦有一個Promise執行完畢就會執行then(),用上面的三個Promise返回值函數舉例
Promise.race([asyncFun1(), asyncFun2(), asyncFun3()]).then((val)=> {
console.log(val); // a
});
5. 鏈式調用經典例題
看了這麼多關於Promise的知識,我們來做一道題鞏固一下。
寫一個類Man實現以下鏈式調用
調用方式:
new Man('lan').sleep(3).eat('apple').sleep(5).eat('banana');
列印:
'hello, lan' -(等待3s)--> 'lan eat apple' -(等待5s)--> 'lan eat banana'
思路:
- 在原型方法中返回this達到鏈式調用的目的
- 等待3s執行的效果可以用Promise & then實現
具體實現如下:
class Man {
constructor(name) {
this.name = name;
this.sayName();
this.rope = Promise.resolve(); // 定義全局Promise作鏈式調用
}
sayName() {
console.log(`hello, ${this.name}`);
}
sleep(time) {
this.rope = this.rope.then(()=> {
return new Promise((res, rej)=> {
setTimeout(()=> {
res();
}, time*1000);
});
});
return this;
}
eat(food) {
this.rope = this.rope.then(()=> {
console.log(`${this.name} eat ${food}`);
});
return this;
}
}
new Man('lan').sleep(3).eat('apple').sleep(5).eat('banana');
ok!不知道你有沒有看懂呢?如果能完全理解代碼那你的Promise可以通關了,順便來個小思考,下麵這種寫法可以嗎?和上面相比有什麼區別?:
class Man1345 {
constructor(name) {
this.name = name;
this.sayName();
}
sayName() {
console.log(`hello, ${this.name}`);
}
sleep(time) {
this.rope = new Promise((res, rej)=> {
setTimeout(()=> {
res();
}, time*1000);
});
return this;
}
eat(food) {
this.rope = this.rope.then(()=> {
console.log(`${this.name} eat ${food}`);
});
return this;
}
}
new Man('lan').sleep(3).eat('apple').sleep(5).eat('banana');
簡單的說,第二段代碼的執行結果是
'hello, lan' -(等待3s)--> 'lan eat apple' ---> 'lan eat banana'
為什麼會出現這種差別? 因為第二段代碼每一次調用sleep都會new一個新的Promise對象,調用了兩次sleep就new了兩個Promise對象。這兩個對象是非同步並行執行,會造成兩句eat同時顯示。
和以下情況類似
var time1 = setTimeout(()=> {
console.log('a');
}, 1000)
var time2 = setTimeout(()=> {
console.log('b');
}, 1000)
// 同時輸出 a b
抽象一點的講解是:
// 第一段正確的代碼的執行為
var p1 = new Promise().then('停頓3s').then('列印食物').then('停頓5s').then('列印食物');
// 第二段代碼的執行行為,p1、p2非同步並行執行
var p1 = new Promise().then('停頓3s').then('列印食物');
var p2 = new Promise().then('停頓5s').then('列印食物');
總結
Promise的經常用到的地方:
- 擺脫回調地獄
- 多個非同步任務同步
Promise是我們的好幫手,不過還有另一種方法也可以做到,那就是async&await,可以多多瞭解一下。
參考資料