Promise入門到精通(初級篇)-附程式碼詳細講解

l偏執l發表於2021-01-11

Promise入門到精通(初級篇)-附程式碼詳細講解

​     Promise,中文翻譯為承諾,期約,從字面意思來看,這應該是類似某種協議,規定了什麼事件發生的條件和觸發方法。

​     Promise的誕生和一個詞有關,就是非同步

​     什麼是非同步???

​     首先javascript是執行在瀏覽器端的語言,必須依賴javascript引擎來解析並執行程式碼,js引擎是單執行緒,也就是一個任務接著一個任務來執行程式,這種單執行緒很容易因為一個任務發生延遲,造成整體的耗時變長,為了解決這個問題,所以就有了非同步這個概念。

​ 非同步就是當系統執行一個事件的時候,不會等待該事件結束,而是會去繼續執行其他事件,當這個非同步事件有了響應結果之後,系統會在空閒的時候繼續執行該事件。

​    簡單來說就是js引擎會首先判斷該事件是同步任務還是非同步任務,如果是同步任務,將該事件壓入巨集任務佇列中排隊等待執行,如果是非同步事件,則進入微任務佇列中等待巨集任務佇列處於空閒狀態時,再將微任務佇列中的事件移入巨集任務佇列執行。這樣我們就可以把不確定執行時間的一些事件用非同步來執行。提高了程式執行的效率,

Promise的核心概念

​     Promise中的核心概念是狀態狀態轉換就是Promise執行非同步事件的時機

​     在Promise中有存在3種狀態,分別對應的是:

​     1、等待(pending

​     2、承諾實現(fulfilled

​     3、承諾失效(reject

​     Promise初始狀態只能為等待的padding狀態,在適當的時機,我們可以選擇改變padding的狀態到fulfilled或者reject。

​     ⚠️ Promise中的狀態是不可逆轉的,且僅允許改變一次,所以無法從fulfilled或reject狀態再次切換到其他狀態。當初始的padding改變為fulfilled或reject後,該Promise就相當於完成了它的使命,後續的非同步處理就會交由一個then( )的方法來實現。

Promise的基本構成

​     在ES6語法中,Promise是一個建構函式,使用時需要用new關鍵詞來建立例項物件。Promise建構函式中自帶excutor執行器,excutor執行器中有2個JavaScript中預設的函式引數resolvereject

​     resolve函式的作用是當Promise狀態從padding轉換到resolve時,可以把Promise中的物件或者變數當成引數傳遞出來供非同步成功時呼叫,reject函式的作用是當Promise狀態從padding轉換到reject時候可以把Promise中的物件或者變數,以及系統報錯當成引數傳遞出來供非同步失敗時呼叫。

​     then是Promise原型上的一個方法,Promise.prototype.then() 所以通過建構函式建立的Promise例項物件也會自帶then( )方法。then( )方法接受2個函式引數,作為Promise中非同步成功和非同步失敗的2個回撥函式。

Promise例項的基本程式碼結構:

//ES6 箭頭函式寫法
let promise = new Promise((resolve,reject)=>{
    if(/判斷條件/){
        resolve()//承諾實現
    }else{
				reject()//承諾實效
    }
})
promise.then(res=>{
		//處理承諾實現方法
},err=>{
    //處理承諾失效方法     
})

    ❗️❗️❗️注意:Promise函式本身不是一個非同步函式,在excutor執行器中執行的程式碼是同步的。執行非同步的是then( )方法中的事件

console.log('步驟1');
new Promise((resolve,reject)=>{
    console.log('步驟2');
})
console.log('步驟3')

//執行結果
### 步驟1
### 步驟2
### 步驟3

console.log('步驟1');
new Promise((resolve,reject)=>{
    console.log('步驟2');
    resolve()
}).then(res=>{
    console.log('步驟3');
})
console.log('步驟4')

//執行結果
### 步驟1
### 步驟2
### 步驟4
### 步驟3

​     .catch也是Promise原型上的一個方法,用來接收和處理Promise中的非同步失敗,乍一看怎麼和then( )中第二個函式引數的功能是一樣的嘞?沒錯滴,這2種方法都是用來處理非同步失敗的回撥函式,但它們2個之間還是有一些小小的區別。? then( )中第二個函式引數只能處理當前Promise非同步失敗的回撥,而catch( )可以處理整個Promise鏈上發生的非同步失敗的回撥,便於非同步失敗和系統報錯的整體處理。

let promise new Promise ((resolve,reject)=>{
  reject('發生錯誤了')
})
promise.catch(err=>{
  console.log(err)
})
//執行結果
### 發生錯誤了

let promise new Promise ((resolve,reject)=>{
  throw ('發生錯誤了')
})
promise.catch(err=>{
  console.log(err)
})
//執行結果
### 發生錯誤了

    ⭕️ (推薦在Promise中使用catch來捕獲處理非同步失敗方法和丟擲錯誤

Promise鏈式呼叫

​     鏈式呼叫是Promise中一個特別重要的屬性。也是Promise能控制非同步操作的關鍵,那麼鏈式呼叫是什麼?它的原理又是什麼呢?

​     鏈式呼叫最重要的作用就是能使非同步事件同步化,將多個非同步事件變為同步,

let promise1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve()
    },100)
}).then(res=>{
    console.log('我成功了');
})


