1.Promise
(名字含義:promise為承諾,表示其他手段無法改變)
- Pending(進行中、未完成的)
- Resolved(已完成,又稱 Fulfilled)
- Rejected(已失敗)
promises的優勢
1.解決回調地獄
2.更好地進行錯誤捕獲
有時我們要進行一些相互間有依賴關系的異步操作,比如有多個請求,后一個的請求需要上一次請求的返回結果。過去常規做法只能 callback 層層嵌套,但嵌套層數過多的話就會有 callback hell 問題。比如下面代碼,可讀性和維護性都很差的。
firstAsync(function(data){ //處理得到的 data 數據 //.... secondAsync(function(data2){ //處理得到的 data2 數據 //.... thirdAsync(function(data3){ //處理得到的 data3 數據 //.... }); }); });
promise例子1
//創建一個Promise實例,獲取數據。並把數據傳遞給處理函數resolve和reject。需要注意的是Promise在聲明的時候就執行了。 var getUserInfo=new Promise(function(resolve,reject){ $.ajax({ type:"get", url:"index.aspx", success:function(){ if(res.code=="ok"){ resolve(res.msg)//在異步操作成功時調用 }else{ reject(res.msg);//在異步操作失敗時調用 } } }); }) //另一個ajax Promise對象, var getDataList=new Promise(function(resolve,reject){ $.ajax({ type:"get", url:"index.aspx", success:function(res){ if(res.code=="ok"){ resolve(res.msg)//在異步操作成功時調用 }else{ reject(res.msg);//在異步操作失敗時調用 } } }); }) //Promise的方法then,catch方法 getUserInfo.then(function(ResultJson){ //通過拿到的數據渲染頁面 }).catch(function(ErrMsg){ //獲取數據失敗時的處理邏輯 }) //Promise的all方法,等數組中的所有promise對象都完成執行 Promise.all([getUserInfo,getDataList]).then(function([ResultJson1,ResultJson2]){ //這里寫等這兩個ajax都成功返回數據才執行的業務邏輯 })
注意:成功的結果需要用resolve包裹,失敗的結果需要用reject包裹
promise例子2
ajax請求
var getData=new Promise(function(resolve,reject){ $.post("http://apptest.hcbkeji.com/php/option/activity/chevron_report_activity.php", {flag: 'click',act:'臨沂頁面',page:'臨沂上報活動'}, function (res) { resolve(res) } ); }) getData.then(res=>{ console.log(res) //{"type":"ok"} })
promise例子3
var test=new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('hello world') },2000) }) test.then(res=>{ console.log(res) }) console.log('雖然在后面,但是我先執行') //打印結果 // 雖然在后面,但是我先執行 // hello world
promise例子4
function mytest(){ return new Promise((resolve,reject)=>{ $.post("http://apptest.hcbkeji.com/php/option/activity/chevron_report_activity.php", {flag: 'click',act:'臨沂頁面',page:'臨沂上報活動'}, function (res) { var res=JSON.parse(res) resolve(res) } ); }) } mytest().then(res=>{ console.log(res) }) console.log('雖然在后面,但是我先執行')
2.async
async使用
作為一個關鍵字放到函數前面,用於表示函數是一個異步函數,因為async就是異步的意思, 異步函數也就意味着該函數的執行不會阻塞后面代碼的執行
調用方法:async 函數也是函數,平時我們怎么使用函數就怎么使用它,直接加方法名括號調用。
async function test(){ return 'hello world' } test().then(res=>{ console.log(res) }) console.log('雖然在后面,但是我先執行') //打印結果 //雖然在后面,但是我先執行 //hello world
首先打印 ‘雖然在后面,但是我先執行’ ,后執行 打印 ‘hello world’
async的作用:輸出的是一個 Promise 對象
async function testAsync() { return "hello async"; } let result = testAsync(); console.log(result)
打印結果Promise {<resolved>: "hello async"} 從結果中可以看到async函數返回的是一個promise對象,如果在函數中 return 一個直接量,async 會把這個直接量通過 Promise.resolve()
封裝成 Promise 對象。
async function testAsync1() { console.log("hello async"); } let result1 = testAsync1(); console.log(result1);
結果返回Promise.resolve(undefined) 因為使用async 但是函數沒有return一個直接量
async 函數(包含函數語句、函數表達式、Lambda表達式)會返回一個 Promise 對象,如果在函數中 return
一個直接量,async 會把這個直接量通過 Promise.resolve()
封裝成 Promise 對象。
async 函數返回的是一個 Promise 對象,所以在最外層不能用 await 獲取其返回值的情況下,我們當然應該用原來的方式:then()
鏈來處理這個 Promise 對象。
Promise 的特點是無等待,所以在沒有 await
的情況下執行 async 函數,它會立即執行,返回一個 Promise 對象,並且,絕不會阻塞后面的語句。這和普通返回 Promise 對象的函數並無二致。
Promise 有一個resolved,這是async 函數內部的實現原理。如果async 函數中有返回一個值 ,當調用該函數時,內部會調用Promise.solve() 方法把它轉化成一個promise 對象作為返回,但如果timeout 函數內部拋出錯誤呢? 那么就會調用Promise.reject() 返回一個promise 對象, 這時修改一下timeout 函數
async function timeout(flag) { if (flag) { return 'hello world' } else { throw 'my god, failure' } } console.log(timeout(true)) // 調用Promise.resolve() 返回promise 對象。 console.log(timeout(false)); // 調用Promise.reject() 返回promise 對象。
控制台如下:
如果函數內部拋出錯誤, promise 對象有一個catch 方法進行捕獲。
timeout(false).catch(err => { console.log(err) })
3.await
因為 async 函數返回一個 Promise 對象,所以 await 可以用於等待一個 async 函數的返回值——這也可以說是 await 在等 async 函數,但要清楚,它等的實際是一個返回值。注意到 await 不僅僅用於等 Promise 對象,它可以等任意表達式的結果,所以,await 后面實際是可以接普通函數調用或者promise對象
注意:await 命令只能用在 async 函數之中,如果用在普通函數,就會報錯。
function getSomething() { return "something"; } async function testAsync() { return Promise.resolve("hello async"); } async function test() { const v1 = await getSomething(); //await后接普通函數調用 const v2 = await testAsync(); //await后接async promise對象 console.log(v1, v2); } test(); //打印結果something hello async
await
是個運算符,用於組成表達式,await 表達式的運算結果取決於它等的東西。
如果它等到的不是一個 Promise 對象,那 await 表達式的運算結果就是它等到的東西。
如果它等到的是一個 Promise 對象,await 就忙起來了,它會阻塞后面的代碼,等着 Promise 對象 resolve,然后得到 resolve 的值,作為 await 表達式的運算結果。
await 必須用在 async 函數中的原因。async 函數調用不會造成阻塞,它內部所有的阻塞都被封裝在一個 Promise 對象中異步執行。
綜合:上面已經說明了 async 會將其后的函數(函數表達式或 Lambda)的返回值封裝成一個 Promise 對象,而 await 會等待這個 Promise 完成,並將其 resolve 的結果返回出來。
使用promise和async await比較
使用promise
function takeLongTime(n){ return new Promise(resolve=>{ setTimeout(()=>resolve(n+200),n) }) } function step1(n){ console.log(`step1 with ${n}`) return takeLongTime(n); } function step2(n){ console.log(`step2 with ${n}`) return takeLongTime(n); } function step3(n){ console.log(`step3 with ${n}`) return takeLongTime(n); } function run(){ console.time('run') const time1=300; step1(time1) .then(time2=>step2(time2)) .then(time3=>step3(time3)) .then(result=>{ console.log(`resutlt is ${result}`) console.timeEnd('run') }) } run()
打印結果
step1 with 300 step2 with 500 step3 with 700 run: 1504.652099609375ms
使用async await
function takeLongTime(n){ return new Promise(resolve=>{ setTimeout(()=>resolve(n+200),n) }) } function step1(n){ console.log(`step1 with ${n}`) return takeLongTime(n); } function step2(n){ console.log(`step2 with ${n}`) return takeLongTime(n); } function step3(n){ console.log(`step3 with ${n}`) return takeLongTime(n); } async function run(){ console.time('run') const time1=300 const time2=await step1(time1) const time3=await step1(time2) const result=await step1(time3) console.log(`result is ${result}`) console.timeEnd('run') } run()
結果和之前的 Promise 實現是一樣的,但是這個代碼看起來是不是清晰得多,幾乎跟同步代碼一樣
await 命令后面的 Promise 對象,運行結果可能是 rejected,所以最好把 await 命令放在 try...catch 代碼塊中
async function myFunction() { try { await somethingThatReturnsAPromise(); } catch (err) { console.log(err); } } // 另一種寫法 async function myFunction() { await somethingThatReturnsAPromise().catch(function (err){ console.log(err); }); }
async function dbFuc(db) { let docs = [{}, {}, {}]; // 報錯 docs.forEach(function (doc) { await db.post(doc); }); }
await 關鍵字,await是等待的意思,那么它等待什么呢,它后面跟着什么呢?其實它后面可以放任何表達式,不過我們更多的是放一個返回promise 對象的表達式。注意await 關鍵字只能放到async 函數里面
現在寫一個函數,讓它返回promise 對象,該函數的作用是2s 之后讓數值乘以2
// 2s 之后返回雙倍的值 function doubleAfter2seconds(num) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(2 * num) }, 2000); } ) }
現在再寫一個async 函數,從而可以使用await 關鍵字, await 后面放置的就是返回promise對象的一個表達式,所以它后面可以寫上 doubleAfter2seconds 函數的調用
async function testResult() { let result = await doubleAfter2seconds(30); console.log(result); }
現在調用testResult 函數
testResult();
打開控制台,2s 之后,輸出了60.
現在我們看看代碼的執行過程,調用testResult 函數,它里面遇到了await, await 表示等一下,代碼就暫停到這里,不再向下執行了,它等什么呢?等后面的promise對象執行完畢,然后拿到promise resolve 的值並進行返回,返回值拿到之后,它繼續向下執行。具體到 我們的代碼, 遇到await 之后,代碼就暫停執行了, 等待doubleAfter2seconds(30) 執行完畢,doubleAfter2seconds(30) 返回的promise 開始執行,2秒 之后,promise resolve 了, 並返回了值為60, 這時await 才拿到返回值60, 然后賦值給result, 暫停結束,代碼才開始繼續執行,執行 console.log語句。
就這一個函數,我們可能看不出async/await 的作用,如果我們要計算3個數的值,然后把得到的值進行輸出呢?
async function testResult() { let first = await doubleAfter2seconds(30); let second = await doubleAfter2seconds(50); let third = await doubleAfter2seconds(30); console.log(first + second + third); }
6秒后,控制台輸出220, 我們可以看到,寫異步代碼就像寫同步代碼一樣了,再也沒有回調地域了。
補充:在vue中使用
設計使用場景,一個手機充值活動,實現選擇用戶的省市,點擊充值按鈕,彈出相應省份的充值列表,2個請求。
請求1 獲取所在省市: 根據手機號得到省和市 方法命名為getLocation 接受一個參數phoneNum 返回結果 province 和city
請求2 獲取可充值面值列表:根據省和市得到充值面值列表 方法命名為getFaceList 接受兩個參數province 和city 返回充值列表
我們首先要根據手機號得到省和市,所以寫一個方法來發送請求獲取省和市,方法命名為getLocation, 接受一個參數phoneNum ,當獲取到城市位置以后,我們再發送請求獲取充值面值,所以還要再寫一個方法getFaceList, 它接受兩個參數, province 和city,
methods: { //獲取到城市信息 getLocation(phoneNum) { return axios.post('phoneLocation', {phoneNum:phoneNum}) }, // 獲取面值 getFaceList(province, city) { return axios.post('/faceList', {province:province,city:city}) }, // 點擊確定按鈕時,顯示面值列表 getFaceResult () { this.getLocation(this.phoneNum) .then(res => { if (res.code=='ok') { let province = res.data.obj.province; let city = res.data.obj.city; this.getFaceList(province, city) .then(res => { if(res.code=='ok') { //最終獲取到面值列表 this.faceList = res.data.obj } }) } }) .catch(err => { console.log(err) }) } }
現在點擊確定按鈕,可以看到頁面中輸出了 從后台返回的面值列表。這時你看到了then 的鏈式寫法,有一點回調地域的感覺。現在我們在有async/ await 來改造一下。
首先把 getFaceResult 轉化成一個async 函數,就是在其前面加async, 因為它的調用方法和普通函數的調用方法是一致,所以沒有什么問題。然后就把 getLocation 和
methods: { //獲取到城市信息 getLocation(phoneNum) { return axios.post('phoneLocation', {phoneNum:phoneNum}) }, // 獲取面值 getFaceList(province, city) { return axios.post('/faceList', {province:province,city:city}) }, // 點擊確定按鈕時,顯示面值列表 async getFaceResult () { let location = await this.getLocation(this.phoneNum); if (location.code=='ok') { let province = location.data.obj.province; let city = location.data.obj.city; let result = await this.getFaceList(province, city); if (result.code=='ok') { this.faceList = result.data.obj; } } } }
現在代碼的書寫方式,就像寫同步代碼一樣,沒有回調的感覺,非常舒服。
現在就還差一點需要說明,那就是怎么處理異常,如果請求發生異常,怎么處理? 它用的是try/catch 來捕獲異常,把await 放到 try 中進行執行,如有異常,就使用catch 進行處理。
methods: { //獲取到城市信息 getLocation(phoneNum) { return axios.post('phoneLocation', {phoneNum:phoneNum}) }, // 獲取面值 getFaceList(province, city) { return axios.post('/faceList', {province:province,city:city}) }, // 點擊確定按鈕時,顯示面值列表 async getFaceResult () { try { let location = await this.getLocation(this.phoneNum); if (location.code=='ok') { let province = location.data.obj.province; let city = location.data.obj.city; let result = await this.getFaceList(province, city); if (result.code=='ok') { this.faceList = result.data.obj; } } } catch(err) { console.log(err); } } }
參考:https://www.cnblogs.com/SamWeb/p/8417940.html
異步解決方案 async和await
前言
異步編程模式在前端開發過程中,顯得越來越重要。從最開始的XHR到封裝后的Ajax都在試圖解決異步編程過程中的問題。隨着ES6新標准的到來,處理異步數據流又有了新的方案。我們都知道,在傳統的ajax請求中,當異步請求之間的數據存在依賴關系的時候,就可能產生很難看的多層回調,俗稱'回調地獄'(callback hell),這卻讓人望而生畏,Promise的出現讓我們告別回調函數,寫出更優雅的異步代碼。在實踐過程中,卻發現Promise並不完美,Async/Await是近年來JavaScript添加的最革命性的的特性之一,Async/Await提供了一種使得異步代碼看起來像同步代碼的替代方法。接下來我們介紹這兩種處理異步編程的方案。
一、Promise的原理與基本語法
1.Promise的原理
Promise 是一種對異步操作的封裝,可以通過獨立的接口添加在異步操作執行成功、失敗時執行的方法。主流的規范是 Promises/A+。
Promise中有幾個狀態:
-
pending: 初始狀態, 非 fulfilled 或 rejected;
-
fulfilled: 成功的操作,為表述方便,fulfilled 使用 resolved 代替;
-
rejected: 失敗的操作。
pending可以轉化為fulfilled或rejected並且只能轉化一次,也就是說如果pending轉化到fulfilled狀態,那么就不能再轉化到rejected。並且fulfilled和rejected狀態只能由pending轉化而來,兩者之間不能互相轉換。
2.Promise的基本語法
-
Promise實例必須實現then這個方法
-
then()必須可以接收兩個函數作為參數
-
then()返回的必須是一個Promise實例
<script src="https://cdn.bootcss.com/bluebird/3.5.1/bluebird.min.js"></script>//如果低版本瀏覽器不支持Promise,通過cdn這種方式
<script type="text/javascript">
function loadImg(src) {
var promise = new Promise(function (resolve, reject) {
var img = document.createElement('img')
img.onload = function () {
resolve(img)
}
img.onerror = function () {
reject('圖片加載失敗')
}
img.src = src
})
return promise
}
var src = 'https://www.imooc.com/static/img/index/logo_new.png'
var result = loadImg(src)
result.then(function (img) {
console.log(1, img.width)
return img
}, function () {
console.log('error 1')
}).then(function (img) {
console.log(2, img.height)
})
</script>
二、Promise多個串聯操作
Promise還可以做更多的事情,比如,有若干個異步任務,需要先做任務1,如果成功后再做任務2,任何任務失敗則不再繼續並執行錯誤處理函數。要串行執行這樣的異步任務,不用Promise需要寫一層一層的嵌套代碼。
有了Promise,我們只需要簡單地寫job1.then(job2).then(job3).catch(handleError);
其中job1、job2和job3都是Promise對象。
比如我們想實現第一個圖片加載完成后,再加載第二個圖片,如果其中有一個執行失敗,就執行錯誤函數:
var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
var result1 = loadImg(src1) //result1是Promise對象
var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
var result2 = loadImg(src2) //result2是Promise對象
result1.then(function (img1) {
console.log('第一個圖片加載完成', img1.width)
return result2 // 鏈式操作
}).then(function (img2) {
console.log('第二個圖片加載完成', img2.width)
}).catch(function (ex) {
console.log(ex)
})
這里需注意的是:then 方法可以被同一個 promise 調用多次,then 方法必須返回一個 promise 對象。上例中result1.then如果沒有明文返回Promise實例,就默認為本身Promise實例即result1,result1.then返回了result2實例,后面再執行.then實際上執行的是result2.then
三、Promise常用方法
除了串行執行若干異步任務外,Promise還可以並行執行異步任務。
試想一個頁面聊天系統,我們需要從兩個不同的URL分別獲得用戶的個人信息和好友列表,這兩個任務是可以並行執行的,用Promise.all()實現如下:
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
// 同時執行p1和p2,並在它們都完成后執行then:
Promise.all([p1, p2]).then(function (results) {
console.log(results); // 獲得一個Array: ['P1', 'P2']
});
有些時候,多個異步任務是為了容錯。比如,同時向兩個URL讀取用戶的個人信息,只需要獲得先返回的結果即可。這種情況下,用Promise.race()實現:
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
console.log(result); // 'P1'
});
由於p1執行較快,Promise的then()將獲得結果'P1'。p2仍在繼續執行,但執行結果將被丟棄。
總結:Promise.all接受一個promise對象的數組,待全部完成之后,統一執行success;
Promise.race接受一個包含多個promise對象的數組,只要有一個完成,就執行success
接下來我們對上面的例子做下修改,加深對這兩者的理解:
var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
var result1 = loadImg(src1)
var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
var result2 = loadImg(src2)
Promise.all([result1, result2]).then(function (datas) {
console.log('all', datas[0])//<img src="https://www.imooc.com/static/img/index/logo_new.png">
console.log('all', datas[1])//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
})
Promise.race([result1, result2]).then(function (data) {
console.log('race', data)//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
})
如果我們組合使用Promise,就可以把很多異步任務以並行和串行的方式組合起來執行
四、Async/Await簡介與用法
異步操作是 JavaScript 編程的麻煩事,很多人認為async函數是異步操作的終極解決方案。
1、Async/Await簡介
-
async/await是寫異步代碼的新方式,優於回調函數和Promise。
-
async/await是基於Promise實現的,它不能用於普通的回調函數。
-
async/await與Promise一樣,是非阻塞的。
-
async/await使得異步代碼看起來像同步代碼,再也沒有回調函數。但是改變不了JS單線程、異步的本質。
2、Async/Await的用法
-
使用await,函數必須用async標識
-
await后面跟的是一個Promise實例
-
需要安裝babel-polyfill,安裝后記得引入 //npm i --save-dev babel-polyfill
function loadImg(src) {
const promise = new Promise(function (resolve, reject) {
const img = document.createElement('img')
img.onload = function () {
resolve(img)
}
img.onerror = function () {
reject('圖片加載失敗')
}
img.src = src
})
return promise
}
const src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
const src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
const load = async function(){
const result1 = await loadImg(src1)
console.log(result1)
const result2 = await loadImg(src2)
console.log(result2)
}
load()
當函數執行的時候,一旦遇到 await 就會先返回,等到觸發的異步操作完成,再接着執行函數體內后面的語句。
五、Async/Await錯誤處理
await 命令后面的 Promise 對象,運行結果可能是 rejected,所以最好把 await 命令放在 try...catch 代碼塊中。try..catch錯誤處理也比較符合我們平常編寫同步代碼時候處理的邏輯。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
六、為什么Async/Await更好?
Async/Await較Promise有諸多好處,以下介紹其中三種優勢:
1. 簡潔
使用Async/Await明顯節約了不少代碼。我們不需要寫.then,不需要寫匿名函數處理Promise的resolve值,也不需要定義多余的data變量,還避免了嵌套代碼。
2. 中間值
你很可能遇到過這樣的場景,調用promise1,使用promise1返回的結果去調用promise2,然后使用兩者的結果去調用promise3。你的代碼很可能是這樣的:
const makeRequest = () => {
return promise1()
.then(value1 => {
return promise2(value1)
.then(value2 => {
return promise3(value1, value2)
})
})
}
使用async/await的話,代碼會變得異常簡單和直觀
const makeRequest = async () => {
const value1 = await promise1()
const value2 = await promise2(value1)
return promise3(value1, value2)
}
3.條件語句
下面示例中,需要獲取數據,然后根據返回數據決定是直接返回,還是繼續獲取更多的數據。
const makeRequest = () => {
return getJSON()
.then(data => {
if (data.needsAnotherRequest) {
return makeAnotherRequest(data)
.then(moreData => {
console.log(moreData)
return moreData
})
} else {
console.log(data)
return data
}
})
}
代碼嵌套(6層)可讀性較差,它們傳達的意思只是需要將最終結果傳遞到最外層的Promise。使用async/await編寫可以大大地提高可讀性:
const makeRequest = async () => {
const data = await getJSON()
if (data.needsAnotherRequest) {
const moreData = await makeAnotherRequest(data);
console.log(moreData)
return moreData
} else {
console.log(data)
return data
}
}
參考文章
ES6 Promise 並行執行和順序執行
1.Promise.all 並行執行promise
getA和getB並行執行,然后輸出結果。如果有一個錯誤,就拋出錯誤
/** * 每一個promise都必須返回resolve結果才正確 * 每一個promise都不處理錯誤 */ const getA = new Promise((resolve, reject) => { //模擬異步任務 setTimeout(function(){ resolve(2); }, 1000) }) .then(result => result) const getB = new Promise((resolve, reject) => { setTimeout(function(){ // resolve(3); reject('Error in getB'); }, 1000) }) .then(result => result) Promise.all([getA, getB]).then(data=>{ console.log(data) }) .catch(e => console.log(e));
getA和getB並行執行,然后輸出結果。總是返回resolve結果
/** * 每一個promise自己處理錯誤 */ const getA = new Promise((resolve, reject) => { //模擬異步任務 setTimeout(function(){ resolve(2); }, 1000) }) .then(result => result) .catch(e=>{ }) const getB = new Promise((resolve, reject) => { setTimeout(function(){ // resolve(3); reject('Error in getB'); }, 1000) }) .then(result => result) .catch(e=>e) Promise.all([getA, getB]).then(data=>{ console.log(data) }) .catch(e => console.log(e));
2.順序執行promise
先getA然后getB執行,最后addAB
- 2.1 方法一——連續使用then鏈式操作
function getA(){ return new Promise(function(resolve, reject){ setTimeout(function(){ resolve(2); }, 1000); }); } function getB(){ return new Promise(function(resolve, reject){ setTimeout(function(){ resolve(3); }, 1000); }); } function addAB(a,b){ return a+b } function getResult(){ var obj={}; Promise.resolve().then(function(){ return getA() }) .then(function(a){ obj.a=a; }) .then(function(){ return getB() }) .then(function(b){ obj.b=b; return obj; }) .then(function(obj){ return addAB(obj['a'],obj['b']) }) .then(data=>{ console.log(data) }) .catch(e => console.log(e)); } getResult();
- 2.2 方法二——使用promise構建隊列
function getResult(){ var res=[]; // 構建隊列 function queue(arr) { var sequence = Promise.resolve(); arr.forEach(function (item) { sequence = sequence.then(item).then(data=>{ res.push(data); return res }) }) return sequence } // 執行隊列 queue([getA,getB]).then(data=>{ return addAB(data[0],data[1]) }) .then(data => { console.log(data) }) .catch(e => console.log(e)); } getResult();
- 2.3方法三——使用async、await實現類似同步編程
function getResult(){ async function queue(arr) { let res = [] for (let fn of arr) { var data= await fn(); res.push(data); } return await res } queue([getA,getB]) .then(data => { return addAB(data[0],data[1]) }).then(data=>console.log(data)) }
3. 總結
實現異步隊列函數的三種方式
方法一——連續使用then鏈式操作
方法二——使用promise構建隊列
方法三——使用async、await實現類似同步編程,async函數內部實現同步
參考
Promise的順序執行和並行執行
構建Promise隊列實現異步函數順序執行
補充
ES6 Promise中斷
中斷或取消Promise鏈的可行方案
(1)reject()
(2)throw new Error()
一般來說,不要在then方法里面定義 reject 狀態的回調函數(即then的第二個參數),總是使用catch方法。
當 async/await 遇上 forEach
當 async/await 遇上 forEach
在文章 ES7 中的 async await 中介紹了 async/await。本文將分析介紹當 async/await 遇上 forEach
出現的一些問題和解決方案。
問題描述
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
var getNumbers = () => {
return Promise.resolve([1, 2, 3])
}
var multi = num => {
return new Promise((resolve, reject) => {
setTimeout(
() => {
if (num) {
resolve(num * num)
}
else {
reject(
new Error('num not specified'))
}
},
1000)
})
}
async function test () {
var nums = await getNumbers()
nums.forEach(
async x => {
var res = await multi(x)
console.log(res)
})
}
test()
|
在這個例子中,通過 forEach
遍歷的將每一個數字都執行 multi
操作。代碼執行的結果是:1 秒后,一次性輸出1,4,9。這個結果和我們的預期有些區別,我們是希望每間隔 1 秒,然后依次輸出 1,4,9;所以當前代碼應該是並行執行了,而我們期望的應該是串行執行。
問題分析
JavaScript 中的循環數組遍歷
在 JavaScript 中提供了如下四種循環遍歷數組元素的方式:
-
for
這是循環遍歷數組元素最簡單的方式123for(i = 0; i < arr.length; i++) {console.log(arr[i]);} -
for-in
for-in
語句以任意順序遍歷一個對象的可枚舉屬性,對於數組即是數組下標,對於對象即是對象的 key 值。注意for-in
遍歷返回的對象屬性都是字符串類型,即使是數組下標,也是字符串 “0”, “1”, “2” 等等。[不推薦使用for-in
語句]123for (var index in myArray) {console. log(myArray[index]);} -
forEach
forEach
方法用於調用數組的每個元素,並將元素傳遞給回調函數;注意在回調函數中無法使用break
跳出當前循環,也無法使用return
返回值123myArray.forEach( function (value) {console. log(value);}); -
for-of
for-of
語句為各種 collection 集合對象專門定制的,遍歷集合對象的屬性值,注意和for-in
的區別123for (var value of myArray) {console.log( value);}
分析問題
在本例中 forEach
的回調函數是一個異步函數,異步函數中包含一個 await
等待 Promise 返回結果,我們期望數組元素串行執行這個異步操作,但是實際卻是並行執行了。
forEach
的 polyfill 參考:MDN-Array.prototype.forEach(),簡單點理解:
1
2
3
4
5
6
7
|
Array.prototype.forEach = function (callback) {
// this represents our array
for (let index = 0; index < this.length; index++) {
// We call the callback for each entry
callback(
this[index], index, this)
}
}
|
相當於 for
循環執行了這個異步函數,所以是並行執行,導致了一次性全部輸出結果:1,4,9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
async function test () {
var nums = await getNumbers()
// nums.forEach(async x => {
// var res = await multi(x)
// console.log(res)
// })
for(let index = 0; index < nums.length; index++) {
(
async x => {
var res = await multi(x)
console.log(res)
})(nums[index])
}
}
|
解決問題
方式一
我們可以改造一下 forEach
,確保每一個異步的回調執行完成后,才執行下一個
1
2
3
4
5
6
7
8
9
10
11
12
13
|
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array)
}
}
async function test () {
var nums = await getNumbers()
asyncForEach(nums,
async x => {
var res = await multi(x)
console.log(res)
})
}
|
方式二
使用 for-of
替代 for-each
。
for-of
可以遍歷各種集合對象的屬性值,要求被遍歷的對象需要實現迭代器 (iterator) 方法,例如 myObject[Symbol.iterator]()
用於告知 JS 引擎如何遍歷該對象。一個擁有 [Symbol.iterator]()
方法的對象被認為是可遍歷的。
1
2
3
4
5
6
7
8
|
var zeroesForeverIterator = {
[
Symbol.iterator]: function () {
return this;
},
next: function () {
return {done: false, value: 0};
}
};
|
如上就是一個最簡單的迭代器對象;for-of
遍歷對象時,先調用遍歷對象的迭代器方法 [Symbol.iterator]()
,該方法返回一個迭代器對象(迭代器對象中包含一個 next
方法);然后調用該迭代器對象上的 next
方法。
每次調用 next
方法都返回一個對象,其中 done
和 value
屬性用來表示遍歷是否結束和當前遍歷的屬性值,當 done
的值為 true
時,遍歷就停止了。
1
2
3
|
for (VAR of ITERABLE) {
STATEMENTS
}
|
等價於:
1
2
3
4
5
6
7
|
var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
VAR = $result.value;
STATEMENTS
$result = $iterator.next();
}
|
由此可以知道 for-of
和 forEach
遍歷元素時處理的方式是不同的。使用 for-of
替代 for-each
后代碼為:
1
2
3
4
5
6
7
|
async function test () {
var nums = await getNumbers()
for(let x of nums) {
var res = await multi(x)
console.log(res)
}
}
|
JavaScript 循環:如何處理 async/await
簡評:作者提供了在 JavaScript 循環中處理 async/await 的解決方案,可以參考參考。
同步循環
很久以前我寫的循環是這樣的:

后來 JavaScript 提供了很多新的特性,現在我們會更傾向於用下面這種寫法:

在開發過程可能會有這么一種需求,我們需要在循環中異步處理 item,那么可以怎么做呢?
異步循環
如何在循環中使用 await?我們試着寫一個異步函數,然后 await 每一次循環任務。

這個代碼會拋出一個錯誤,因為我們不能在同步方法中使用 await, processArray 確實是異步函數,但是 array.forEach 里的匿名函數是同步的。
1. 不要等待結果
要處理這個問題,我們可以把這個匿名函數定義為異步的:

但是這樣的話 forEach 方法就相當於異步的了,不會等待遍歷完所有的 item,例如下面這段代碼:

將會輸出:
Done!
1
2
3
如果你不需要等待這個循環完成,這樣就已經可以了。但是大部分情況我們還是需要等待這個循環完成才進行之后的操作。
2. 串行遍歷
要等待所有的結果返回,我們還是要回到老式的 for 循環寫法:

最后的結果符合我們的預期:
1
2
3
Done!
上面這段的遍歷代碼是串行執行的,我們也可以把它換成並行的。
3. 並行遍歷
我們可以稍微更改上面的代碼來編程並行的:

(注意:對於特別大的數組不建議使用這種寫法,太多的並行任務會加重 CPU 和內存的負荷)
原文: JavaScript loops — how to handle async/await
推薦閱讀: 前端開發備忘錄(合集)
歡迎關注:知乎專欄「極光日報」,每天為 Makers 導讀三篇優質英文文章。
async/await 原理及執行順序分析
之前寫了篇文《這一次,徹底理解Promise原理》,剖析了Promise的相關原理,反應不錯,這次把學習到的相關的知識也寫下。
我們都知道,Promise解決了回調地獄的問題,但是如果遇到復雜的業務,代碼里面會包含大量的 then 函數,使得代碼依然不是太容易閱讀。
基於這個原因,ES7 引入了 async/await,這是 JavaScript 異步編程的一個重大改進,提供了在不阻塞主線程的情況下使用同步代碼實現異步訪問資源的能力,並且使得代碼邏輯更加清晰,而且還支持 try-catch 來捕獲異常,非常符合人的線性思維。
所以,要研究一下如何實現 async/await。總的來說,async 是Generator函數的語法糖,並對Generator函數進行了改進。
Generator函數簡介
Generator 函數是一個狀態機,封裝了多個內部狀態。執行 Generator 函數會返回一個遍歷器對象,可以依次遍歷 Generator 函數內部的每一個狀態,但是只有調用next
方法才會遍歷下一個內部狀態,所以其實提供了一種可以暫停執行的函數。yield
表達式就是暫停標志。
有這樣一段代碼:
function* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator(); 復制代碼
調用及運行結果:
hw.next()// { value: 'hello', done: false } hw.next()// { value: 'world', done: false } hw.next()// { value: 'ending', done: true } hw.next()// { value: undefined, done: true } 復制代碼
由結果可以看出,Generator函數
被調用時並不會執行,只有當調用next方法
、內部指針指向該語句時才會執行,即函數可以暫停,也可以恢復執行
。每次調用遍歷器對象的next方法,就會返回一個有着value
和done
兩個屬性的對象。value
屬性表示當前的內部狀態的值,是yield表達式后面那個表達式的值;done
屬性是一個布爾值,表示是否遍歷結束。
Generator函數暫停恢復執行原理
要搞懂函數為何能暫停和恢復,那你首先要了解協程的概念。
一個線程(或函數)執行到一半,可以暫停執行,將執行權交給另一個線程(或函數),等到稍后收回執行權的時候,再恢復執行。這種可以並行執行、交換執行權的線程(或函數),就稱為協程。
協程是一種比線程更加輕量級的存在。普通線程是搶先式的,會爭奪cpu資源,而協程是合作的,可以把協程看成是跑在線程上的任務,一個線程上可以存在多個協程,但是在線程上同時只能執行一個協程。它的運行流程大致如下:
- 協程
A
開始執行 - 協程
A
執行到某個階段,進入暫停,執行權轉移到協程B
- 協程
B
執行完成或暫停,將執行權交還A
- 協程
A
恢復執行
協程遇到yield命令
就暫停,等到執行權返回,再從暫停的地方繼續往后執行。它的最大優點,就是代碼的寫法非常像同步操作,如果去除yield命令,簡直一模一樣。
執行器
通常,我們把執行生成器的代碼封裝成一個函數,並把這個執行生成器代碼的函數稱為執行器,co 模塊
就是一個著名的執行器。
Generator 是一個異步操作的容器。它的自動執行需要一種機制,當異步操作有了結果,能夠自動交回執行權。兩種方法可以做到這一點:
- 回調函數。將異步操作包裝成 Thunk 函數,在回調函數里面交回執行權。
- Promise 對象。將異步操作包裝成 Promise 對象,用then方法交回執行權。
一個基於 Promise 對象的簡單自動執行器:
function run(gen){ var g = gen(); function next(data){ var result = g.next(data); if (result.done) return result.value; result.value.then(function(data){ next(data); }); } next(); } 復制代碼
我們使用時,可以這樣使用即可,
function* foo() { let response1 = yield fetch('https://xxx') //返回promise對象 console.log('response1') console.log(response1) let response2 = yield fetch('https://xxx') //返回promise對象 console.log('response2') console.log(response2) } run(foo); 復制代碼
上面代碼中,只要 Generator 函數還沒執行到最后一步,next函數就調用自身,以此實現自動執行。通過使用生成器配合執行器,就能實現使用同步的方式寫出異步代碼了,這樣也大大加強了代碼的可讀性。
async/await
ES7 中引入了 async/await,這種方式能夠徹底告別執行器和生成器,實現更加直觀簡潔的代碼。根據 MDN 定義,async 是一個通過異步執行並隱式返回 Promise 作為結果的函數。可以說async 是Generator函數的語法糖,並對Generator函數進行了改進。
前文中的代碼,用async
實現是這樣:
const foo = async () => { let response1 = await fetch('https://xxx') console.log('response1') console.log(response1) let response2 = await fetch('https://xxx') console.log('response2') console.log(response2) } 復制代碼
一比較就會發現,async函數就是將 Generator 函數的星號(*)替換成async,將yield替換成await,僅此而已。
async函數對 Generator 函數的改進,體現在以下四點:
內置執行器
。Generator 函數的執行必須依靠執行器,而 async 函數自帶執行器,無需手動執行 next() 方法。更好的語義
。async和await,比起星號和yield,語義更清楚了。async表示函數里有異步操作,await表示緊跟在后面的表達式需要等待結果。更廣的適用性
。co模塊約定,yield命令后面只能是 Thunk 函數或 Promise 對象,而async函數的await命令后面,可以是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時會自動轉成立即 resolved 的 Promise 對象)。返回值是 Promise
。async 函數返回值是 Promise 對象,比 Generator 函數返回的 Iterator 對象方便,可以直接使用 then() 方法進行調用。
這里的重點是自帶了執行器,相當於把我們要額外做的(寫執行器/依賴co模塊)都封裝了在內部。比如:
async function fn(args) { // ... } 復制代碼
等同於:
function fn(args) { return spawn(function* () { // ... }); } function spawn(genF) { //spawn函數就是自動執行器,跟簡單版的思路是一樣的,多了Promise和容錯處理 return new Promise(function(resolve, reject) { const gen = genF(); function step(nextF) { let next; try { next = nextF(); } catch(e) { return reject(e); } if(next.done) { return resolve(next.value); } Promise.resolve(next.value).then(function(v) { step(function() { return gen.next(v); }); }, function(e) { step(function() { return gen.throw(e); }); }); } step(function() { return gen.next(undefined); }); }); } 復制代碼
async/await執行順序
通過上面的分析,我們知道async
隱式返回 Promise 作為結果的函數,那么可以簡單理解為,await后面的函數執行完畢時,await會產生一個微任務(Promise.then是微任務)。但是我們要注意這個微任務產生的時機,它是執行完await之后,直接跳出async函數,執行其他代碼(此處就是協程的運作,A暫停執行,控制權交給B)。其他代碼執行完畢后,再回到async函數去執行剩下的代碼,然后把await后面的代碼注冊到微任務隊列當中。我們來看個例子:
console.log('script start') async function async1() { await async2() console.log('async1 end') } async function async2() { console.log('async2 end') } async1() setTimeout(function() { console.log('setTimeout') }, 0) new Promise(resolve => { console.log('Promise') resolve() }) .then(function() { console.log('promise1') }) .then(function() { console.log('promise2') }) console.log('script end') // script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout 復制代碼
分析這段代碼:
- 執行代碼,輸出
script start
。 - 執行async1(),會調用async2(),然后輸出
async2 end
,此時將會保留async1函數的上下文,然后跳出async1函數。 - 遇到setTimeout,產生一個宏任務
- 執行Promise,輸出
Promise
。遇到then,產生第一個微任務 - 繼續執行代碼,輸出
script end
- 代碼邏輯執行完畢(當前宏任務執行完畢),開始執行當前宏任務產生的微任務隊列,輸出
promise1
,該微任務遇到then,產生一個新的微任務 - 執行產生的微任務,輸出
promise2
,當前微任務隊列執行完畢。執行權回到async1 - 執行await,實際上會產生一個promise返回,即
let promise_ = new Promise((resolve,reject){ resolve(undefined)}) 復制代碼
執行完成,執行await后面的語句,輸出async1 end
- 最后,執行下一個宏任務,即執行setTimeout,輸出
setTimeout
注意
新版的chrome瀏覽器中不是如上打印的,因為chrome優化了,await變得更快了,輸出為:
// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout 復制代碼
但是這種做法其實是違法了規范的,當然規范也是可以更改的,這是 V8 團隊的一個 PR ,目前新版打印已經修改。
知乎上也有相關討論,可以看看 www.zhihu.com/question/26…
參考資料
- 極客時間《瀏覽器工作原理與實踐》
- 阮一峰《es6入門》
Promise資料
最后
- 歡迎加我微信(winty230),拉你進技術群,長期交流學習...
- 歡迎關注「前端Q」,認真學前端,做個有專業的技術人...