tft每日頭條

 > 圖文

 > promise 現場版

promise 現場版

圖文 更新时间:2024-11-28 03:53:03

Promise簡介

Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。(摘自<ECMAScript 6 入門>),但是對于Promise的創建和執行流程,其實有很多人初學時,容易走入誤區。

今天就以一首歌來帶大家學習Promise。

Promise創建

Promise翻譯過來的意思就是"承諾",其實Promise的創建本質就是一個許下承諾的過程,想到這我的腦海中不由得浮現了一首經典老歌,海鳴威 《你的承諾》。

歌中相知相戀的兩個人,奈何情深緣淺,終于還是分開,許下了“不為彼此難過, 過各自的生活"這樣的承諾。但是随後歌曲又以"Oh baby 你答應我的我都記得 ,但是你卻忘了你的承諾 ,不是說好彼此都不再聯絡, 誰都别再犯錯",暗示承諾的狀态發生改變。

由此不難想象以下兩種情況:

  1. 遵守承諾

promise 現場版(一首歌帶你搞懂Promise)1

  1. 違背承諾

promise 現場版(一首歌帶你搞懂Promise)2

同樣,JS中的Promise的創建也是許下代碼承諾的一種方式,它默認也包含三種狀态:

  1. pending (進行中)
  2. fulfilled(已成功)
  3. rejected(已失敗)

那麼如何許下JS中的承諾呢:

