為什么需要引入 Promise
我們都知道web 單線程 有很多異步回調,這短短的一段代碼里面竟然出現(xiàn)了五次回調露戒,這么多的回調會導致代碼的邏輯不連貫畔勤、不線性峦甩,非常不符合人的直覺习寸,這就是異步回調影響到我們的編碼方式乘盖。
//執(zhí)行狀態(tài)
function onResolve(response){console.log(response) }
function onReject(error){console.log(error) }
let xhr = new XMLHttpRequest()
xhr.ontimeout = function(e) { onReject(e)}
xhr.onerror = function(e) { onReject(e) }
xhr.onreadystatechange = function () { onResolve(xhr.response) }
//設置請求類型射赛,請求URL副签,是否同步信息
let URL = 'https://time.geekbang.com'
xhr.open('Get', URL, true);
//設置參數
xhr.timeout = 3000 //設置xhr請求的超時時間
xhr.responseType = "text" //設置響應返回的數據格式
xhr.setRequestHeader("X_TEST","time.geekbang")
//發(fā)出請求
xhr.send();
那么怎么才能 變的更加的線性山析,我們開始封裝異步回調堰燎,只在乎輸入和輸出的結果。
引入promise 是如何解決消滅嵌套調用和多次錯誤處理盖腿?
Promise 如何實現(xiàn)了回調函數的延時綁定爽待?
如何將回調函數 onResolve 的返回值穿透到最外層,擺脫嵌套循環(huán)的翩腐?
Promise 出錯后鸟款,是怎么通過“冒泡”傳遞給最后那個捕獲異常的函數?
Promise 中為什么要引入微任務茂卦?
promise
- 1何什、對象的狀態(tài)不受外界影響。Promise對象代表一個異步操作等龙,有三種狀態(tài):pending(進行中)处渣、fulfilled(已成功)和rejected(已失敗)
- 2蛛砰、一旦狀態(tài)改變罐栈,就不會再變,任何時候都可以得到這個結果泥畅。Promise對象的狀態(tài)改變荠诬,只有兩種可能:從pending變?yōu)閒ulfilled和從pending變?yōu)閞ejected。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
- Promise實例生成以后位仁,可以用then方法分別指定resolved狀態(tài)和rejected狀態(tài)的回調函數柑贞。
- resolved 函數是,promise 對象從pending 變?yōu)?resolved聂抢,在異步操作成功時調用钧嘶,并將異步操作的結果,作為參數傳遞出去琳疏。
- rejected 函數是有决,promise 對象從pending 變?yōu)?rejected,在異步操作失敗時調用空盼,并將異步操作報出的錯誤疮薇,作為參數傳遞出去。
- Promise實例生成以后我注,可以用then方法分別指定resolved狀態(tài)和rejected狀態(tài)的回調函數。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
第一個回調函數是Promise對象的狀態(tài)變?yōu)閞esolved時調用迟隅,
第二個回調函數是Promise對象的狀態(tài)變?yōu)閞ejected時調用但骨。
其中励七,第二個函數是可選的,不一定要提供奔缠。
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
// promise 是立即執(zhí)行的掠抬,promise狀態(tài)進入resolve 狀態(tài),
//則直接將回調放入微任務隊列中校哎,執(zhí)行then 方法是同步的两波,
//但是then 中的回調是異步的
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
// 調用resolve或reject并不會終結 Promise 的參數函數的執(zhí)行。
//調用resolve(1)以后闷哆,后面的console.log(2)還是會執(zhí)行腰奋,并且會首先打印出來。
Promise.prototype.then()
Promise 實例具有then方法抱怔,也就是說劣坊,then方法是定義在原型對象。
- then方法的第一個參數是resolved狀態(tài)的回調函數屈留,
- 第二個參數(可選)是rejected狀態(tài)的回調函數局冰。
then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)
Promise.prototype.catch()
Promise.prototype.catch方法是.then(null, rejection)或.then(undefined, rejection)的別名灌危,用于指定發(fā)生錯誤時的回調函數康二。
const promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
console.log(error);
});
// Error: test
Promise 對象的錯誤具有“冒泡”性質,會一直向后傳遞勇蝙,直到被捕獲為止沫勿。也就是說,錯誤總是會被下一個catch語句捕獲浅蚪。
- catch 返回的仍然是一個promise藕帜,后面還可以調用then ,如果catch 中間不拋出錯誤惜傲,就直接跳過洽故。
Promise.prototype.finally()
不管promise最后的狀態(tài),在執(zhí)行完then或catch指定的回調函數以后盗誊,都會執(zhí)行finally方法指定的回調函數
finally方法的回調函數不接受任何參數时甚,這意味著沒有辦法知道,前面的 Promise 狀態(tài)到底是fulfilled還是rejected哈踱。這表明荒适,finally方法里面的操作,應該是與狀態(tài)無關的开镣,不依賴于 Promise 的執(zhí)行結果刀诬。
Promise.all()
Promise.all()方法用于將多個 Promise 實例,包裝成一個新的 Promise 實例邪财。
const p = Promise.all([p1, p2, p3]);
只有p1陕壹、p2质欲、p3的狀態(tài)都變成fulfilled,p的狀態(tài)才會變成fulfilled糠馆,此時p1嘶伟、p2、p3的返回值組成一個數組又碌,傳遞給p的回調函數九昧。
只要p1、p2毕匀、p3之中有一個被rejected铸鹰,p的狀態(tài)就變成rejected,此時第一個被reject的實例的返回值期揪,會傳遞給p的回調函數掉奄。
實現(xiàn)一個promise.all
有時候面試會遇到這樣的問題
首先我們要知道 promise.all 返回的是一個promise 實例
如果傳入的參數中的 promise 都變成完成狀態(tài),Promise.all 返回的 promise 異步地變?yōu)橥瓿伞?/p>
如果傳入的參數中凤薛,有一個 promise 失敗姓建,Promise.all 異步地將失敗的那個結果給失敗狀態(tài)的回調函數,而不管其它 promise 是否完成
在任何情況下缤苫,Promise.all 返回的 promise 的完成狀態(tài)的結果都是一個數組
Promise.all = function (promises){
return new Promise((resolve,reject) => {
// 將迭代對象轉化為數組
promises = Array.from(promises)
if(promises.length === 0){
resolve([])
}else{
let result = [];
let index = 0;
for( let i = 0; i < promises.length; i++){
Promise.resolve(promises[i]).then(data=>{
result[i] = data;
if(++index === promises.length){
resolve(result)
}
},err =>{
reject(err)
return
})
}
}
})
}
// 封裝 Promise.all方法
Promise.all = function (values) {
return new Promise((resolve, reject) => {
let result = []; // 存放返回值
let counter = 0; // 計數器速兔,用于判斷異步完成
function processData(key, value) {
result[key] = value;
// 每成功一次計數器就會加1,直到所有都成功的時候會與values長度一致活玲,則認定為都成功了涣狗,所以能避免異步問題
if (++counter === values.length) {
resolve(result);
}
}
// 遍歷 數組中的每一項,判斷傳入的是否是promise
for (let i = 0; i < values.length; i++) {
let current = values[i];
// 如果是promise則調用獲取data值舒憾,然后再處理data
if (isPromise(current)) {
current.then(data => {
processData(i, data);
}, reject);
} else {
// 如果不是promise镀钓,傳入的是普通值,則直接返回
processData(i, current);
}
}
});
}
promise 核心原理解析:
Promise函數參數可以作為輸入信息镀迂,而后經過Promise的內部處理
// 實例化 Promise
new Promise((resolve, reject)=> {
// 輸入
AjaxRequest.post({
url: 'url',
data: {},
sueccess: ()=> {
// resolve
resolve(res)
},
fail: (err)=> {
// reject
reject(err)
}
})
}).then((res)=> {
// res 輸出
// ...操作
}).catch((err)=> {
// err 輸出
// ...操作
})
內部進行了哪些操作呢丁溅?
pending狀態(tài)下會運行的函數
1、實例化構造函數
// 首先運行探遵,Promise構造函數
function Promise(fn) {
if (typeof this !== 'object') {
throw new TypeError('Promises must be constructed via new');
}
if (typeof fn !== 'function') {
throw new TypeError('Promise constructor\'s argument is not a function');
}
// _deferreds的類型窟赏,1是 single,2是 array
this._deferredState = 0;
// 0 - pending
// 1 - fulfilled(resolved)
// 2 - rejected
// 3 - 另一個Promise的狀態(tài)
this._state = 0;
// promise 執(zhí)行結果
this._value = null;
// then注冊回調數組
this._deferreds = null;
// fn等于noop 即return
if (fn === noop) return;
// 接受Promise回調函數 和 this 作為參數
doResolve(fn, this);
}
Promise構造函數箱季,會初始化屬性涯穷,fn就是我們傳入的函數。
doResolve(fn, this);藏雏,this指向它自己拷况,負責執(zhí)行fn函數。
等下面的then函數和catch函數的回調函數注冊完之后,doResolve函數將立即執(zhí)行
2赚瘦、then 方法注冊回調函數
Promise.prototype.then = function(onFulfilled, onRejected) {
if (this.constructor !== Promise) {
// safeThen函數也是通過調用handle函數最疆,return 新的Promise對象
return safeThen(this, onFulfilled, onRejected);
}
// 生成新的Promise對象
var res = new Promise(noop);
handle(this, new Handler(onFulfilled, onRejected, res));
return res;
};
function safeThen(self, onFulfilled, onRejected) {
return new self.constructor(function (resolve, reject) {
var res = new Promise(noop);
res.then(resolve, reject);
handle(self, new Handler(onFulfilled, onRejected, res));
});
}
// Handler構造函數
// 它的作用是掛載 then中的回調函數 和 一個空的Promise對象
function Handler(onFulfilled, onRejected, promise){
// then中的Fulfilled回調函數
this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
// then中的Rejected回調函數
this.onRejected = typeof onRejected === 'function' ? onRejected : null;
// 保存新的Promise
this.promise = promise;
}
// 保存then注冊回調函數,更新回調函數狀態(tài)
function handle(self, deferred) {
while (self._state === 3) {
self = self._value;
}
if (Promise._onHandle) {
Promise._onHandle(self);
}
// pedding 狀態(tài)
if (self._state === 0) {
// deferred == new Handler(onFulfilled, onRejected, res)
if (self._deferredState === 0) {
self._deferredState = 1;
// 存儲then回調deferred對象
self._deferreds = deferred;
return;
}
if (self._deferredState === 1) {
self._deferredState = 2;
// 存儲then回調deferred對象
self._deferreds = [self._deferreds, deferred];
return;
}
// 存儲then回調函數對象
self._deferreds.push(deferred);
return;
}
// 只有當進入到非pedding狀態(tài)蚤告,handleResolved才會運行
handleResolved(self, deferred);
}
Handler函數生成一個deffer對象,用于保存then函數中的onFulfilled和onRejected回調.以及返回的新的promise實例服爷。
then方法中的核心函數就是handle函數杜恰,它負責接收this和new Handler對象。
若在pedding狀態(tài)下仍源,handle函數只負責注冊回調函數心褐,更新回調函數狀態(tài)。在非pedding狀態(tài)下笼踩,則會執(zhí)行handleResolved函數逗爹。
3、 catch方法注冊回調函數
Promise.prototype['catch'] = function (onRejected) {
return this.then(null, onRejected);
};
// catch方法的回調函數實際是通過then方法來完成保存的嚎于。
4掘而、調用doResolve函數執(zhí)行fn
// 調用doResolve函數
function doResolve(fn, promise) {
var done = false;
// tryCallTwo函數執(zhí)行 類似于
// (resolve, reject) => {if(err){reject(err);return};resolve(res)}執(zhí)行;
var res = tryCallTwo(fn, function (value) {
if (done) return;
done = true;
resolve(promise, value);
}, function (reason) {
if (done) return;
done = true;
reject(promise, reason);
});
// fn函數調用失敗,手動運行reject函數
if (!done && res === IS_ERROR) {
done = true;
reject(promise, LAST_ERROR);
}
}
doResolve是同步直接調用傳入的函數于购。
其中tryCallTwo函數作用是調用函數fn袍睡,它接受三個參數。先執(zhí)行fn函數肋僧,根據結果斑胜,再執(zhí)行resolve函數或reject函數。
在resolve函數或reject函數被調用之前嫌吠,Promise對象的狀態(tài)依然是pending止潘。
進入resolve或reject狀態(tài)時會運行的函數:
- 調用resolve
- 調用finale
- 調用handleResolved函數
調用resolve
若Promise對象的fn函數執(zhí)行正常,之后就會調用resolve函數
function resolve(self, newValue) {
// 辫诅。凭戴。。省略
// newValue存在 & (newValue是一個對象 || newValue是一個函數)
if (
newValue &&
(typeof newValue === 'object' || typeof newValue === 'function')
) {
// 獲取then函數
var then = getThen(newValue);
// 泥栖。簇宽。。省略
if (
then === self.then &&
newValue instanceof Promise
) {
// 如果newValue 是一個Promise對象吧享,那么調用finale函數
self._state = 3;
self._value = newValue;
finale(self);
return;
} else if (typeof then === 'function') {
// 如果newValue 是一個函數魏割,就繼續(xù)調用doResolve函數
doResolve(then.bind(newValue), self);
return;
}
}
// 標記完成,進入結束流程
self._state = 1;
self._value = newValue;
finale(self);
}
確認newValue的值钢颂,如果newValue是一個函數钞它,就繼續(xù)循環(huán)調用doResolve函數;
如果newValue 是一個Promise對象,那么就直接調用finale函數遭垛。
都不是尼桶,則直接調用finale函數。
調用finale函數
function finale(self) {
// 單個回調
if (self._deferredState === 1) {
// 執(zhí)行handle函數锯仪,實際是執(zhí)行handleResolved
handle(self, self._deferreds);
self._deferreds = null;
}
// 回調數組
if (self._deferredState === 2) {
for (var i = 0; i < self._deferreds.length; i++) {
// 執(zhí)行handle函數泵督,實際是執(zhí)行handleResolved
handle(self, self._deferreds[i]);
}
self._deferreds = null;
}
}
finale函數表示進入結束流程,執(zhí)行handle函數庶喜。
同時在上面已經說到小腊,在非pedding狀態(tài)下,執(zhí)行handle函數久窟,實際會是執(zhí)行handleResolved函數秩冈。
調用handleResolved函數
var asap = require('asap/raw');
function handleResolved(self, deferred) {
asap(function() {
var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
// 不存在 onFulfilled & onRejected
// deferred.promise 只是一個空的Promise對象
if (cb === null) {
// 1 - fulfilled(resolved)
if (self._state === 1) {
resolve(deferred.promise, self._value);
} else {
reject(deferred.promise, self._value);
}
return;
}
// 執(zhí)行cb回調函數
var ret = tryCallOne(cb, self._value);
if (ret === IS_ERROR) {
// 錯誤,報reject
reject(deferred.promise, LAST_ERROR);
} else {
resolve(deferred.promise, ret);
}
});
}
通過異步
asap
調用斥扛,若不存在onFulfilled
和onRejected
入问,直接調用resolve
或reject
。若存在稀颁,則tryCallOne
回調的結果芬失,直接調用resolve
或reject
。
Promise 的注冊和執(zhí)行過程
第一道題
new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
})
.then(() => {
console.log("外部第一個then");
return new Promise((resolve, reject) => {
console.log("內部promise");
resolve();
})
.then(() => {
console.log("內部第一個then");
})
.then(() => {
console.log("內部第二個then");
});
})
.then(() => {
console.log("外部第二個then");
});
// 當執(zhí)行 then 方法時峻村,如果前面的 promise 已經是 resolved 狀態(tài)麸折,
// 則直接將回調放入微任務隊列中
output:
外部promise
外部第一個then
內部promise
內部第一個then
內部第二個then
外部第二個then
外部第一個 new Promise 執(zhí)行,執(zhí)行完 resolve ,
所以立即將回調放入微任務隊列
粘昨。所以此時 外部第一個then 放入微任務外部第一個 then 方法里面 return 一個 Promise垢啼,這個 return ,代表 外部的第二個 then 的執(zhí)行需要等待 return 之后的結果
當然會先執(zhí)行完內部兩個 then 之后张肾,再執(zhí)行 外部的第二個 then
如果前面的 promise 已經是 resolved 狀態(tài)芭析,則會立即將回調推入微任務隊列(但是執(zhí)行回調還是要等到所有同步任務都結束后)
如果 then 中的回調返回了一個 promise,那么 then 返回的 promise 會等待這個 promise 被 resolve 后再 resolve
第二道題
new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
})
.then(() => {
console.log("外部第一個then");
new Promise((resolve, reject) => {
console.log("內部promise");
resolve();
})
.then(() => {
console.log("內部第一個then");
})
.then(() => {
console.log("內部第二個then");
});
})
.then(() => {
console.log("外部第二個then");
});
// 比上面的代碼少一個return
output:
外部promise
外部第一個then
內部promise
內部第一個then
外部第二個then
內部第二個then
new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
})
.then(() => {
console.log("外部第一個then");
new Promise((resolve, reject) => {
console.log("內部promise");
resolve();
})
.then(() => {
console.log("內部第一個then");
})
.then(() => {
console.log("內部第二個then");
}) .then(() => {
console.log("內部第3個then");
})
})
.then(() => {
console.log("外部第二個then");
})
.then(() => {
console.log("外部第3個then");
})
外部promise
外部第一個then
內部promise
內部第一個then
外部第二個then
內部第二個then
外部第3個then
內部第3個then
事件執(zhí)行機制: 先注冊后執(zhí)行
*1吞瞪、 當new Promise 執(zhí)行碰到resolve 狀態(tài)確定之后馁启,開始第一個then 的微任務注冊。
2芍秆、外1then 的回調還沒有執(zhí)行惯疙,是pending 狀態(tài)。
所以外2then 的回調也不會被推入微任務隊列也不會執(zhí)行
妖啥。(外部的第二個 then 的注冊霉颠,需要等待 外部的第一個 then 的同步代碼執(zhí)行完成
)3、在實例化時執(zhí)行函數荆虱,打印 log: 內部promise蒿偎,然后執(zhí)行 resolve 函數朽们,接著執(zhí)行到內部的第一個 then(內1then),由于前面的 promise 已被 resolve诉位,所以將回調放入微任務隊列中骑脱。
4、內1then 返回的 promise 是 pending 狀態(tài) 時苍糠,內2then和外2 then 不注冊不執(zhí)行叁丧。
5、外部1then 執(zhí)行完岳瞭,外1then 返回的 promise 的狀態(tài)由 pending 變?yōu)?resolved歹袁。
同時遍歷之前通過 then 給這個 promise 注冊的所有回調,將它們的回調放入微任務隊列中寝优,也就是外2then 的回調
外部的第一個 then 的同步操作已經完成了,然后開始注冊外部的第二個 then枫耳,此時外部的同步任務也都完成了乏矾。
同步操作完成之后,那么開始執(zhí)行微任務:
內部的第一個 then 是優(yōu)先于外部的第二個 then 的注冊迁杨,所以會執(zhí)行完內部的第一個 then 之后钻心;
然后注冊內部的第二個 then ;
然后執(zhí)行外部的第二個 then ;
,然后再執(zhí)行內部的第二個 then铅协。
第三道
如果調用resolve函數和reject函數時帶有參數, 參數會被傳遞給回調函數捷沸,
resolve函數的參數除了正常的值以外,還可能是另一個 Promise 實例
const p1 = new Promise(function (resolve, reject) {
// ...
});
const p2 = new Promise(function (resolve, reject) {
// ...
resolve(p1);
})
這時p1的狀態(tài)就會傳遞給p2狐史,也就是說痒给,p1的狀態(tài)決定了p2的狀態(tài)。
如果p1的狀態(tài)已經是resolved或者rejected骏全,那么p2的回調函數將會立刻執(zhí)行苍柏。
const p111 = new Promise(function (resolve, reject) {
// ...
resolve()
}).then(()=>{
console.log('p1 then')
return 11
});
const p12 = new Promise(function (resolve, reject) {
// ...
resolve(p111);
}).then((p3)=>{
console.log('p2 then',p111,p3)
})
// p1 then > p2 then Promise {<resolved>: 11} 11
改寫函數成promise 形式:
var fs = require("fs");
function myReadFile(){
return new Promise(function(resolve,reject){
fs.readFile("./index.html",function(err,data){
if (!err) {
resolve(data);
}else{
reject(err);
}
});
});
}
myReadFile().then(function(d){
console.log(d.toString());
}).catch();
紅燈3秒亮一次,綠燈1秒亮一次姜贡,黃燈2秒亮一次试吁;如何讓三個燈不斷交替重復亮燈?(用Promise實現(xiàn))三個亮燈函數已經存在:
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
紅燈3秒亮一次楼咳,綠燈1秒亮一次 熄捍,黃燈2秒亮一次,意思就是3秒執(zhí)行一次red函數母怜,2秒執(zhí)行一次green函數余耽,1秒執(zhí)行一次yellow函數,不斷交替重復亮燈糙申,意思就是按照這個順序一直執(zhí)行這3個函數宾添,這步可以利用遞歸來實現(xiàn)船惨。
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
var light = function (timmer, cb) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
cb();
resolve();
}, timmer);
});
};
var step = function () {
Promise.resolve().then(function () {
return light(3000, red);
}).then(function () {
return light(2000, green);
}).then(function () {
return light(1000, yellow);
}).then(function () {
step();
});
}
step();