我們知道Promise
與Async/await
函數都是用來解決JavaScript中的非同步問題的,從最開始的回撥函數處理非同步,到Promise
處理非同步,到Generator
處理非同步,再到Async/await
處理非同步,每一次的技術更新都使得JavaScript處理非同步的方式更加優雅,從目前來看,Async/await
被認為是非同步處理的終極解決方案,讓JS的非同步處理越來越像同步任務。非同步程式設計的最高境界,就是根本不用關心它是不是非同步。
1.回撥函數
從早期的Javascript程式碼來看,在ES6誕生之前,基本上所有的非同步處理都是基於回撥函數函數實現的,你們可能會見過下面這種程式碼:
ajax('aaa', () => { // callback 函數體 ajax('bbb', () => { // callback 函數體 ajax('ccc', () => { // callback 函數體 }) }) })
沒錯,在ES6出現之前,這種程式碼可以說是隨處可見。它雖然解決了非同步執行的問題,可隨之而來的是我們常聽說的回撥地獄問題:
所以,為了解決這個問題,社群最早提出和實現了Promise
,ES6將其寫進了語言標準,統一了用法。
2.Promise
Promise 是非同步程式設計的一種解決方案,比傳統的解決方案——回撥函數和事件——更合理和更強大。它就是為了解決回撥函數產生的問題而誕生的。
有了Promise
物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函數。此外,Promise
物件提供統一的介面,使得控制非同步操作更加容易。
所以上面那種回撥函數的方式我們可以改成這樣:(前提是ajax已用Promise包裝)
ajax('aaa').then(res=>{ return ajax('bbb') }).then(res=>{ return ajax('ccc') })
通過使用Promise
來處理非同步,比以往的回撥函數看起來更加清晰了,解決了回撥地獄的問題,Promise
的then
的鏈式呼叫更能讓人接受,也符合我們同步的思想。
但Promise也有它的缺點:
try catch
捕獲不到,只能只用then
的第二個回撥或catch
來捕獲let pro try{ pro = new Promise((resolve,reject) => { throw Error('err....') }) }catch(err){ console.log('catch',err) // 不會列印 } pro.catch(err=>{ console.log('promise',err) // 會列印 })
之前寫過一篇,講解了Promise如何使用以及內部實現原理。對Promise還不太理解的同學可以看看~
從如何使用到如何實現一個Promise
https://juejin.cn/post/7051364317119119396
3.Generator
Generator
函數是 ES6 提供的一種非同步程式設計解決方案,語法行為與傳統函數完全不同。Generator
函數將 JavaScript 非同步程式設計帶入了一個全新的階段。
宣告
與函數宣告類似,不同的是function
關鍵字與函數名之間有一個星號,以及函數體內部使用yield
表示式,定義不同的內部狀態(yield
在英語裡的意思就是「產出」)。
function* gen(x){ const y = yield x + 6; return y; } // yield 如果用在另外一個表示式中,要放在()裡面 // 像上面如果是在=右邊就不用加() function* genOne(x){ const y = `這是第一個 yield 執行:${yield x + 1}`; return y; }
執行
const g = gen(1); //執行 Generator 會返回一個Object,而不是像普通函數返回return 後面的值 g.next() // { value: 7, done: false } //呼叫指標的 next 方法,會從函數的頭部或上一次停下來的地方開始執行,直到遇到下一個 yield 表示式或return語句暫停,也就是執行yield 這一行 // 執行完成會返回一個 Object, // value 就是執行 yield 後面的值,done 表示函數是否執行完畢 g.next() // { value: undefined, done: true } // 因為最後一行 return y 被執行完成,所以done 為 true
呼叫 Generator 函數後,該函數並不執行,返回的也不是函數執行結果,而是一個指向內部狀態的指標物件,也就是遍歷器物件(Iterator Object)
。下一步,必須呼叫遍歷器物件的next
方法,使得指標移向下一個狀態。
所以上面的回撥函數又可以寫成這樣:
function *fetch() { yield ajax('aaa') yield ajax('bbb') yield ajax('ccc') } let gen = fetch() let res1 = gen.next() // { value: 'aaa', done: false } let res2 = gen.next() // { value: 'bbb', done: false } let res3 = gen.next() // { value: 'ccc', done: false } let res4 = gen.next() // { value: undefined, done: true } done為true表示執行結束
由於 Generator 函數返回的遍歷器物件,只有呼叫next
方法才會遍歷下一個內部狀態,所以其實提供了一種可以暫停執行的函數。yield
表示式就是暫停標誌。
遍歷器物件的next
方法的執行邏輯如下。
(1)遇到yield
表示式,就暫停執行後面的操作,並將緊跟在yield
後面的那個表示式的值,作為返回的物件的value
屬性值。
(2)下一次呼叫next
方法時,再繼續往下執行,直到遇到下一個yield
表示式。
(3)如果沒有再遇到新的yield
表示式,就一直執行到函數結束,直到return
語句為止,並將return
語句後面的表示式的值,作為返回的物件的value
屬性值。
(4)如果該函數沒有return
語句,則返回的物件的value
屬性值為undefined
。
yield
表示式本身沒有返回值,或者說總是返回undefined
。next
方法可以帶一個引數,該引數就會被當作上一個yield
表示式的返回值。
怎麼理解這句話?我們來看下面這個例子:
function* foo(x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z); } var a = foo(5); a.next() // Object{value:6, done:false} a.next() // Object{value:NaN, done:false} a.next() // Object{value:NaN, done:true} var b = foo(5); b.next() // { value:6, done:false } b.next(12) // { value:8, done:false } b.next(13) // { value:42, done:true }
由於yield
沒有返回值,所以(yield(x+1))執行後的值是undefined
,所以在第二次執行a.next()
是其實是執行的2*undefined
,所以值是NaN
,所以下面b的例子中,第二次執行b.next()
時傳入了12,它會當成第一次b.next()
的執行返回值,所以b的例子中能夠正確計算。這裡不能把next執行結果中的value值與yield返回值搞混了,它兩不是一個東西
yield與return的區別
相同點:
區別:
4.Async/await
Async/await
其實就是上面Generator
的語法糖,async
函數其實就相當於funciton *
的作用,而await
就相當與yield
的作用。而在async/await
機制中,自動包含了我們上述封裝出來的spawn
自動執行函數。
所以上面的回撥函數又可以寫的更加簡潔了:
async function fetch() { await ajax('aaa') await ajax('bbb') await ajax('ccc') } // 但這是在這三個請求有相互依賴的前提下可以這麼寫,不然會產生效能問題,因為你每一個請求都需要等待上一次請求完成後再發起請求,如果沒有相互依賴的情況下,建議讓它們同時發起請求,這裡可以使用Promise.all()來處理
async
函數對Generator
函數的改進,體現在以下四點:
async
函數執行與普通函數一樣,不像Generator
函數,需要呼叫next
方法,或使用co
模組才能真正執行async
和await
,比起星號和yield
,語意更清楚了。async
表示函數裡有非同步操作,await
表示緊跟在後面的表示式需要等待結果。co
模組約定,yield
命令後面只能是 Thunk 函數或 Promise 物件,而async
函數的await
命令後面,可以是 Promise 物件和原始型別的值(數值、字串和布林值,但這時會自動轉成立即 resolved 的 Promise 物件)。async
函數的返回值是 Promise 物件,這比 Generator 函數的返回值是 Iterator 物件方便多了。你可以用then
方法指定下一步的操作。async函數
async函數的返回值為Promise物件,所以它可以呼叫then方法
async function fn() { return 'async' } fn().then(res => { console.log(res) // 'async' })
await表示式
await 右側的表示式一般為 promise 物件, 但也可以是其它的值
如果表示式是 promise 物件, await 返回的是 promise 成功的值
如果表示式是其它值, 直接將此值作為 await 的返回值
await後面是Promise物件會阻塞後面的程式碼,Promise 物件 resolve,然後得到 resolve 的值,作為 await 表示式的運算結果
所以這就是await必須用在async的原因,async剛好返回一個Promise物件,可以非同步執行阻塞
function fn() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(1000) }, 1000); }) } function fn1() { return 'nanjiu' } async function fn2() { // const value = await fn() // await 右側表示式為Promise,得到的結果就是Promise成功的value // const value = await '南玖' const value = await fn1() console.log('value', value) } fn2() // value 'nanjiu'
後三種方案都是為解決傳統的回撥函數而提出的,所以它們相對於回撥函數的優勢不言而喻。而async/await
又是Generator
函數的語法糖。
try catch
捕獲不到,只能只用then
的第二個回撥或catch
來捕獲,而async/await
的錯誤可以用try catch
捕獲Promise
一旦新建就會立即執行,不會阻塞後面的程式碼,而async
函數中await後面是Promise物件會阻塞後面的程式碼。async
函數會隱式地返回一個promise
,該promise
的reosolve
值就是函數return的值。async
函數可以讓程式碼更加簡潔,不需要像Promise
一樣需要呼叫then
方法來獲取返回值,不需要寫匿名函數處理Promise
的resolve值,也不需要定義多餘的data變數,還避免了巢狀程式碼。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 -> async1 end -> promise1 -> promise2 -> setTimeout
老規矩,全域性程式碼自上而下執行,先列印出script start
,然後執行async1(),裡面先遇到await async2(),執行async2,列印出async2 end
,然後await後面的程式碼放入微任務佇列,接著往下執行new Promise,列印出Promise
,遇見了resolve,將第一個then方法放入微任務佇列,接著往下執行列印出script end
,全域性程式碼執行完了,然後從微任務佇列中取出第一個微任務執行,列印出async1 end
,再取出第二個微任務執行,列印出promise1
,然後這個then方法執行完了,當前Promise的狀態為fulfilled
,它也可以出發then的回撥,所以第二個then這時候又被加進了微任務佇列,然後再出微任務佇列中取出這個微任務執行,列印出promise2
,此時微任務佇列為空,接著執行宏任務佇列,列印出setTimeout
。
解題技巧:
【相關推薦:】
以上就是淺析Promise、Generator和Async間的差異的詳細內容,更多請關注TW511.COM其它相關文章!