在異步編程中,Promise 扮演了舉足輕重的角色,比傳統的解決方案(回調函數和事件)更合理和更強大。可能有些小夥伴會有這樣的疑問:2020年了,怎麼還在談論Promise?事實上,有些朋友對於這個幾乎天天都在打交道的「老朋友」,貌似全懂,但稍加深刻就可能疑問百出,本文帶你們深刻理解這個熟悉的陌生人—— Promise.javascript
new Promise( function(resolve, reject) {...} /* executor */ )
值得注意的是,Promise 是用來管理異步編程的,它自己不是異步的,new Promise的時候會當即把executor函數執行,只不過咱們通常會在executor函數中處理一個異步操做。好比下面代碼中,一開始是會先打印出2。html
let p1 = new Promise(()=>{ setTimeout(()=>{ console.log(1) },1000) console.log(2) }) console.log(3) // 2 3 1
Promise 採用了回調函數延遲綁定技術,在執行 resolve 函數的時候,回調函數尚未綁定,那麼只能推遲迴調函數的執行。這具體是啥意思呢?咱們先來看下面的例子:前端
let p1 = new Promise((resolve,reject)=>{ console.log(1); resolve('浪裏行舟') console.log(2) }) // then:設置成功或者失敗後處理的方法 p1.then(result=>{ //p1延遲綁定回調函數 console.log('成功 '+result) },reason=>{ console.log('失敗 '+reason) }) console.log(3) // 1 // 2 // 3 // 成功 浪裏行舟
new Promise的時候先執行executor函數,打印出 一、2,Promise在執行resolve時,觸發微任務,仍是繼續往下執行同步任務,
執行p1.then時,存儲起來兩個函數(此時這兩個函數尚未執行),而後打印出3,此時同步任務執行完成,最後執行剛剛那個微任務,從而執行.then中成功的方法。java
Promise 對象的錯誤具備「冒泡」性質,會一直向後傳遞,直到被 onReject 函數處理或 catch 語句捕獲爲止。具有了這樣「冒泡」的特性後,就不須要在每一個 Promise 對象中單獨捕獲異常了。git
要遇到一個then,要執行成功或者失敗的方法,但若是此方法並無在當前then中被定義,則順延到下一個對應的函數es6
function executor (resolve, reject) { let rand = Math.random() console.log(1) console.log(rand) if (rand > 0.5) { resolve() } else { reject() } } var p0 = new Promise(executor) var p1 = p0.then((value) => { console.log('succeed-1') return new Promise(executor) }) var p2 = p1.then((value) => { console.log('succeed-2') return new Promise(executor) }) p2.catch((error) => { console.log('error', error) }) console.log(2)
這段代碼有三個 Promise 對象:p0~p2。不管哪一個對象裏面拋出異常,均可以經過最後一個對象 p2.catch 來捕獲異常,經過這種方式能夠將全部 Promise 對象的錯誤合併到一個函數來處理,這樣就解決了每一個任務都須要單獨處理異常的問題。編程
經過這種方式,咱們就消滅了嵌套調用和頻繁的錯誤處理,這樣使得咱們寫出來的代碼更加優雅,更加符合人的線性思惟。數組
咱們都知道能夠把多個Promise鏈接到一塊兒來表示一系列異步驟。這種方式能夠實現的關鍵在於如下兩個Promise 固有行爲特性:promise
先經過下面的例子,來解釋一下剛剛這段話是什麼意思,而後詳細介紹下鏈式調用的執行流程瀏覽器
let p1=new Promise((resolve,reject)=>{ resolve(100) // 決定了下個then中成功方法會被執行 }) // 鏈接p1 let p2=p1.then(result=>{ console.log('成功1 '+result) return Promise.reject(1) // 返回一個新的Promise實例,決定了當前實例是失敗的,因此決定下一個then中失敗方法會被執行 },reason=>{ console.log('失敗1 '+reason) return 200 }) // 鏈接p2 let p3=p2.then(result=>{ console.log('成功2 '+result) },reason=>{ console.log('失敗2 '+reason) }) // 成功1 100 // 失敗2 1
咱們經過返回 Promise.reject(1) ,完成了第一個調用then建立並返回的promise p2。p2的then調用在運行時會從return Promise.reject(1) 語句接受完成值。固然,p2.then又建立了另外一個新的promise,能夠用變量p3存儲。
new Promise出來的實例,成功或者失敗,取決於executor函數執行的時候,執行的是resolve仍是reject決定的,或executor函數執行發生異常錯誤,這兩種狀況都會把實例狀態改成失敗的。
p2執行then返回的新實例的狀態,決定下一個then中哪個方法會被執行,有如下幾種狀況:
咱們再來看個例子
new Promise(resolve=>{ resolve(a) // 報錯 // 這個executor函數執行發生異常錯誤,決定下個then失敗方法會被執行 }).then(result=>{ console.log(`成功:${result}`) return result*10 },reason=>{ console.log(`失敗:${reason}`) // 執行這句時候,沒有發生異常或者返回一個失敗的Promise實例,因此下個then成功方法會被執行 // 這裏沒有return,最後會返回 undefined }).then(result=>{ console.log(`成功:${result}`) },reason=>{ console.log(`失敗:${reason}`) }) // 失敗:ReferenceError: a is not defined // 成功:undefined
從上面一些例子,咱們能夠看出,雖然使用 Promise 能很好地解決回調地獄的問題,可是這種方式充滿了 Promise 的 then() 方法,若是處理流程比較複雜的話,那麼整段代碼將充斥着 then,語義化不明顯,代碼不能很好地表示執行流程。
ES7中新增的異步編程方法,async/await的實現是基於 Promise的,簡單而言就是async 函數就是返回Promise對象,是generator的語法糖。不少人認爲async/await是異步操做的終極解決方案:
不過也存在一些缺點,由於 await 將異步代碼改形成了同步代碼,若是多個異步代碼沒有依賴性卻使用了 await 會致使性能上的下降。
async function test() { // 如下代碼沒有依賴性的話,徹底可使用 Promise.all 的方式 // 若是有依賴性的話,其實就是解決回調地獄的例子了 await fetch(url1) await fetch(url2) await fetch(url3) }
觀察下面這段代碼,你能判斷出打印出來的內容是什麼嗎?
let p1 = Promise.resolve(1) let p2 = new Promise(resolve => { setTimeout(() => { resolve(2) }, 1000) }) async function fn() { console.log(1) // 當代碼執行到此行(先把此行),構建一個異步的微任務 // 等待promise返回結果,而且await下面的代碼也都被列到任務隊列中 let result1 = await p2 console.log(3) let result2 = await p1 console.log(4) } fn() console.log(2) // 1 2 3 4
若是 await 右側表達邏輯是個 promise,await會等待這個promise的返回結果,只有返回的狀態是resolved狀況,纔會把結果返回,若是promise是失敗狀態,則await不會接收其返回結果,await下面的代碼也不會在繼續執行。
let p1 = Promise.reject(100) async function fn1() { let result = await p1 console.log(1) //這行代碼不會執行 }
咱們再來看道比較複雜的題目:
console.log(1) setTimeout(()=>{console.log(2)},1000) async function fn(){ console.log(3) setTimeout(()=>{console.log(4)},20) return Promise.reject() } async function run(){ console.log(5) await fn() console.log(6) } run() //須要執行150ms左右 for(let i=0;i<90000000;i++){} setTimeout(()=>{ console.log(7) new Promise(resolve=>{ console.log(8) resolve() }).then(()=>{console.log(9)}) },0) console.log(10) // 1 5 3 10 4 7 8 9 2
作這道題以前,讀者需明白:
接下來,咱們一步一步分析:
Promise.resolve(value)方法返回一個以給定值解析後的Promise 對象。
Promise.resolve()等價於下面的寫法:
Promise.resolve('foo') // 等價於 new Promise(resolve => resolve('foo'))
Promise.resolve方法的參數分紅四種狀況。
(1)參數是一個 Promise 實例
若是參數是 Promise 實例,那麼Promise.resolve將不作任何修改、原封不動地返回這個實例。
const p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('fail')), 3000) }) const p2 = new Promise(function (resolve, reject) { setTimeout(() => resolve(p1), 1000) }) p2 .then(result => console.log(result)) .catch(error => console.log(error)) // Error: fail
上面代碼中,p1是一個 Promise,3 秒以後變爲rejected。p2的狀態在 1 秒以後改變,resolve方法返回的是p1。因爲p2返回的是另外一個 Promise,致使p2本身的狀態無效了,由p1的狀態決定p2的狀態。因此,後面的then語句都變成針對後者(p1)。又過了 2 秒,p1變爲rejected,致使觸發catch方法指定的回調函數。
(2)參數不是具備then方法的對象,或根本就不是對象
Promise.resolve("Success").then(function(value) { // Promise.resolve方法的參數,會同時傳給回調函數。 console.log(value); // "Success" }, function(value) { // 不會被調用 });
(3)不帶有任何參數
Promise.resolve()方法容許調用時不帶參數,直接返回一個resolved狀態的 Promise 對象。若是但願獲得一個 Promise 對象,比較方便的方法就是直接調用Promise.resolve()方法。
Promise.resolve().then(function () { console.log('two'); }); console.log('one'); // one two
(4)參數是一個thenable對象
thenable對象指的是具備then方法的對象,Promise.resolve方法會將這個對象轉爲 Promise 對象,而後就當即執行thenable對象的then方法。
let thenable = { then: function(resolve, reject) { resolve(42); } }; let p1 = Promise.resolve(thenable); p1.then(function(value) { console.log(value); // 42 });
Promise.reject()方法返回一個帶有拒絕緣由的Promise對象。
new Promise((resolve,reject) => { reject(new Error("出錯了")); }); // 等價於 Promise.reject(new Error("出錯了")); // 使用方法 Promise.reject(new Error("BOOM!")).catch(error => { console.error(error); });
值得注意的是,調用resolve或reject之後,Promise 的使命就完成了,後繼操做應該放到then方法裏面,而不該該直接寫在resolve或reject的後面。因此,最好在它們前面加上return語句,這樣就不會有意外。
new Promise((resolve, reject) => { return reject(1); // 後面的語句不會執行 console.log(2); })
let p1 = Promise.resolve(1) let p2 = new Promise(resolve => { setTimeout(() => { resolve(2) }, 1000) }) let p3 = Promise.resolve(3) Promise.all([p3, p2, p1]) .then(result => { // 返回的結果是按照Array中編寫實例的順序來 console.log(result) // [ 3, 2, 1 ] }) .catch(reason => { console.log("失敗:reason") })
Promise.all 生成並返回一個新的 Promise 對象,因此它可使用 Promise 實例的全部方法。參數傳遞promise數組中全部的 Promise 對象都變爲resolve的時候,該方法纔會返回, 新建立的 Promise 則會使用這些 promise 的值。
若是參數中的任何一個promise爲reject的話,則整個Promise.all調用會當即終止,並返回一個reject的新的 Promise 對象。
有時候,咱們不關心異步操做的結果,只關心這些操做有沒有結束。這時,ES2020 引入Promise.allSettled()方法就頗有用。若是沒有這個方法,想要確保全部操做都結束,就很麻煩。Promise.all()方法沒法作到這一點。
假若有這樣的場景:一個頁面有三個區域,分別對應三個獨立的接口數據,使用 Promise.all 來併發請求三個接口,若是其中任意一個接口出現異常,狀態是reject,這會致使頁面中該三個區域數據全都沒法出來,顯然這種情況咱們是沒法接受,Promise.allSettled的出現就能夠解決這個痛點:
Promise.allSettled([ Promise.reject({ code: 500, msg: '服務異常' }), Promise.resolve({ code: 200, list: [] }), Promise.resolve({ code: 200, list: [] }) ]).then(res => { console.log(res) /* 0: {status: "rejected", reason: {…}} 1: {status: "fulfilled", value: {…}} 2: {status: "fulfilled", value: {…}} */ // 過濾掉 rejected 狀態,儘量多的保證頁面區域數據渲染 RenderContent( res.filter(el => { return el.status !== 'rejected' }) ) })
Promise.allSettled跟Promise.all相似, 其參數接受一個Promise的數組, 返回一個新的Promise, 惟一的不一樣在於, 它不會進行短路, 也就是說當Promise所有處理完成後,咱們能夠拿到每一個Promise的狀態, 而無論是否處理成功。
Promise.all()方法的效果是"誰跑的慢,以誰爲準執行回調",那麼相對的就有另外一個方法"誰跑的快,以誰爲準執行回調",這就是Promise.race()方法,這個詞原本就是賽跑的意思。race的用法與all同樣,接收一個promise對象數組爲參數。
Promise.all在接收到的全部的對象promise都變爲FulFilled或者Rejected狀態以後纔會繼續進行後面的處理,與之相對的是Promise.race只要有一個promise對象進入FulFilled或者Rejected狀態的話,就會繼續進行後面的處理。
// `delay`毫秒後執行resolve function timerPromisefy(delay) { return new Promise(resolve => { setTimeout(() => { resolve(delay); }, delay); }); } // 任何一個promise變爲resolve或reject的話程序就中止運行 Promise.race([ timerPromisefy(1), timerPromisefy(32), timerPromisefy(64) ]).then(function (value) { console.log(value); // => 1 });
上面的代碼建立了3個promise對象,這些promise對象會分別在1ms、32ms 和 64ms後變爲肯定狀態,即FulFilled,而且在第一個變爲肯定狀態的1ms後,.then註冊的回調函數就會被調用。
ES9 新增 finally() 方法返回一個Promise。在promise結束時,不管結果是fulfilled或者是rejected,都會執行指定的回調函數。這爲在Promise是否成功完成後都須要執行的代碼提供了一種方式。這避免了一樣的語句須要在then()和catch()中各寫一次的狀況。
好比咱們發送請求以前會出現一個loading,當咱們請求發送完成以後,無論請求有沒有出錯,咱們都但願關掉這個loading。
this.loading = true request() .then((res) => { // do something }) .catch(() => { // log err }) .finally(() => { this.loading = false })
finally方法的回調函數不接受任何參數,這代表,finally方法裏面的操做,應該是與狀態無關的,不依賴於 Promise 的執行結果。
假設有這樣一個需求:紅燈 3s 亮一次,綠燈 1s 亮一次,黃燈 2s 亮一次;如何讓三個燈不斷交替重複亮燈?
三個亮燈函數已經存在:
function red() { console.log('red'); } function green() { console.log('green'); } function yellow() { console.log('yellow'); }
這道題複雜的地方在於須要「交替重複」亮燈,而不是亮完一遍就結束的一錘子買賣,咱們能夠經過遞歸來實現:
// 用 promise 實現 let task = (timer, light) => { return new Promise((resolve, reject) => { setTimeout(() => { if (light === 'red') { red() } if (light === 'green') { green() } if (light === 'yellow') { yellow() } resolve() }, timer); }) } let step = () => { task(3000, 'red') .then(() => task(1000, 'green')) .then(() => task(2000, 'yellow')) .then(step) } step()
一樣也能夠經過async/await 的實現:
// async/await 實現 let step = async () => { await task(3000, 'red') await task(1000, 'green') await task(2000, 'yellow') step() } step()
使用 async/await 能夠實現用同步代碼的風格來編寫異步代碼,毫無疑問,仍是 async/await 的方案更加直觀,不過深刻理解Promise 是掌握async/await的基礎。給你們推薦一個好用的BUG監控工具Fundebug,歡迎免費試用!
歡迎關注公衆號:前端工匠,你的成長咱們一塊兒見證!