promise
是幹什麼的
在JavaScript的世界中,所有程式碼都是單執行緒執行的。由於這個“缺陷”,導致JavaScript的所有網路操作,瀏覽器事件,都必須是非同步執行。非同步執行可以用回撥函式實現,然而在需要多次回撥巢狀的時候,就容易進入回撥地獄
了,promise
解決了這一問題
Promise
是非同步程式設計的一種解決方案,比傳統的解決方案–回撥函式和事件--更合理和更強大。它由社群最早提出和實現,ES6將其寫進了語言標準,統一了語法,原生提供了Promise
。所謂Promise
,簡單說就是一個容器,裡面儲存著某個未來才回結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise
是一個物件,從它可以獲取非同步操作的訊息。 Promise
物件的狀態不受外界影響
promise
是什麼樣的
promise
物件上的方法
Promise.all()
Promise.race()
Promise.reject()
Promise.resolve()
Promise.prototype.catch()
Promise.prototype.finally()
Promise.prototype.then()
複製程式碼
- 三種狀態:
promise
只有三種狀態
pending 進行中
fulfilled 已經成功
rejected 已經失敗
複製程式碼
- 狀態改變:
Promise
物件的狀態改變,只有兩種可能:
pending => fulfilled
pending => rejected
複製程式碼
基本用法
ES6規定,Promise物件是一個建構函式,用來生成Promise例項
const p = new Promise(function(resolve,reject){
if(/*非同步操作成功*/){
resolve(value);
}else{
reject(error);
}
})
複製程式碼
resolve
函式的作用是,將Promise物件的狀態從“未完成”變為“成功”(即從 pending
變為 resolved
),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;
reject
函式的作用是,將Promise物件的狀態從“未完成”變為“失敗”(即從 pending
變為 rejected
),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
Promise
例項生成以後,可以用then 方法分別指定resolved
狀態和rejected
狀態的回撥函式。
p.then(function(value){
//success
},function(error){
//failure
});
複製程式碼
原理解析
針對以上的基本用法,我們深入解析一下Promise
的實現原理:
首先promsie
有三種狀態,它是個建構函式,且接收一個函式作為引數,這個函式我們稱之為excutor
,函式裡有兩個引數resolve和reject
,這兩個回掉的作用我們在基本用法裡已經說了
跟jQuery鏈式呼叫相似,promise
的鏈式呼叫是在 Promise.prototype.then()
和 Promise.prototype.catch()
裡返回了promise
例項
這張圖清楚的描述了promise
鏈式呼叫的過程
- 建構函式
const PENDING = `pengding`; // 初始態
const FULFILLED = `fulfilled`;// 成功態
const REJECTED = `rejected`;// 成功態
function Promise(excutor) {
let self = this;
self.status = PENDING; // 設定狀態
// 定義存放成功回撥的陣列
self.onResolveCallbacks = [];
// 定義存放失敗回撥的陣列
self.onRejectCallbacks = [];
// 當呼叫此方法的時候,如果promise狀態為pending時可以以轉成成功態,如果已經是成功或者失敗則什麼都不做
function resolve(value) {
if (self.status == PENDING){ // 如果是初始態則轉成成功態
self.status = FULFILLED;
self.value = value; // 成功後會得到一個值且不能改變
self.onResolveCallbacks.forEach(cb => cb(self.value)) // 呼叫所有成功的回撥
}
}
function reject(reason) {
if(self.status == PENDING){ // 如果是初始態則轉成失敗態
self.status = REJECTED;
self.value = reason;
self.onRejectCallbacks.forEach(cb=>cb(self.value))
}
}
try{
// excutor函式執行可能會異常,需要捕獲,如果出錯需要用這個錯誤物件reject
excutor(resolve,reject)
}catch (e) {
// excutor執行失敗需要用失敗原因reject這個promise
reject(e)
}
}
複製程式碼
then
方法
在基本用法中我們知道在呼叫then
方法時傳進去的是兩個函式,分別對應的是promise
中非同步方法成功和失敗時的回撥
Promise.prototype.then = function (onFulfilled, onRejected) {
// 如果成功和失敗的回撥沒傳,表示這個then沒任何邏輯,只會把值往後拋
onFulfilled = typeof onFulfilled == `function` ? onFulfilled : value=>value;
onRejected = typeof onRejected == `function` ? onRejected : reason=>{ throw reason};
let self = this;
let promise2;
// 為了實現鏈式呼叫,每一種狀態都要返回的是一個promise例項
if(self.status == FULFILLED){ //如果promise狀態已經是成功態了,onFulfilled直接取值
return promise2 = new Promise(function (resolve,reject) {
setTimeout(function () { // 保證返回的promise是非同步
try{
onFulfilled(self.value);
}catch (e) {
// 如果執行成功的回撥過程中出錯,用錯誤原因把promise2 reject
reject(e)
}
})
});
}
if(self.status == REJECTED){
return promise2 = new Promise(function (resolve, reject) {
setTimeout(function () {
try{
onRejected(self.value);
}catch(e){
reject(e)
}
})
})
}
if(self.status == PENDING){
return promise2 = new Promise(function (resolve, reject){
// pending狀態時把所有的回撥函式都新增到例項的兩個堆疊中暫存,等狀態改變後依次執行,其實這個過程就是觀察者模式
self.onResolveCallbacks.push(function () {
setTimeout(function () {
try{
onFulfilled(self.value);
}catch(e){
reject(e)
}
})
});
self.onRejectCallbacks.push(function () {
setTimeout(function () {
try{
onRejected(self.value);
}catch(e){
reject(e)
}
})
})
});
}
};
複製程式碼
以上只是支援了一次then
的呼叫,而現實中我們會有這種需求
const p = new Promise(function(resolve,reject){
if(/*非同步操作成功*/){
resolve(value);
}else{
reject(error);
}
});
p.then(function(){
//success
}).then(function(){
//success
}).then(function(){
//success
}).catch(function(e){
//failure
})
複製程式碼
這種連續鏈式呼叫then
方法,連續返回promise
例項的情況,而且我們要相容then
方法裡返回的不是promise
物件,這要求對then
優化,加入一個解析promise
的方法resolvePromise
。這樣我們就會遇到三種情況:
-
返回值是
promise
例項 -
返回值是個
thenable
物件或者函式 -
沒有返回值或者只返回一個普通值
先看一下什麼是
thenable
物件
const thenable = {
// 所謂 thenable 物件,就是具有 then 屬性,而且屬性值是如下格式函式的物件
then: (resolve, reject) => {
resolve(200)
}
}
複製程式碼
//resolvePromise
function resolvePromise(promise2,x,resolve,reject){
if(promise2 === x){
return reject(new TypeError(`迴圈引用`))
}
let called = false;// 是否resolve或reject被呼叫,這兩個回撥只能被呼叫一次
if(x instanceof Promise){
if(x.status === PENDING){
x.then(function (y) { // 深度遞迴
resolvePromise(promise2,y,resolve,reject)
},reject)
}else{
x.then(resolve,reject)
}
}else if(x != null && ((typeof x ==`object`) || (typeof x == `function`))){
// x 是個thenable物件或函式
try{
let then = x.then;
if(typeof then == `function`){
then.call(x,function (y) {
// 如果promise2已經成功或者失敗了就不要再呼叫了
if(called) return;
called = true;
resolvePromise(promise2,y,resolve,reject)
},function (err) {
if(called) return;
called = true;
reject(err)
})
}else{
// 到此的話x不是個thenabe物件,直接把它當成值 resolve promise2就可以了
resolve(x)
}
}catch (e) {
if(called) return;
called = true;
reject(e)
}
}else{
// 如果x是個普通值,則用x的值去resolve promise2
resolve(x)
}
}
複製程式碼
優化後的then
方法
Promise.prototype.then = function (onFulfilled, onRejected) {
// 如果成功和失敗的回撥沒傳,表示這個then沒任何邏輯,只會把值往後拋
onFulfilled = typeof onFulfilled == `function` ? onFulfilled : value=>value;
onRejected = typeof onRejected == `function` ? onRejected : reason=>{ throw reason};
let self = this;
let promise2;
if(self.status == FULFILLED){ //如果promise狀態已經是成功態了,onFulfilled直接取值
return promise2 = new Promise(function (resolve,reject) {
setTimeout(function () {
try{
let x = onFulfilled(self.value);
if(x instanceof Promise){
// 如果獲取到返回值X,會走解析promise的過程
resolvePromise(promise2,x,resolve,reject)
}
}catch (e) {
// 如果執行成功的回撥過程中出錯,用錯誤原因把promise2 reject
reject(e)
}
})
});
}
if(self.status == REJECTED){
return promise2 = new Promise(function (resolve, reject) {
setTimeout(function () {
try{
let x = onRejected(self.value);
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
})
})
}
if(self.status == PENDING){
return promise2 = new Promise(function (resolve, reject){
self.onResolveCallbacks.push(function () {
setTimeout(function () {
try{
let x = onFulfilled(self.value);
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
})
});
self.onRejectCallbacks.push(function () {
setTimeout(function () {
try{
let x = onRejected(self.value);
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
})
})
});
}
};
複製程式碼
catch
方法
catch
原理就是隻傳失敗的回撥
Promise.prototype.catch = function(onReject){
this.then(null,onReject);
};
複製程式碼
- 最後匯出這個類就可以了
try {
module.exports = Promise
} catch (e) {
console.log(e)
}
複製程式碼
實際專案中的應用—封裝一個非同步載入圖片的方法
function imgLoad(url) {
return new Promise(function(resolve, reject) {
var request = new XMLHttpRequest();
request.open(`GET`, url);
request.responseType = `blob`;
request.onload = function() {
if (request.status === 200) {
resolve(request.response);
} else {
reject(Error(`Image didn`t load successfully; error code:` + request.statusText));
}
};
request.onerror = function() {
reject(Error(`There was a network error.`));
};
request.send();
});
}
var body = document.querySelector(`body`);
var myImage = new Image();
imgLoad(`XXX.jpg`).then(function(response) {
var imageURL = window.URL.createObjectURL(response);
myImage.src = imageURL;
body.appendChild(myImage);
}, function(Error) {
console.log(Error);
});
複製程式碼