雖然今年已經18年,可是今天仍是要繼續聊聊ES6的東西,ES6已通過去幾年,但是咱們對於ES6的語法到底是掌握了什麼程度,是瞭解?會用?仍是精通?相信你們和我同樣都對本身有着一個提高的心,對於新玩具可不能僅僅瞭解,對於其中的思想纔是最吸引人的,因此接下來會經過一篇文章,來讓你們對於Promise
這個玩具作到精通的程度!!!
npm
此處打開一瓶冰闊落~~~編程
Promise
是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最先提出和實現,ES6將其寫進了語言標準,統一了用法,原生提供了Promise
對象。promise
嗝~~~~~bash
首先,咱們經過字面能夠看出來Pormise
是一種解決方案,並且還有兩種傳統的解決方案·回調函數
和事件
,ok,那麼咱們就來先聊聊這兩種方案。app
回調函數想必你們都不陌生,就是咱們常見的把一個函數當作參數傳遞給另一個函數,在知足了必定的條件以後再去執行回調,好比咱們想要實現一個在三秒後去計算1到5的和,那麼:dom
// 求和函數
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')
})
})
})
複製代碼
這樣就會看到造成了一個回調嵌套,也就是咱們常說的回調地獄
,致使代碼可讀性十分差。async
事件處理就是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')
複製代碼
恰西瓜中~~~函數
嗝~ok,吃完咱們進入正題,看到上面異步編程如此如此如此麻煩,對於我這種頭大用戶,固然是拒絕的啊,還好咱們有Pormise
(Pormise
大法好),下面咱們就來經過實現一個Promise
去更深的瞭解Promise
的原理,首先咱們瞭解一下PromiseA+
,它是一種規範,用來約束你們寫的Promise
方法的,爲了讓你們寫的Promise
杜絕一些錯誤,按照咱們所指望的流程來走,所以就出現了PromiseA+
規範。
咱們根據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.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的時候可以更加的快速,因此作了一層改變,咱們來看一個例子,好比當咱們封裝一個異步讀取圖片的寬高函數
// 原來的方式
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
最終跑出來咱們所有經過測試!酷!晚餐再加個雞腿~