let promise2 = new Promise((resolve,reject)=>{
    resolve()
}).then(res=>{
    console.log('我也成功了');
})

//執行結果
### 我也成功了
### 我成功了

​     可以看出,這兩個方法執行順序並不按照程式碼結構上的順序來執行,也就是所謂的非同步事件

​     鏈式呼叫主要原理就是Promise.prototype.then方法和Promise.prototype.catch會返回一個新的Prmise物件,所以我們在Prmise後面可以一直使用then方法來處理非同步事件,這樣每個非同步事件都會等上一個非同步事件的then( )方法觸發後才會執行自身,從而達到同步的效果.

​     當我們想讓2個非同步事件也遵循同步來執行就可以用Prmise的鏈式呼叫方法來重寫程式碼結構:

let promise1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve()
    },100)
}).then(res=>{
    console.log('我成功了');
    return new Promise((resolve,reject)=>{
        resolve()
    })
}).then((res)=>{
    console.log('我也成功了');
})

//執行結果
### 我成功了
### 我也成功了

Promise鏈式呼叫的規則

​     鏈式呼叫是Promise中一個重要的方法,能有效處理同步非同步之間的邏輯關係,但是鏈式呼叫也有著自己一套使用規則,熟悉掌握它的規則才能更好的在開發中靈活的使用

​     規則1:Promise物件會預設返回一個新的Promise,當我們不手動進行干預的時候,這個返回的Promise物件狀態為fulfilled

let promise = new Promise((resolve,reject)=>{
    resolve()
}).then(res=>{
    
}).then(res=>{
    console.log('規則1');
})

//上述的原理等價於下面的寫法??

let promise = new Promise((resolve,reject)=>{
    resolve()
}).then(res=>{
    return new Promise((resolve,reject)=>{
        resolve()
    })
}).then(res=>{
    console.log('規則1');
})

//執行結果
### 規則1

​     規則2:Promise物件返回值型別是非Promise時,會自動轉成狀態是fulfilled的Promise物件,這個返回值會被then( )方法中的第一個引數所接收。

let promise = new Promise((resolve,reject)=>{
    resolve()
}).then(res=>{
    let str ='規則2'
    return str
}).then(res=>{
    console.log(res)
})

//上述的原理等價於下面的寫法??

let promise = new Promise((resolve,reject)=>{
    resolve()
}).then(res=>{
    let str ='規則2'
    return new Promise((resolve,reject)=>{
        resolve(str)
    })
}).then(res=>{
    console.log(res)
})

