1. 程式人生 > 實用技巧 >八. ES6的Promise詳解

八. 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]); //結果二
})