var p = new Promise(function(){ // 回調函數 })

Promise中接收一個回調函數(簡單說就是一個容器),傳入某個未來才會結束的事件(通常是一個異步操作)的結果;此時打印,你會得到一個Promise實例(實例化對象),展開之後默認顯示:

Promise {<pending>} [[Prototype]]: Promise, [[PromiseState]]: "pending", [[PromiseResult]]: undefined,

此時我們會看到

[[PromiseState]]用于存儲Promise實例(當前承諾)的狀态 ,默認顯示pending ; [[PromiseResult]]用于表示Promise實例中存儲的數據 , 默認顯示undefined; [[Prototype]]表示原型 以後再做講解

但是有人就會問了,既然有三種狀态,而默認是pending(進行中)狀态,那麼我們如何将當前Promise實例的狀态改為另外兩種:

這就不得不提到Promise的第一特征了:

對象的狀态不受外界影響。Promise對象代表一個異步操作,有三種狀态:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。隻有異步操作的結果,可以決定當前是哪一種狀态,任何其他操作都無法改變這個狀态。

可以理解為,狀态并不是自行改變的,而是由異步(同步也可以)的結果決定了.。

所以Promise在創建過程中,傳入的回調函數接收了改變狀态的兩個方法 ,resolve,reject作為形式參數傳入.

var p = new Promise(function(resolve,reject){//形式參數 // 回調函數 })

resolve() 将Promise實例的狀态 由pending(進行中) 改為 fulfilled(已成功)

reject() 将Promise實例的狀态 由pending(進行中) 改為rejected(已失敗)

而且resolve() reject() 方法在調用時 還可以接收一個參數,作為數據傳入,存儲到Promise實例中。

同步代碼展示:

var p = new Promise(function(resolve,reject){//形式參數 // 回調函數 resolve(111) }) console.log(p) 結果: Promise {<fulfilled>: 111} [[Prototype]]: Promise [[PromiseState]]: "fulfilled" // 已成功 [[PromiseResult]]: 111 // 存儲數據 var p = new Promise(function (resolve, reject) {//形式參數 // 回調函數 reject(222) }) Promise {<rejected>: 111} [[Prototype]]: Promise [[PromiseState]]: "rejected" // 已失敗 [[PromiseResult]]: 222 // 存儲數據 Uncaught (in promise) 111 // 報錯 (可以不用理會 後面會被then() / catch() 方法捕獲)

resolve() 和 reject() 方法,在同步代碼中調用,會立即改變Promise實例的狀态。

異步代碼展示:

var p = new Promise(function (resolve, reject) {//形式參數 setTimeout(function () { resolve(111); console.log("line 24:", p); }, 2000) }) console.log("line 27:", p); 結果依次為: line 27: Promise {<pending>} line 24: Promise {<fulfilled>: 111} -------------------------------------兩次代碼請分開執行------------------------------------------------- var p = new Promise(function (resolve, reject) {//形式參數 setTimeout(function () { reject(111); console.log("line 24:", p); }, 2000) }) console.log("line 27:", p); 結果依次為: line 27: Promise {<pending>} ...2s後 line 24: Promise {<rejected>: 111} 報錯: 2.html:60 Uncaught (in promise) 111 (可以不用理會 後面會被then() / catch() 方法捕獲)

resolv e() 和 reject() 方法,在異步代碼中調用:由于JS是單線程,會優先在主線程執行同步代碼,異步代碼會放到任務隊列中,所以在頁面加載時,,Promise實例為pending(進行中)狀态,等待主線程執行完畢,任務隊列通知主線程,異步任務可以執行了,該任務才會進入主線程執行。

此時setTimeout中的回調函數被調用,執行了resolve() 或 reject() 方法, Promise實例會随之改變狀态,并存儲傳入的數據。

那麼resolve() reject() 方法,可以重複調用麼?

這就要講到Promise的第二大特征:一旦狀态改變,就不會再變,任何時候都可以得到這個結果。Promise對象的狀态改變,隻有兩種可能:從pending變為fulfilled和從pending變為rejected。隻要這兩種情況發生,狀态就凝固了,不會再變了,會一直保持這個結果,這時就稱為 resolved(已定型) 注意,為了行文方便,本章後面的resolved統一隻指fulfilled狀态,不包含rejected狀态。

由此可見Promise本質,就是一個狀态機,當該Promise對象創建出來之後,其狀态就是pending(進行中),然後通過程序來控制到底是執行已完成,還是執行已失敗。

因為Promise處理的是異步任務,所以我們還得對Promise做監聽,當Promise的狀态發生變化時,我們要執行相應的函數(then catch finally)。

Promise的動态方法

所謂Promise動态方法,即将封裝的方法存儲在Promise的原型對象(prototype)上,供所有通過構造函數創建的實例化對象使用。

Promise.prototype.then()

Promise 實例具有then方法,它的作用是為 Promise 實例添加狀态改變時的回調函數,前面說過 Promise實例的狀态可以從pending(進行中) 變為 fulfilled(已成功)或rejected(已失敗),所以then方法中接收兩個回調函數(它們都是可選的)。

p.then(resolveHandler,rejectHandler)

resolveHandler 第一參數,用于指定Promise實例,由pending(進行中) 變為 fulfilled(已成功)時執行的回調函數。

rejectHandler 第二參數,用于指定Promise實例,由pending(進行中) 變為 rejected(已失敗)時執行的回調函數。

可以理解為 then()方法中的兩個回調函數,提前規定好了狀态改變時需要執行的内容,等待以後Promise實例的狀态之後再執行對應的回調函數 (本質還是回調函數,要用魔法打敗魔法)。

注意:resolveHandler rejectHandler 中可以傳入一個形式參數 接收Promise實例中存儲數據。

// 等待兩秒後 随機一個0-1的數,如果 num>=0.5 就變為fulfilled,否則變為rejected var p = new Promise(function (resolve, reject) {//形式參數 setTimeout(function () { var num = Math.random(); if (num >= 0.5) { resolve(111) } else { reject(2222); } console.log("line 24:", p); }, 2000) }) console.log("line 27:", p); p.then(function (arg) { console.log("line 50", arg); }, function (arg) { console.log("line 52", arg); }) 情況1: (num>=0.5) line 27: Promise {<pending>} ...2秒後 line 24: Promise {<fulfilled>: 111} line 50 111 情況2: (num<0.5) line 27: Promise {<pending>} ...2秒後 2.html:81 line 24: Promise {<rejected>: 2222} 2.html:89 line 52 2222

解析:在異步代碼中調用: 由于JS是單線程,會優先在主線程執行同步代碼,異步代碼會放到任務隊列中,所以

  1. Promise創建完畢,Promise實例為pending(進行中)狀态;
  2. 調用 then()方法,傳入兩個回調函數,提前規定好了狀态改變時需要執行的内容,等待以後Promise實例的狀态之後再執行對應的回調函數;
  3. 等待主線程執行完畢,任務隊列通知主線程,異步任務可以執行了,該任務才會進入主線程執行。=> 随機一個0-1的随機數,根據結果改變Promise實例的狀态 ,存儲傳入的數據 => 執行then方法中對應的回調函數。

還有一個有趣的地方:then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)。因此可以采用鍊式寫法,即then方法後面再調用另一個then方法。

var p = new Promise(function (resolve, reject) {//形式參數 setTimeout(function () { resolve(1); }, 2000); }) p.then(function (num) { console.log("line 64:", num); return 2; }).then(function (num) { console.log("line 67:", num); return 2; }) 結果: line 64: 1 line 67: 2

上面的代碼使用then方法,依次指定了兩個回調函數。第一個回調函數完成以後,會将返回結果作為參數,傳入第二個回調函數。當然這裡有一個前提,那就是Promise實例的狀态為已成功且代碼執行過程中不出錯的情況下。萬一出錯的話,那我們就可以使用then()方法的第二個回調函數或者catch()方法捕獲錯誤。

Promise.prototype.catch()

catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定發生錯誤時的回調函數。

function readText(url) { var p = new Promise(function (resolve, reject) { $.ajax({ type: "get", url: url, success: function (text) { resolve(text); }, error:function(err){ reject(err); } }) }) return p; // {pending} } readText("../data/1.txt").then(res => { console.log("line 93", res) }).catch(err => { console.log(err); })

上面代碼中,readText()方法返回一個 Promise 對象,如果該對象狀态變為resolved,則會調用then()方法指定的回調函數;如果異步操作抛出錯誤,狀态就會變為rejected,就會調用catch()方法指定的回調函數,處理這個錯誤。另外,then()方法指定的回調函數,如果運行中抛出錯誤,也會被catch()方法捕獲。

then和catch的鍊式操作

then()和catch()配合使用,實現的鍊式操作效果更佳。

var p = new Promise(function (resolve, reject) { setTimeout(function () { resolve(1); }, 2000); }) console.log("line 21", p); p.then(function (num) { console.log("line 23:", num); return 2; }).then(function (num) { console.log("line 25:", num); }).then(function (num) { console.log("line 28:", num); }).catch(function (err) { console.log(err); }) 結果: line 21 Promise {<pending>} ...等待2s後 line 23: 1 line 25: 2 line 28: 3

上面的代碼使用then catch方法進行鍊式調用,依次指定了三個then()的回調函數以及一個catch()的回調函數。第一個回調函數完成以後,會将返回結果作為參數,傳入第二個回調函數,同樣第二個回調函數完成後,會将返回結果作為參數,傳入第三個回調函數,以此類推。當然這裡同樣也有一個前提,那就是Promise實例的狀态為已成功且代碼執行過程中不出錯的情況下。萬一出錯的話,那catch()方法剛好捕獲鍊式操作過程中出現的錯誤。

如果then方法中回調函數的返回值也是一個Promise實例;

function readText(url) { var p = new Promise(function (resolve, reject) { $.ajax({ type: "get", url: url, success: function (text) { resolve(text); }, error: function (err) { reject(err); } }) }) return p; // {pending} } // ajax恐怖回調的解決方式1: // T = T1 T2 T3 (時間) var str = ""; readText("../data/1.txt").then(txt => { console.log("line 36", txt); str = txt; return readText("../data/2.txt"); }).then(txt => { console.log("line 40", txt); str = txt; return readText("../data/3.txt"); }).then(txt => { console.log("line 44", txt); str = txt; return str; }).then(txt => { console.log("line 48", txt); }).catch(err => { console.log("失敗:", err); }) 結果: line 36 11111 line 40 22222 line 44 33333 line 48 111112222233333

上面代碼中,第一個then方法指定的回調函數,返回的是另一個Promise對象。這時,第二個then方法指定的回調函數,就會等待這個新的Promise對象狀态發生變化。如果變為resolved,繼續鍊式調用後面then方法中指定的回調函數,如果狀态變為rejected,就會被catch方法捕獲。

注意:Promise 實例的錯誤具有“冒泡”性質,會一直向後傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch語句捕獲。

Promise的靜态方法

所謂Promise靜态方法,即将封裝的方法存儲在構造函數Promise上,由構造函數自身調用。

Promise.all

Promise.all()方法用于将多個 Promise 實例,包裝成一個新的 Promise 實例。

const p = Promise.all([p1, p2, p3]);

相較于then方法的鍊式操作,Promise.all()更注重并發,即依次按順序發送多個請求,等待所有的請求均有結果之後,對應請求順序将數據依次存放到數組中返回。

模拟封裝(簡易版) Promise.myAll = function (list) { return new Promise(function (resolve, reject) { // 返回一個新的Promise實例 var arr = []; for (let i = 0; i < list.length; i ) { let p = list[i];// 每一個Promise實例(如果傳入的數據非Promise實例 會通過Promise.resolve轉化) // 提前規定好每一個實例 成功和失敗時執行的内容 => 請求有結果之後會執行 p.then(res => { //等待異步操作有結果之後 對應下标放到數組中 arr[i] = res; if (arr.length === list.length) { // 所有的請求全都成功 => (也是一個異步操作) resolve(arr); } }).catch(err => { //隻要有一個失敗 就走catch console.log(err); reject(err); }) } }) } const p = Promise.myAll([p1, p2, p3]);

通過以上代碼可以發現: (摘自ECMAScript 6 入門)

Promise.all()方法接受一個數組作為參數,p1、p2、p3都是 Promise 實例,如果不是,就會先調用下面講到的Promise.resolve方法,将參數轉為 Promise 實例,再進一步處理。另外,Promise.all()方法的參數可以不是數組,但必須具有 Iterator 接口,且返回的每個成員都是 Promise 實例。

p的狀态由p1、p2、p3決定,分成兩種情況。

(1)隻有p1、p2、p3的狀态都變成fulfilled,p的狀态才會變成fulfilled,此時p1、p2、p3的返回值組成一個數組,傳遞給p的回調函數。

(2)隻要p1、p2、p3之中有一個被rejected,p的狀态就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。

Promise.race

Promise.race()方法同樣是将多個 Promise 實例,包裝成一個新的 Promise 實例。

const p = Promise.race([p1, p2, p3]);

相較于Promise.all方法, Promise.race方法更側重狀态改變的速度。

模拟封裝(簡易版) Promise.myRace = function (list) { return new Promise(function (resolve, reject) { // 返回一個新的Promise實例 for (let i = 0; i < list.length; i ) { let p = list[i];// 每一個Promise實例 (如果傳入的數據非Promise實例 會通過Promise.resolve轉化) // 提前規定好每一個實例 成功和失敗時執行的内容 => 請求有結果之後會執行 p.then(res => { //等待異步操作有結果之後 對應下标放到數組中 resolve(res); }).catch(err => { reject(err); }) } }) } const p = Promise.myRace([p1, p2, p3]);

通過以上代碼可以發現: (摘自ECMAScript 6 入門)

隻要p1、p2、p3之中有一個實例率先改變狀态,p的狀态就跟着改變。那個率先改變的 Promise 實例的返回值,就傳遞給p的回調函數。

Promise.allSettled

Promise.allSettled()方法同樣是将多個 Promise 實例,包裝成一個新的 Promise 實例。

const p = Promise.allSettled([p1, p2, p3]);

相較于Promise.all方法,Promise.allSettled跟注重于不管每一個Promise實例是成功還是失敗,都會将結果依次按請求順序放到數組中。

模拟封裝(簡易版) Promise.myAllSettled = function (list) { return new Promise(function (resolve, reject) { // 返回一個新的Promise實例 var arr = []; for (let i = 0; i < list.length; i ) { let p = list[i];// 每一個Promise實例 (如果傳入的數據非Promise實例 會通過Promise.resolve轉化) // 提前規定好每一個實例 成功和失敗時執行的内容 => 請求有結果之後會執行 p.then(res => { //等待異步操作有結果之後 對應下标放到數組中 var obj = { status: "fulfilled", value: txt }; arr[i] = obj; if (arr.length === list.length) { // 請求全都成功 => arr(異步) resolve(arr); } }).catch(err => { var obj = { status: "rejected", reason: err }; arr[i] = obj; if (arr.length === list.length) { // 請求全都成功 => arr(異步) resolve(arr); } }) } }) } const p = Promise.myAllSettled([p1, p2, p3])

值得注意的地方時,在将數據放到數組的過程中,傳入的Promise實例的狀态不同,放置的結果稍有不同(數組中的每個成員是一個對象,對象的格式是固定的)。

// 異步操作成功時 {status: 'fulfilled', value: value} // 異步操作失敗時 {status: 'rejected', reason: reason}

成員對象的status屬性的值隻可能是字符串fulfilled或字符串rejected,用來區分異步操作是成功還是失敗。如果是成功(fulfilled),對象會有value屬性,如果是失敗(rejected),會有reason屬性,對應兩種狀态時前面異步操作的返回值。

總結

有了Promise對象,就可以将異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise對象提供統一的接口,使得控制異步操作更加容易。

Promise也有一些缺點。首先,無法取消Promise,一旦新建它就會立即執行,無法中途取消。其次,如果不設置回調函數,Promise内部抛出的錯誤,不會反應到外部。第三,當處于pending狀态時,無法得知目前進展到哪一個階段(剛剛開始還是即将完成)。

升級用法 async await

ES2017 标準引入了 async 函數,使得異步操作變得更加方便。

async 函數是什麼?一句話,它就是 Generator 函數的語法糖。

基本用法

async function fn() { var str = ""; var text = await readText("../data/1.txt"); // Promise實例 console.log("line 29:",text); str = text; var text = await readText("../data/2.txt"); // Promise實例 console.log("line 33:",text); str = text; var text = await readText("../data/3.txt"); // Promise實例 console.log("line 37:",text); str = text; return str; } fn().then(res=>{ console.log("line 44:",res) }).catch(err=>{ console.log("line 46:",err) }) 結果: line 29: 11111 line 33: 22222 line 37: 33333 line 44: 111112222233333

async函數的返回值也是一個Promise實例,在async函數執行的過程中,一旦遇到await就會先返回pending(進行中)狀态的Promise實例,等待異步操作有結果之後,繼續執行await之後的語句,依次類推,語句全部執行完畢且無錯誤的情況下,則返回的Promise實例會變為已成功,否則會變為已失敗。

你學會了麼?文章底部留言告訴我喲。

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关圖文资讯推荐

热门圖文资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved