八. ES6的Promise詳解
1. Promise理解
ES6中一個非常重要和好用的特性就是Promise類。
但是初次接觸Promise會一臉懵逼,這TM是什麼東西?看看官方或者一些文章對它的介紹和用法也是一頭霧水。
Promise到底是做什麼的呢? => Promise是非同步程式設計的一種解決方案。一般情況下有非同步操作時,使用Promise對這個非同步操作進行封裝。
那什麼時候我們會來處理非同步事件呢?一種很常見的場景應該就是 網路請求 了。
我們封裝一個網路請求的函式不能立即拿到結果,所以不能像簡單的 3+4=7 一樣將結果返回。所以我們往往會傳入另外一個函式,在資料請求成功時將資料通過傳入的函式回調出去。如果只是一個簡單的網路請求,那麼這種方案不會給我們帶來很大的麻煩。但是當網路請求非常複雜時就會出現回撥地獄。
OK,我以一個非常誇張的案例來說明。我們來考慮下面的場景(有誇張的成分):
- 我們需要通過一個url1從伺服器載入一個數據data1,data1中包含了下一個請求的url2
- 我們需要通過data1取出url2,從伺服器載入資料data2,data2中包含了下一個請求的url3
- 我們需要通過data2取出url3,從伺服器載入資料data3,data3中包含了下一個請求的url4
- 傳送網路請求url4,獲取最終的資料data4
$.ajax('url1',function(data1) { $.ajax(data1['url2'],function(data2) { $.ajax(data2['url3'],function(data3) { $.ajax(data3['url4'],function(data4) { console.log(data4); }) }) }) })
上面的程式碼有什麼問題嗎?正常情況下不會有什麼問題,可以正常執行並且獲取我們想要的結果。但是這樣額程式碼難看而且不容易維護,我們更加期望的是一種更加優雅的方式來進行這種非同步操作。如何做呢?就是使用Promise,Promise可以以一種非常優雅的方式來解決這個問題。
2. Promise基本使用
2.1 定時器的非同步事件案例
我們先來看看Promise最基本的語法。
這裡我們用一個定時器來模擬非同步事件:假設下面的data是從網路上1秒後請求的資料,console.log就是我們的處理方式。
setTimeout(() => { console.log('Hello World'); },1000)
上面是我們過去的處理方式,我們將它用Promise進行封裝(雖然這個例子會讓我們感覺脫褲放屁多此一舉)
- 首先下面的Promise程式碼明顯比上面的程式碼看起來還要複雜。
- 其次下面的Promise程式碼中包含的resolve、reject、then、catch都是些什麼東西?
- 我們先不管複雜度的問題,因為這樣的一個屁大點的程式根本看不出來Promise真正的作用。
new Promise((resolve,reject) => {
setTimeout(() => {
resolve('Hello World')
//reject('Error Data')
}, 1000)
}).then(data => {
console.log(data) //Hello World
}).catch(error => {
console.log(error) //Error Data
})
//注意:另一種寫法,成功和失敗的訊息都可以寫在then這個回撥函式中
new Promise((resolve,reject) => {
setTimeout(() => {
resolve('Hello World')
//reject('Error Data')
}, 1000)
}).then(data => {
console.log(data) //Hello World
},error => {
console.log(error) //Error Data
})
我們先來認認真真的讀一讀這個程式到底做了什麼?
-
new Promise很明顯是建立一個Promise物件
-
小括號中
(resolve, reject) => {}
也很明顯就是一個函式,而且我們這裡用的是箭頭函式-
但是resolve, reject它們是什麼呢?
-
我們先知道一個事實:在建立Promise時傳入的這個箭頭函式是固定的(一般我們都會這樣寫)
-
resolve 和 reject 它們兩個也是函式,通常情況下我們會根據請求資料的成功和失敗來決定呼叫哪一個。
-
-
成功還是失敗?
- 如果是成功的,那麼通常我們會呼叫
resolve(messsage)
,這個時候我們後續的then會被回撥。 - 如果是失敗的,那麼通常我們會呼叫
reject(error)
,這個時候我們後續的catch會被回撥。
- 如果是成功的,那麼通常我們會呼叫
OK,這就是Promise最基本的使用了。
2.2 Promise三種狀態
首先, 當我們開發中有非同步操作時, 就可以給非同步操作包裝一個Promise
非同步操作之後會有三種狀態
pending
:等待狀態,比如正在進行網路請求,或者定時器沒有到時間。fulfill
:滿足狀態,當我們主動回調了resolve時,就處於該狀態並且會回撥 then()reject
:拒絕狀態,當我們主動回調了reject時,就處於該狀態並且會回撥 catch()
new Promise((resolve,reject) => {
setTimeout(() => {
//resolve('Hello World')
reject('Error Data')
}, 1000)
}).then(data => {
console.log(data)
}).catch(error => {
console.log(error)
})
3. Promise鏈式呼叫
3.1 使用方式一:每一次呼叫都是非同步操作
new Promise((resolve, reject) => {
//1.第一次模擬網路請求的程式碼
setTimeout(() => {
resolve("Hello World");
}, 2000);
}).then((data) => {
//第一次拿到結果的處理程式碼
console.log(data); //Hello World
return new Promise((resolve, reject) => {
//第二次模擬網路請求的程式碼
setTimeout(() => {
resolve(data + " 111");
}, 2000);
}).then((data) => {
//第二次拿到結果的處理程式碼
console.log(data); //Hello World 111
return new Promise((resolve, reject) => {
//第三次模擬網路請求的程式碼
setTimeout(() => {
resolve(data + "222");
}, 2000);
}).then((data) => {
//第三次拿到結果的處理程式碼
console.log(data); //Hello World 111222
return new Promise((resolve, reject) => {
//第四次模擬網路請求錯誤的程式碼
setTimeout(() => {
reject(data + "error");
}, 2000);
}).then((data) => {
//這裡沒有輸出,這部分程式碼不會執行
console.log(data);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(data + "333");
}, 2000);
});
}).catch((data) => {
//第四次拿到結果的處理程式碼
console.log(data); //Hello World 111222error
//第五次模擬網路請求的程式碼
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(data + "444");
}, 2000);
}).then((data) => {
//第五次拿到結果的處理程式碼
console.log(data); //Hello World 111222error444
//..不能再套娃了
});
});
});
});
});
//注意:其實reject是可選的,當我們不用的時候可以只寫 resolve => {}
3.2 使用方式二:只有第一次呼叫是非同步操作
只有第一次呼叫是非同步操作,後面的呼叫不是非同步操作但是我們希望後面的呼叫也是分層的
new Promise((resolve,reject) => {
setTimeout(() => {
resolve('Hello World')
}, 1000)
}).then(data => {
console.log(data) //Hello World
return Promise.resolve(data + ' 111')
}).then(data => {
console.log(data) //Hello World 111
return Promise.resolve(data + '222')
}).then(data => {
console.log(data) //Hello World 111222
return Promise.reject(data + 'error')
}).then(data => {
console.log(data)
return Promise.resolve(data + '333')
}).catch(data => {
console.log(data) //Hello World 111222error
return Promise.resolve(data + ' 444')
}).then(data => {
console.log(data) //Hello World 111222error444
})
這裡我們直接通過Promise包裝了一下新的資料,將Promise物件返回了
-
Promise.resovle()
:將資料包裝成Promise物件,並且在內部回撥resolve()
函式 -
Promise.reject()
:將資料包裝成Promise物件,並且在內部回撥reject()
函式
鏈式呼叫簡寫
簡化版程式碼:如果我們希望資料直接包裝成Promise.resolve,那麼在then中可以直接返回資料
注意下面的程式碼中我將return Promise.resovle(data)
改成了return data
結果依然是一樣的
new Promise((resolve,reject) => {
setTimeout(() => {
resolve('Hello World')
}, 1000)
}).then(data => {
console.log(data) //Hello World
return data + ' 111'
}).then(data => {
console.log(data) //Hello World 111
return data + '222'
}).then(data => {
console.log(data) //Hello World 111222
return Promise.reject(data + 'error')
}).then(data => {
console.log(data)
return data + '333'
}).catch(data => {
console.log(data) //Hello World 111222error
return data + ' 444'
}).then(data => {
console.log(data) //Hello World 111222error444
})
4. Promise的all方法使用
4.1 案例
假設有兩個網路請求,我們必須要保證兩個網路請求都成功後才能執行一些操作。即兩個網路請求加上後續的操作才是一個完整的業務。怎麼實現呢?
以前的實現方式
//兩個flag
let isResult1 = false;
let isResult2 = false;
//第一個請求
$.ajax({
url:'url1'
success: () => {
console.log("結果一");
isResult1 = true
handleResult()
}
})
//第二個請求
$.ajax({
url:'url2'
success: () => {
console.log("結果二");
isResult2 = true
handleResult()
}
})
function handleResult() {
if(isResult1 && isResult2) {
//後續操作
}
}
4.2 all方法的使用
Promise.all([
new Promise((resolve,reject) => {
//模擬網路請求一
setTimeout(() => {
resolve('result1');
},1000)
}),
new Promise((resolve,reject) => {
//模擬網路請求二
setTimeout(() => {
resolve('result2');
},5000)
}),
]).then(results => {
//5秒後才會列印
console.log(results[0]); //結果一
console.log(results[1]); //結果二
})