//執行結果
### 規則2

​     規則3:Promise物件可以手動返回一個新的Promise,這個新的Promise的狀態型別可以由我們來決定在什麼時間轉變為fulfilledreject,自由度較高,方便我們自由的來控制邏輯如何執行

function init (params) {
    let promise = new Promise((resolve,reject)=>{
        let num=params
        resolve(num)
    }).then(res1=>{
        return new Promise((resolve,reject)=>{
           res1>=5?resolve('大於等於5'):reject('小於5')
        })
    }).then(res2=>{
        console.log(res2);
    }).catch(err=>{
        console.log(err);
    })
}

//執行
init(10)
//執行結果
### 大於等於5

//執行
init(1)
//執行結果
###小於5

​     規則4:Promise的回撥函式中如果丟擲錯誤error,會返回一個狀態為reject的Promise物件,將這個錯誤作為引數傳遞給下一個Promise鏈中的then( )方法的第二個函式引數或catch()方法

let promise = new Promise((resolve, reject) => {
    throw new Error('發生錯誤')
}).then(res=>{
    console.log(res);
}).catch(err=>{
    console.log(err);
})

//執行結果
### 發生錯誤

​     規則5:Promise的值可以發生穿透現象,當中間的then()方法沒有定義回撥引數時,上一個Promise鏈上的值會作為引數傳遞到下一個Promise物件的then( )方法的回撥方法中,可以發生多層穿透。

let promise = new Promise((resolve, reject) => {
    resolve('規則5')
}).then().then().then(res=>{
    console.log(res);
})

//執行結果
### 規則5

​     規則6: Promise的resolvereject只能傳遞一個引數,如果傳遞多個引數必須用陣列或物件進行封裝,否則多餘引數的值為undefined

 //用陣列封裝引數
let promise = new Promise((resolve,reject)=>{
            resolve([1,2])
        }).then(res=>{
            console.log(res);
        })
 //執行結果
 ### [1,2]
 
//直接傳遞多引數
  let promise = new Promise((resolve,reject)=>{
            resolve(1,2)
        }).then((num1,num2)=>{
            console.log(num1,num2);
        })
 //執行結果
 ### 1 undefined

Promise解決的實際問題

​     通過上面的介紹,我們大致瞭解了Promise的用法,但是為什麼我們要使用Promise,在什麼地方去使用Promise呢?接下來就來告訴你

​     在JavaScript中尤其是Node.js中,很多的Api方法都是非同步方法,獲取結果後通過回撥函式來執行,當多個這樣的非同步方法巢狀在一起使用的時候就會出現臭名昭著的回撥地獄,我相信大部分開發者面對一個複雜的回撥地獄時都免不了頭皮發麻。下面用這段計數器功能的程式碼簡單模擬一下回撥地獄的形式

let num=0

let callback = function (value,fn) {
    console.log(value);
    fn()
}

callback(num,()=>{
    num++
    callback(num,()=>{
        num++
        callback(num,()=>{
            num++
            callback(num,()=>{
                num++
                callback(num,()=>{
                    console.log('結束');
                })
            })
        })
    })
})

//執行結果
###
0
1
2
3
4
結束

​     上面的程式碼只是簡單的在每次回撥的時候進行num+1的操作,但整體看上去就讓人有點不太舒服了,更不要提在回撥中執行其他非同步操作,定時器,介面請求等,這種寫法的程式碼層級巢狀太深不說,就程式碼長度來看也是越來越寬影響閱讀體驗。

​     接來下我們就用上面學過的Promise方法來簡單的改裝一下這個計數器程式碼

let num=0
let promise = new Promise((resolve,reject)=>{
    console.log(num);
    resolve(++num)
}).then(res=>{
    console.log(res);
    return ++res
}).then(res=>{
    console.log(res);
    return ++res
}).then(res=>{
    console.log(res);
    return ++res
}).then(res=>{
    console.log(res);
}).then(res=>{
    console.log('結束');
})

