回顧JS異步編程方法的發展,主要有如下幾種方式:node
顯示購物車商品列表的頁面,用戶能夠勾選想要刪除商品(單選或多選),點擊確認刪除按鈕後,將已勾選的商品清除購物車,頁面顯示剩餘商品。express
爲了便於本文內容闡述,假設後端沒有提供一個批量刪除商品的接口,因此對用戶選擇的商品列表,須要逐個調用刪除接口。編程
用一個定時器表明一次接口請求。那思路就是遍歷存放用戶已選擇商品的id數組,逐個發起刪除請求del,待所有刪除完成後,調用獲取購物車商品列表的接口getsegmentfault
let ids = [1, 2, 3] // 假設已選擇三個商品 let len = ids.length let count = 0 let start // 便於後面計算執行時間
傳統常規的寫法,若是是多個繼行任務就會陷入回調地獄。好比此例中get
做爲del
的回調函數後端
let get = () => { setTimeout(() => { console.log(`get:${new Date() -start}ms`) }, 1000) } let del = (id, cb) => { setTimeout(() => { console.log(id) count++ if (count === len) { cb() } }, 1000) } let confirmDel = () => { start = new Date() for (id of ids) { del(id, get) } console.log(`done:${new Date() -start}ms`) } confirmDel()
注意觀察和對比done的打印順序和get完成時間。
setTimeout是異步執行的,沒有阻塞主流程的執行,因此done最早打印。
三個del任務是並行的,加上一個回調執行時間,因此整個點擊刪除按鈕事件耗時2秒左右數組
done:1ms 1 2 3 get:2007ms
let getP = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(`get:${new Date() -start}ms`) resolve() }, 1000) }) } let delP = (id, cb) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(id) count++ if (count === len) { cb() } resolve() }, 1000) }) } let confirmDelP = () => { start = new Date() for (id of ids) { delP(id, getP) } console.log(`done:${new Date() -start}ms`) } confirmDelP()
單純經常使用Promise寫法,看上去結構跟回調寫法同樣,並且運行時間也同樣。promise
done:2ms 1 2 3 get:2007ms
可是,若是使用Promise.all方法,就能很好將併發任務(三個del)和繼發任務(get)區分開了,就是get不用嵌入回調中了。瀏覽器
Promise對象then / catch / all / race / finally,以及resolve / reject更多內容請參閱MDN
併發
let delP_1 = (id) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(id) resolve() }, 1000) }) } let getP_1 = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(`get:${new Date() -start}ms`) resolve() }, 1000) }) } let confirmDelP_all = () => { start = new Date() let p_Arr = ids.map(id => delP_1(id)) Promise.all(p_Arr) .then(() => { return getP_1() }) .then(() => { console.log(`done:${new Date() -start}ms`) }) } confirmDelP_all()
在這裏,代碼的語義就很直觀了,先併發三個刪除del
,所有成功後執行get
,get
成功後done
。
注意看done
的打印順序app
1 2 3 get:2008ms done:2010ms
Generator
類型是一種特殊的函數,它擁有本身獨特的語法和方法屬性。好比函數名前加*,配合yield 返回異步回調結果, 經過next 傳入函數、next返回特殊的包含value和done屬性的對象等等,具體見MDN
Generator
是一種惰性求值函數,執行一次next()纔開啓一次執行,到yield又中斷,等待下一次next()。因此本人更喜歡叫它步進函數,很是適合執行繼發任務
假設如今每個接口請求都是繼發任務,就是說只有當上一個請求成功後,纔開始下一個請求。在實際的場景中,一般是當前請求須要使用上一個請求返回的結果數據。此時使用Generator
函數是最好的方式。
let generator let getG = () => { setTimeout(() => { console.log(`get:${new Date() -start}ms`) generator.next() }, 1000) } let delG = (id) => { setTimeout(() => { console.log(id) generator.next() }, 1000) } function *confimrDelG () { start = new Date() for (id of ids) { yield delG(id) } yield getG() console.log(`done:${new Date() -start}ms`) } generator = confimrDelG() generator.next() console.log('會被阻塞嗎?')
觀察打印的時間,四個異步任務4秒左右。
注意"阻塞「文字最早打印
會被阻塞嗎? 1 2 3 get:4009ms done:4011ms
我理解Generator
就是一個用來裝載異步繼發任務的容器,不阻塞容器外部流程,可是容器內部任務用yield
設置斷點,用next
步進執行,能夠經過next向下一步任務傳值,或者直接使用yield返回的上一任務結果。
咱們先看MDN上關於async function怎麼說的:
When an async function is called, it returns a Promise. When the async function returns a value, the Promise will be resolved with the returned value. When the async function throws an exception or some value, the Promise will be rejected with the thrown value.
也就是說async函數會返回一個Promise對象。
例子顯示下,咱們先用Promise
寫法
function imPromise(num) { return new Promise(function (resolve, reject) { if (num > 0) { resolve(num); } else { reject(num); } }) } imPromise(1).then(function (v) { console.log(v); // 1 }) imPromise(0).catch(function (v) { console.log(v); // 0 })
再用Async
寫法
async function imAsync(num) { if (num > 0) { return num // 這裏至關於resolve(num) } else { throw num // 這裏至關於reject(num) } } imAsync(1).then(function (v) { console.log(v); // 1 }); // 注意這裏是catch imAsync(0).catch(function (v) { console.log(v); // 0 })
因此理解Async
爲new Promise
的語法糖也是這個緣由。但要注意一點的是上面imPromise
函數和imAsync
函數調用返回的結果區別。
`new Promise`生成的是一個`pending`狀態的`Promise`對象,而`async`返回的是一個`resolved`或`rejected`狀態的`Promise`對象,就是一個已經終結狀態的`promise`對象。理解這點,對下面的`await`理解很重要。
let p = imPromise(1) console.log(p) // Promise { pending } let a = imAsync(1) console.log(a) // Promise { resolved }
再來看看MDN對於await是怎麼說的:
An async function can contain an await expression, that pauses the execution of the async function and watis for the passed Promise's resolution, and then resumes the async function's execution and returns the resolved value.
await會暫停當前async函數的執行,等待後面的Promise的計算結果返回之後再繼續執行當前的async函數
await等待一個Promise對象從pending狀態到resoled或rejected狀態的這段時間。
因此若是要實現中斷步進執行的效果,await
後面接的必須是一個pedding
狀態的promise
對象,其它狀態的promise
對象或非promise
對象一律不等待。
這也是await
和yield
的區別(yield
無論後面是什麼,執行完緊接着的表達式就中斷)。
Promise
解決callback
嵌套致使回調地獄的問題,但實際上並不完全,仍是在then
中使用了回調函數。而async / await
使得異步回調在寫法上完成沒有,就像同步寫法同樣。
看個例子:
// callback get((a) => { (a,b) => { (b,c) => { (c,d) => { (d,e) => { console.log(e) } } } } })
// promise get() .then(a => p1(a)) .then(b => p1(b)) .then(c => p1(c)) .then(d => p1(d)) .then(e => {console.log(e)})
// async / await (async (a) => { const b = await A(a); const c = await A(b); const d = await A(c); const e = await A(d); console.log(e) })()
咱們用async / await
改寫上面Generator
的例子
let delP_1 = (id) => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(id) resolve() }, 1000) }) } let getP_1 = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log(`get:${new Date() -start}ms`) resolve() }, 1000) }) } async function confimrDelAsync () { start = new Date() for (id of ids) { await delP_1(id) } await getP_1() console.log(`done:${new Date() -start}ms`) } confimrDelAsync() console.log('被阻塞了嗎?')
打印結果基本跟generator
同樣。但在語義上更明確。
被阻塞了嗎? 1 2 3 get:4014ms done:4016ms
let delP_1 = (id) => { setTimeout(() => { console.log(id) }, 1000) } let getP_1 = () => { setTimeout(() => { console.log(`get:${new Date() -start}ms`) }, 1000) } async function confimrDelAsync () { start = new Date() for (id of ids) { await delP_1(id) } await getP_1() console.log(`done:${new Date() -start}ms`) } confimrDelAsync() console.log('被阻塞了嗎?')
不返回Promise
對象,或者使promise
對象處理resoled
狀態,就能夠不執行等待。但這樣的寫法跟直接用同步方式寫同樣,因此並不推薦,顯得畫蛇添足。
done:4ms 1 2 3 get:1009ms
若是事件函數中併發任務和繼發任務都有,此時使用async / await
纔是最好的解決方式。其中的併發任務用promise.all
實現,由於它返回的正是await
可用的pending
狀態的Promise
對象。
let delP_1 = (id) => { setTimeout(() => { console.log(id) resolve() }, 1000) } let getP_1 = () => { setTimeout(() => { console.log(`get:${new Date() -start}ms`) resolve() }, 1000) } async function confimrDelAsync_all () { start = new Date() let p_Arr = ids.map(id => delP_1(id)) await Promise.all(p_Arr) await getP_1() console.log(`done:${new Date() -start}ms`) } confimrDelAsync_all() console.log('被阻塞了嗎?')
觀察時間是繼發任務的一半。且不阻塞主流程。
被阻塞了嗎? 1 2 3 get:2009ms done:2010ms
因此說async
是promise
的語法糖,可是函數返回的promise
的狀態是不同的。說await
是yield
的語法糖,可是await
只能接受pending
狀態的promise
對象
async
能夠單獨使用,await
不能單獨使用,只能在async
函數體內使用
因此針對開頭的需求:
顯示購物車商品列表的頁面,用戶能夠勾選想要刪除商品(單選或多選),點擊確認刪除按鈕後,將已勾選的商品清除購物車,頁面顯示剩餘商品。
最好的解決方案是: `promise.all` 與 `async / await`結合 其次是: `promise.all`
在實際項目中還應該加上捕獲錯誤的代碼。
在async / await
中結合try...catch
在promise
中,由於錯誤具備冒泡以性質,因此在結尾加上.catch
便可。
文章只是本身的一個併發和繼發混合需求引起的知識總結。但JS編程還有不少內容,包括異步事件、事件循環(瀏覽器和nodejs區別)、異步任務錯誤的捕獲、promise/generator/async具體API細節等。還須要繼續學習。
https://blog.csdn.net/ken_ding/article/details/81201248
https://segmentfault.com/a/1190000009070711?from=timeline&isappinstalled=0#articleHeader5 《Javascript ES6 函數式編程入門指南》 第10章 使用Generator