前端js學習中,讓大家最難受的就是異步的問題,解決異步、回調地獄
等問題時你必須得學會promise,對于多數前端程序員來說promise簡直就是噩夢,本篇文章就是從通俗易懂的角度做為切入點,幫助大家輕松掌握promise
異步編程
想要學習promise,你必須要懂得什么是異步編程!眾所周知,js語言是單線程
機制。所謂單線程就是按次序執行,執行完一個任務再執行下一個。但是不影響存在同步
和異步
的兩種操作,這兩種操作做事情其實都是在一條流水線上(單線程),只是這兩種操作在單線程上的執行順序不一樣罷了!當js觸發到異步任務時,會將異步任務交給瀏覽器處理,當執行有結果時,會把異步任務的回調函數插入待處理隊列的隊尾!
我們通俗的去解釋一下我們的異步:異步就是從主線程發射一個子線程來完成任務
,每一個任務有一個或多個回調函數(callback),前一個任務結束后,不是執行后一個任務,而是執行回調函數
,后一個任務則是不等前一個任務結束就執行
,所以程序的執行順序與任務的排列順序是不一致的、異步的.
該圖摘自于菜鳥教程中的
異步編程
小節,幫助大家更好的理解什么是異步!
回調函數
回調函數的定義非常簡單:一個函數被當做一個
實參
傳入到另一個函數(外部函數
),并且這個函數在外部函數內被調用,用來完成某些任務的函數。就稱為回調函數
回調函數的兩種寫法(實現效果相同
):
const text = () => { document.write('hello james') } setTimeout(text,1000)
setTimeout(()=>{ document.write("hello james") },1000)
這段代碼中的 setTimeout
就是一個消耗時間較長的過程,它的第一個參數是個回調函數
,第二個參數是毫秒數
,這個函數執行之后會產生一個子線程,子線程會等待 1 秒,然后執行回調函數 "text"
,在文本中輸出hello james
setTimeout會在子線程中等待1秒,但是主線程的運行不會受到影響!例如以下代碼:
setTimeout(()=>{ document.write("hello davis") },1000) console.log('123456');
在這里會先打印出來
123456
(主線程
),然后一秒后在文本中輸出hello davis
(子線程
)
回調地獄
回調地獄這個詞聽起來就非常的高大上,想要接觸Promise之前,必須要懂得什么是回調地獄
,以及為什么會產生回調地獄?
先來看看概念:當一個回調函數嵌套一個回調函數的時候就會出現一個嵌套結構當嵌套的多了就會出現回調地獄的情況
。
舉個例子:
比如我們發送三個 ajax 請求:
- 第一個正常發送
- 第二個請求需要第一個請求的結果中的某一個值作為參數
- 第三個請求需要第二個請求的結果中的某一個值作為參數
你會看到以下代碼:
$.ajax({ url: '我是第一個請求', type: 'get', success (res) { // 現在發送第二個請求 $.ajax({ url: '我是第二個請求', type:'post', data: { a: res.a, b: res.b }, success (res1) { // 進行第三個請求 $.ajax({ url: '我是第三個請求', type:'post', data: { a: res1.a, b: res1.b }, success (res2) { console.log(res2) } }) } }) } })
這種代碼看起來屬實是折磨人啊!當我們把代碼寫成這樣的時候,就陷入了
可維護性差
的狀態了,代碼體驗非常的不良好,看一會就給看懵了,為了解決這個問題,于是,就引入了我們的Promise
,用Promise去解決回調地獄問題!
Promise
Promise
是異步編程的一種解決方案
,比傳統的解決方案——回調函數和事件——更合理和更強大,它是一個 ECMAScript 6 提供的類
,目的是更加優雅地書寫復雜的異步任務
。
Promise對象有以下兩個特點:
-
對象的狀態不受外界影響。Promise對象代表一個異步操作,有三種狀態:
pending
(進行中)、fulfilled
(已成功)和rejected
(已失敗)。只有異步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態
。這也是Promise這個名字的由來,它的英語意思就是“承諾”
,表示其他手段無法改變。 -
一旦狀態改變,就不會再變,任何時候都可以得到這個結果
。Promise對象的狀態改變,只有兩種可能:從pending變為fulfilled和從pending變為rejected
。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱為 resolved(已定型)。如果改變已經發生了,你再對Promise對象添加回調函數,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。
兩個特點摘自于??阮一峰ES6文章
new Promise(function (resolve, reject) { // resolve 表示成功的回調 // reject 表示失敗的回調 }).then(function (res) { // 成功的函數 }).catch(function (err) { // 失敗的函數 })
出現了new
關鍵字,就明白了Promise
對象其實就是一個構造函數,是用來生成Promise
實例的。能看出來構造函數接收了一個函數作為參數,該函數就是Promise構造函數的回調函數
,該函數中有兩個參數resolve
和reject
,這兩個參數也分別是兩個函數!
簡單的去理解的話resolve
函數的目的是將Promise對象狀態變成成功狀態
,在異步操作成功時調用,將異步操作的結果,作為參數
傳遞出去。reject
函數的目的是將Promise對象的狀態變成失敗狀態
,在異步操作失敗時調用,并將異步操作報出的錯誤,作為參數
傳遞出去。
Promise實例生成以后,可以用then方法
分別指定resolved狀態
和rejected狀態
的回調函數。
代碼示例:
const promise = new Promise((resolve,reject)=>{ //異步代碼 setTimeout(()=>{ // resolve(['111','222','333']) reject('error') },2000) }) promise.then((res)=>{ //兌現承諾,這個函數被執行 console.log('success',res); }).catch((err)=>{ //拒絕承諾,這個函數就會被執行 console.log('fail',err); })
代碼分析:
上邊說到Promise是一個構造函數,new之后等于說調用了構造函數,構造函數中傳的參數是一個函數,這個函數內的兩個參數分別又是兩個函數(
reslove
和reject
),雖然感覺很繞,但是理清思路會很清晰的!
我們得到對象
promise
,promise對象中自帶有兩個方法then
和catch
,這兩個方法中會分別再傳入一個回調函數,這個回調函數的目的在于返回你所需要成功或失敗的信息!那么怎么去調用這兩個回調函數呢?
看下方圖可以快速理解:
這兩個函數分別做為參數(reslove
和reject
)傳到上方的函數中去了.隨后在構造函數的回調函數中寫入異步代碼
(例如:ajax
和定時器
),這里使用了定時器作為例子,如果你想表達的是成功回調,你可以在內部調用函數reslove('一般情況下是后端返回的成功數據)
。如果你想表達的是失敗回調,你可以調用reject('一般情況下是后端返回的失敗信息')
.
這些就是Promise執行的過程!雖然理解著很繞,但是多讀幾遍絕對有不一樣的收獲!
then
方法返回的是一個新的Promise實例
(注意:不是原來那個Promise實例
)。因此可以采用鏈式寫法
,即then方法后面再調用另一個then方法
實際案例:
我想要實現在一個數組中查看一個帖子,但是我最終的目的是得到這個帖子下面的所有評論,這該怎么實現呢?
實現思路:
先從一個接口中獲取這個帖子的信息,然后通過該帖子的帖子id
從而獲取到該帖子下的所有評論
代碼如下:
pajax({ url:"http://localhost:3000/news", data : { author : "james" } }).then(res=>{ return pajax({ url : "http://localhost:3000/comments", data : { newsId : res[0].id } }) }).then(res=>{ console.log(res); }).catch(err=>{ console.log(err); })
代碼分析:
這里使用了一個Promise已經封裝過的ajax,我們從第一個接口中得到了
帖子id
,然后在then中的函數發送第二個請求(攜帶了第一個請求返回過來的參數
),我們最后想要拿到第二個接口的結果,于是又有了一個then方法,但是在第一個then方法中要把一個新的Promise實例return
出去,這樣的話,第二個then才起作用!(這是因為then
方法是Promise 實例
所具有的方法,也就是說,then方法是定義在原型對象Promise.prototype上的
)====>我們可以打印一下:console.log(Promise.prototype)
可以看的出來原型對象Promise.prototype
中是有then方法的!
Promise.all()
Promise.all()
方法用于將多個 Promise 實例,包裝成一個新的 Promise 實例
。
語法格式:
const p = Promise.all([p1, p2, p3]);
Promise.all()
方法接受一個數組
作為參數,p1、p2、p3都是 Promise 實例
,如果不是,就會調用Promise.reslove() [該方法可自行了解]
自動將參數轉為 Promise 實例,再進一步處理。
說那么多白話沒用,我們可以根據一個案例,就可以明白Promise.all()
的用途了。
實際案例:
如果你想實現一個效果:在一個頁面中,等到頁面中所有的請求返回數據后,再渲染頁面,該怎么實現呢?(在實際開發中我們會看到loading
加載頁面,等數據返回完后,loading加載頁面會消失,整個頁面就展現出來了,增強用戶的體驗。)
實現思路:
通過Promise.all()
方法,等多個接口全部接收到數據后,再統一進行處理,然后渲染頁面
代碼如下:
console.log("顯示加載中") const q1 = pajax({ url:"http://localhost:3000/looplist" }) const q2 = pajax({ url:"http://localhost:3000/datalist" }) Promise.all([q1,q2]).then(res=>{ console.log(res) console.log("隱藏加載中...") }).catch(err=>{ console.log(err) })
代碼分析:
在上方代碼中,全局打印
顯示加載中
是代替loading的頁面,表示該頁面現在正是loading
頁面中,等到q1
和q2
所請求接口都得到返回的信息后,在then
方法中接收收據,并且可以進行渲染頁面,同時隱藏了loading
加載頁面!
小結
不論是在前端的項目開發中還是在前端的面試過程中,Promise的地位就是舉足輕重的
,雖然解決異步編程的終極解決方案是async和await
,但是它們也是基于Promise封裝而來的,在以往文章中,我就說過,學習編程重要的是搞懂某個技術是怎么實現的,而不是做一個cv俠
,多去思考,才能進步。繼續加油吧,少年!
【