//執行結果
###
0
1
2
3
4
結束

​     兩段程式碼進行對比,首先在樣式上Promise方法書寫的程式碼看起來更具有美感,在結構上Promise的程式碼比正常回撥函式的寫法更具結構性,每一個then方法裡對應一個回撥,這樣寫出的程式碼更容易被其他人所讀懂。

​     在普通回撥函式中,我們不能保證每次回撥的執行時間和次數和我們預設的一摸一樣,當2個開發人員共同開發一個功能模組的時候可能由於溝通出現問題或另一個開發者的粗心,把傳入的回撥執行了多次

function init(fn) {
        let num = 1;
        fn && fn(num);
        /* 
            #### 其他業務程式碼
        */
        num += 1;
        fn && fn(num);
        //這個方法可能被錯誤的呼叫了2次
      }

init((num) => {
    console.log("我被呼叫了,輸出" + num);
 });

//期望得到的結果
### 我被呼叫了,輸出1
//實際得到的結果
### 我被呼叫了,輸出1
### 我被呼叫了,輸出2

​     如上面所見的模擬程式碼這就可能會造成系統執行上的報錯或得到了一個錯誤的結果。這樣的結果是大家都不想看到的。那我們如果用Promise改進一下呢

function init(fn) {
        return new Promise((resolve,reject)=>{
            let num = 1;
            resolve(num)
            /*
             其他業務程式碼
            */
            num+=1
            resolve(num)
            resolve(num)
        }).then(res=>{
            fn && fn(res);
        })
      }

init((num) => {
   console.log("我被呼叫了,輸出" + num);
 });

​     這樣用Promise修改的程式碼,無論後面呼叫了多少次resolve()方法,都不會再執行了,因為Promise的狀態一旦被改變,就不能再更改了。一定程度上避免了回撥被多次執行的問題。

Promise存在的一些問題

​     1、Promise一旦被生成就會立刻執行,中途是無法退出的

​     2、Promise執行器內部的程式碼如果在resolereject改變狀態後出現報錯,是無法通過then方法第二個引數和catch捕獲到,必須通過內部回撥或者用try catch的方式來丟擲錯誤

//程式在resolve()被執行後出現報錯
let promise = new Promise((resolve,reject)=>{
         resolve()
         throw new Error('錯誤')
     }).then(res=>{
        
     },err=>{
         console.log(err);
     }).catch(err=>{
         console.log(err);
     })
//期待執行結果
###  Error: 錯誤
//實際執行結果
### 沒有任何輸出

//用try catch捕獲錯誤方式
let promise = new Promise((resolve, reject) => {
        try {
          resolve();
          throw new Error("錯誤");
        } catch (error) {
          console.log(error);
        }
 });

//執行結果
### Error: 錯誤

總結

​      PromiseECMAscript ES6原生的物件,是解決javascript語言非同步程式設計產生回撥地獄的一種方法。但它的本質也沒有跳出回撥問題,只是把巢狀關係優化成類似層級結構的寫法來幫助開發者更容易處理非同步中的邏輯程式碼。配合它的一些Api方法讓我們更容易處理一些網路請求,但它也有自身的缺陷,在專案大量使用的話會降低一些效能,需要開發者在適時的時候去正確的使用它。



Promise入門到精通(中級篇)-附程式碼詳細講解
(Promise Api在專案中對應用場景)正在碼字中~

Promise入門到精通(高階篇)-附程式碼詳細講解
(深入瞭解Promise原理,並手動實現一個Promise)正在碼字中~

    對Prmose後續知識點感興趣對小夥伴可以先關注我噢
    如果您覺得文章有錯誤需要修正,請聯絡我,採納通過修改後,您的使用者名稱會留在下面的感謝名單中



      ?感謝名單?

      Sam Xiao,
      Wan-deuk

?感謝對本篇文章的貢獻?

相關文章