前言
雖然今年已經18年,但是今天還是要繼續聊聊ES6的東西,ES6已經過去幾年,可是我們對於ES6的語法究竟是掌握了什麼程度,是瞭解?會用?還是精通?相信大家和我一樣都對自己有著一個提升的心,對於新玩具可不能僅僅瞭解,對於其中的思想才是最吸引人的,所以接下來會通過一篇文章,來讓大家對於Promise
這個玩具做到精通的程度!!!
此處開啟一瓶冰闊落~~~
Promise
Promise
是非同步程式設計的一種解決方案,比傳統的解決方案——回撥函式和事件——更合理和更強大。它由社群最早提出和實現,ES6將其寫進了語言標準,統一了用法,原生提供了Promise
物件。
嗝~~~~~
首先,我們通過字面可以看出來Pormise
是一種解決方案,而且還有兩種傳統的解決方案·回撥函式
和事件
,ok,那麼我們就來先聊聊這兩種方案。
回撥函式 Callback
回撥函式想必大家都不陌生,就是我們常見的把一個函式當做引數傳遞給另外一個函式,在滿足了一定的條件之後再去執行回撥,比如我們想要實現一個在三秒後去計算1到5的和,那麼:
// 求和函式
function sum () {
return eval([...arguments].join('+'))
}
// 三秒後執行函式
function asyncGetSum (callback) {
setTimeout(function(){
var result = callback(1,2,3,4,5);
console.log(result)
},3000)
}
asyncGetSum(sum);
複製程式碼
這樣的實現就是回撥函式,但是如果我要實現在一段動畫,動畫的執行過程是小球先向右移動100px,然後再向下移動100px,在向左移動100px,每段動畫持續時間都是3s.
dom.animate({left:'100px'},3000,'linear',function(){
dom.animate({top:'100px'},3000,'linear',function(){
dom.animate({left:'0px'},3000,'linear',function(){
console.log('動畫 done')
})
})
})
複製程式碼
這樣就會看到形成了一個回撥巢狀,也就是我們常說的回撥地獄
,導致程式碼可讀性十分差。
事件
事件處理就是jQuery
中的on
繫結事件和trigger
觸發事件,其實就是我們常見的釋出訂閱模式,當我訂閱了一個事件,那麼我就是訂閱者,如果釋出者釋出了資料之後,那麼我就要收到相應的通知。
// 定義一個釋出中心
let publishCenter = {
subscribeArrays:{}, // 定義一個訂閱者回撥函式callback
subscribe:function(key,callback){
// 增加訂閱者
if(!this.subscribeArrays[key]){
this.subscribeArrays[key] = [];
}
this.subscribeArrays[key].push(callback)
},
publish:function(){
//釋出 第一個引數是key
let params = [...arguments];
let key = params.shift();
let callbacks = this.subscribeArrays[key];
if(!callbacks || callbacks.length === 0){
// 如果沒人訂閱 那麼就返回
return false
}
for( let i = 0 ; i < callbacks.length; i++ ){
callbacks[i].apply( this, params );
}
}
};
// 訂閱 一個wantWatermelon事件
publishCenter.subscribe('wantWatermelon',function(){console.log('恰西瓜咯~~')})
//觸發wantWatermelon事件 好咯 可以看到 恰西瓜咯
publishCenter.publish('wantWatermelon')
複製程式碼
恰西瓜中~~~
Promise A+
嗝~ok,吃完我們進入正題,看到上面非同步程式設計如此如此如此麻煩,對於我這種頭大使用者,當然是拒絕的啊,還好我們有Pormise
(Pormise
大法好),下面我們就來通過實現一個Promise
去更深的瞭解Promise
的原理,首先我們瞭解一下PromiseA+
,它是一種規範,用來約束大家寫的Promise
方法的,為了讓大家寫的Promise
杜絕一些錯誤,按照我們所期望的流程來走,因此就出現了PromiseA+
規範。
Promise特點
我們根據PromiseA+
文件來一步一步的看Promise
有什麼特點。
首先我們看文件的2.1節,題目是Promise states,也就是說講的是Promise
的狀態,那麼都說了些什麼呢,我們來看一哈:
- 一個promise只有三種狀態,pending態,fulfilled態(完成態),rejected(拒絕態)
- 當promise處於pending態時,可能轉化成fulfilled或者rejected
- 一旦promise的狀態改成了fulfilled後,狀態就不能再改變了,並且需要提供一個不可變的value
- 一旦promise的狀態改成了rejected後,狀態就不能再改變了,並且需要提供一個不可變的reason
ok,那麼我們就開始寫我們自己的Promise
,我們先看看一段正常Promise
的寫法
// 成功或者失敗是需要提供一個value或者reason
let promise1 = new Promise((resolve,rejected)=>{
// 可以發現 當我們new Promise的時候這句話是同步執行的 也就是說當我們初始化一個promise的時候 內部的回撥函式(通常我們叫做執行器executor)會立即執行
console.log('hahahha');
// promise內部支援非同步
setTimeout(function(){
resolve(123);
},100)
// throw new Error('error') 我們也可以在執行器內部直接丟擲一個錯誤 這時promise會直接變成rejected態
})
複製程式碼
根據我們上面的程式碼還有PromiseA+規範中的狀態說明,我們可以知道Promise
已經有了下面幾個特點
promise
有三種狀態 預設pending
態pending
可以變成fulfilled
(成功態)或者rejected
(失敗態),而一旦轉變之後就不能在變成其他值了promise
內部有一個value
用來儲存成功態的結果promise
內部有一個reason
用來儲存失敗態的原因promise
接受一個executor
函式,這個函式有兩個引數,一個是resolve
方法,一個是reject
方法,當執行resolve
時,promise
狀態改變為fulfilled
,執行reject
時,promise
狀態改變為rejected
- 預設
new Promise
執行的時候內部的executor
函式執行 promise
內部支援非同步改變狀態promise
內部支援丟擲異常,那麼該promise
的狀態直接改成rejected
我們接下來繼續看PromiseA+文件:
promise
必須要有一個then
方法,用來訪問它當前的value
或者是reason
- 該方法接受兩個引數
onFulfilled
(成功回掉函式),onRejected
(失敗回撥函式)promise.then(onFulfilled, onRejected)
- 這兩個引數都是可選引數,如果發現這兩個引數不是函式型別的話,那麼就忽略 比如
promise.then().then(data=>console.log(data),err=>console.log(err))
就可以形成一個值穿透onFulfilled
必須在promise
狀態改成fulfilled
之後改成呼叫,並且呢promise
內部的value
值是這個函式的引數,而且這個函式不能重複呼叫onRejected
必須在promise
狀態改成rejected
之後改成呼叫,並且呢promise
內部的reason
值是這個函式的引數,而且這個函式不能重複呼叫onFulfilled
和onRejected
這兩個方法必須要在當前執行棧的上下文執行完畢後再呼叫,其實就是事件迴圈中的微任務(setTimeout
是巨集任務,有一定的差異)onFulfilled
和onRejected
這兩個方法必須通過函式呼叫,也就是說 他們倆不是通過this.onFulfilled()
或者this.onRejected()
呼叫,直接onFulfilled()
或者onRejected()
then
方法可以在一個promise
上多次呼叫,也就是我們常見的鏈式呼叫- 如果當前
promise
的狀態改成了fulfilled
那麼就要按照順序依次執行then
方法中的onFulfilled
回撥- 如果當前
promise
的狀態改成了rejected
那麼就要按照順序依次執行then
方法中的onRejected
回撥then
方法必須返回一個promise
(接下來我們會把這個promise
稱做promise2
),類似於promise2 = promise1.then(onFulfilled, onRejected);
- 如果呢
onFulfilled()
或者onRejected()
任一一個返回一個值x
,那麼就要去執行resolvePromise
這個函式中去(這個函式是用來處理返回值x
遇到的各種值,然後根據這些值去決定我們剛剛then
方法中onFulfilled()
或者onRejected()
這兩個回撥返回的promise2
的狀態)- 如果我們在
then
中執行onFulfilled()
或者onRejected()
方法時產生了異常,那麼就將promise2
用異常的原因e
去reject
- 如果
onFulfilled
或者onRejected
不是函式,並且promise
的狀態已經改成了fulfilled
或者rejected
,那麼就用同樣的value
或者reason
去更新promise2
的狀態(其實這一條和第三條一個道理,也就是值得穿透問題)
好吧,我們總結了這麼多規範特點,那麼我們就用這些先來練練手
/**
* 實現一個PromiseA+
* @description 實現一個簡要的promise
* @param {Function} executor 執行器
* @author Leslie
*/
function Promise(executor){
let self = this;
self.status = 'pending'; // 儲存promise狀態 pending fulfilled rejected.
self.value = undefined; // 儲存成功後的值
self.reason = undefined; // 記錄失敗的原因
self.onfulfilledCallbacks = []; // 非同步時候收整合功回撥
self.onrejectedCallbacks = []; // 非同步時候收集失敗回撥
function resolve(value){
if(self.status === 'pending'){
self.status = 'fulfilled';// resolve的時候改變promise的狀態
self.value = value;//修改成功的值
// 非同步執行後 呼叫resolve 再把儲存的then中的成功回撥函式執行一遍
self.onfulfilledCallbacks.forEach(element => {
element()
});
}
}
function reject(reason){
if(self.status === 'pending'){
self.status = 'rejected';// reject的時候改變promise的狀態
self.reason = reason; // 修改失敗的原因
// 非同步執行後 呼叫reject 再把儲存的then中的失敗回撥函式執行一遍
self.onrejectedCallbacks.forEach(element => {
element()
});
}
}
// 如果執行器中丟擲異常 那麼就把promise的狀態用這個異常reject掉
try {
//執行 執行器
executor(resolve,reject);
} catch (error) {
reject(error)
}
}
Promise.prototype.then = function(onfulfilled,onrejected){
// onfulfilled then方法中的成功回撥
// onrejected then方法中的失敗回撥
let self = this;
// 如果onfulfilled不是函式 那麼就用預設的函式替代 以便達到值穿透
onfulfilled = typeof onfulfilled === 'function'?onfulfilled:val=>val;
// 如果onrejected不是函式 那麼就用預設的函式替代 以便達到值穿透
onrejected = typeof onrejected === 'function'?onrejected: err=>{throw err}
let promise2 = new Promise((resolve,reject)=>{
if(self.status === 'fulfilled'){
// 加入setTimeout 模擬非同步
// 如果呼叫then的時候promise 的狀態已經變成了fulfilled 那麼就呼叫成功回撥 並且傳遞引數為 成功的value
setTimeout(function(){
// 如果執行回撥發生了異常 那麼就用這個異常作為promise2的失敗原因
try {
// x 是執行成功回撥的結果
let x = onfulfilled(self.value);
// 呼叫resolvePromise函式 根據x的值 來決定promise2的狀態
resolvePromise(promise2,x,resolve,reject);
} catch (error) {
reject(error)
}
},0)
}
if(self.status === 'rejected'){
// 加入setTimeout 模擬非同步
// 如果呼叫then的時候promise 的狀態已經變成了rejected 那麼就呼叫失敗回撥 並且傳遞引數為 失敗的reason
setTimeout(function(){
// 如果執行回撥發生了異常 那麼就用這個異常作為promise2的失敗原因
try {
// x 是執行失敗回撥的結果
let x = onrejected(self.reason);
// 呼叫resolvePromise函式 根據x的值 來決定promise2的狀態
resolvePromise(promise2,x,resolve,reject);
} catch (error) {
reject(error)
}
},0)
}
if(self.status === 'pending'){
//如果呼叫then的時候promise的狀態還是pending,說明promsie執行器內部的resolve或者reject是非同步執行的,那麼就需要先把then方法中的成功回撥和失敗回撥儲存襲來,等待promise的狀態改成fulfilled或者rejected時候再按順序執行相關回撥
self.onfulfilledCallbacks.push(()=>{
//setTimeout模擬非同步
setTimeout(function(){
// 如果執行回撥發生了異常 那麼就用這個異常作為promise2的失敗原因
try {
// x 是執行成功回撥的結果
let x = onfulfilled(self.value)
// 呼叫resolvePromise函式 根據x的值 來決定promise2的狀態
resolvePromise(promise2,x,resolve,reject);
} catch (error) {
reject(error)
}
},0)
})
self.onrejectedCallbacks.push(()=>{
//setTimeout模擬非同步
setTimeout(function(){
// 如果執行回撥發生了異常 那麼就用這個異常作為promise2的失敗原因
try {
// x 是執行失敗回撥的結果
let x = onrejected(self.reason)
// 呼叫resolvePromise函式 根據x的值 來決定promise2的狀態
resolvePromise(promise2,x,resolve,reject);
} catch (error) {
reject(error)
}
},0)
})
}
})
return promise2;
}
複製程式碼
一氣呵成,是不是覺得之前總結出的特點十分有效,對著特點十分順暢的就擼完了程式碼~
那麼就讓我們接著來看看promiseA+文件裡還有些什麼內容吧
resolvePromise
這個函式呢會決定promise2
用什麼樣的狀態,如果x
是一個普通值,那麼就直接採用x
,如果x
是一個promise
那麼就將這個promise
的狀態當成是promise2
的狀態- 判斷如果
x
和promise2
是一個物件,即promise2 === x
,那麼就陷入了迴圈呼叫,這時候promise2
就會以一個TypeError
為reason
轉化為rejected
- 如果
x
是一個promise
,那麼promise2
就採用x
的狀態,用和x
相同的value
去resolve
,或者用和x
相同的reason
去reject
- 如果
x
是一個物件或者是函式 那麼就先執行let then = x.then
- 如果
x
不是一個物件或者函式 那麼就resolve
這個x
- 如果在執行上面的語句中報錯了,那麼就用這個錯誤原因去
reject
promise2
- 如果
then
是一個函式,那麼就執行then.call(x,resolveCallback,rejectCallback)
- 如果
then
不是一個函式,那麼就resolve
這個x
- 如果
x
是fulfilled
態 那麼就會走resolveCallback
這個函式,這時候就預設把成功的value
作為引數y
傳遞給resolveCallback
,即y=>resolvePromise(promise2,y)
,繼續呼叫resolvePromise
這個函式 確保 返回值是一個普通值而不是promise
- 如果
x
是rejected
態 那麼就把這個失敗的原因reason
作為promise2
的失敗原因reject
出去- 如果
resolveCallback
,rejectCallback
這兩個函式已經被呼叫了,或者多次被相同的引數呼叫,那麼就確保只調第一次,剩下的都忽略掉- 如果呼叫
then
丟擲異常了,並且如果resolveCallback
,rejectCallback
這兩個函式已經被呼叫了,那麼就忽略這個異常,否則就用這個異常作為promise2
的reject
原因
我們又又又又又又總結了這麼多,好吧不說了總結多少就開擼吧。
/**
* 用來處理then方法返回結果包裝成promise 方便鏈式呼叫
* @param {*} promise2 then方法執行產生的promise 方便鏈式呼叫
* @param {*} x then方法執行完成功回撥或者失敗回撥後的result
* @param {*} resolve 返回的promise的resolve方法 用來更改promise最後的狀態
* @param {*} reject 返回的promise的reject方法 用來更改promise最後的狀態
*/
function resolvePromise(promise2,x,resolve,reject){
// 首先判斷x和promise2是否是同一引用 如果是 那麼就用一個型別錯誤作為Promise2的失敗原因reject
if( promise2 === x) return reject(new TypeError('typeError:大佬,你迴圈引用了!'));
// called 用來記錄promise2的狀態改變,一旦發生改變了 就不允許 再改成其他狀態
let called;
if( x !== null && ( typeof x === 'object' || typeof x === 'function')){
// 如果x是一個物件或者函式 那麼他就有可能是promise 需要注意 null typeof也是 object 所以需要排除掉
//先獲得x中的then 如果這一步發生異常了,那麼就直接把異常原因reject掉
try {
let then = x.then;//防止別人瞎寫報錯
if(typeof then === 'function'){
//如果then是個函式 那麼就呼叫then 並且把成功回撥和失敗回撥傳進去,如果x是一個promise 並且最終狀態時成功,那麼就會執行成功的回撥,如果失敗就會執行失敗的回撥如果失敗了,就把失敗的原因reject出去,做為promise2的失敗原因,如果成功了那麼成功的value時y,這個y有可能仍然是promise,所以需要遞迴呼叫resolvePromise這個方法 直達返回值不是一個promise
then.call(x,y => {
if(called) return;
called = true;
resolvePromise(promise2,y,resolve,reject)
}, error=>{
if(called) return
called = true;
reject(error)
})
}else{
resolve(x)
}
} catch (error) {
if(called) return
called = true;
reject(error)
}
}else{
// 如果是一個普通值 那麼就直接把x作為promise2的成功value resolve掉
resolve(x)
}
}
複製程式碼
finnnnnnnnnally,我們終於通過我們的不懈努力實現了一個基於PromiseA+規範的Promise
!
最後呢為了完美,我們還要在這個promise
上實現Promise.resolve
,Promise.reject
,以及catch
,Promise.all
和Promise.race
這些方法。
Promise的一些方法
Promise.resolve = function(value){
return new Promise((resolve,reject)=>{
resolve(value)
})
}
Promise.reject = function(reason){
return new Promise((resolve,reject)=>{
reject(reason)
})
}
Promise.prototype.catch = function(onRejected){
return this.then(null,onRejected)
}
Promise.all = function(promises){
return new Promise((resolve,reject)=>{
let arr = [];
let i = 0;
function getResult(index,value){
arr[index] = value;
if(++i == promises.length) {
resolve(arr)
}
}
for(let i = 0;i<promises.length;i++){
promises[i].then(data=>{
getResult(i,data)
},reject)
}
})
}
Promise.race = function(promises){
return new Promise((resolve,reject)=>{
for(let i = 0 ; i < promises.length ; i++){
promises[i].then(resolve,reject)
}
})
}
複製程式碼
Promise 語法糖
恰完西瓜來口糖,語法糖是為了讓我們書寫promise的時候能夠更加的快速,所以做了一層改變,我們來看一個例子,比如當我們封裝一個非同步讀取圖片的寬高函式
// 原來的方式
let getImgWidthHeight = function(imgUrl){
return new Promise((resolve,reject)=>{
let img = new Image();
img.onload = function(){
resolve(img.width+'-'+img.height)
}
img.onerror = function(e){
reject(e)
}
img.src = imgUrl;
})
}
複製程式碼
是不是覺得怎麼寫起來有點舒服但又有點不舒服,好像我每次都要去寫執行器啊!為什麼!好的,沒有為什麼,既然不舒服 我們就改!
// 實現一個promise的語法糖
Promise.defer = Promise.deferred = function (){
let dfd = {};
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject;
})
return dfd
}
複製程式碼
有了上面的語法糖我們再看一下那個圖片的函式怎麼寫
let newGetImgWidthHeight = function(imgUrl){
let dfd = Promise.defer();
let img = new Image();
img.onload = function(){
dfd.resolve(img.width+'-'+img.height)
}
img.onerror = function(e){
dfd.reject(e)
}
img.url = imgUrl;
return dfd.promise
}
複製程式碼
是不是發現我們少了一層函式巢狀,呼~~ 得勁~~
最終檢測
npm install promises-aplus-tests -g
複製程式碼
既然我們都說了我們是遵循promiseA+規範的,那至少要拿出點證據來是不是,不然是不是說服不了大家,那麼我們就用promises-aplus-tests這個包來檢測我們寫的promise
究竟怎麼樣呢!安裝完成之後來跑一下我們的promise
最終跑出來我們全部通過測試!酷!晚餐再加個雞腿~