本文首發於 本人博客
衆所周知JS是單線程的,這種設計讓JS避免了多線程的各類問題,但同時也讓JS同一時刻只能執行一個任務,若這個任務執行時間很長的話(如死循環),會致使JS直接卡死,在瀏覽器中的表現就是頁面無響應,用戶體驗很是之差。前端
所以,在JS中有兩種任務執行模式:同步(Synchronous)和異步(Asynchronous)。相似函數調用、流程控制語句、表達式計算等就是以同步方式運行的,而異步主要由setTimeout/setInterval
、事件實現。git
做爲一個前端開發者,不管是瀏覽器端仍是Node,相信你們都使用過事件吧,經過事件確定就能想到回調函數,它就是實現異步最經常使用、最傳統的方式。es6
不過要注意,不要覺得回調函數就都是異步的,如ES5的數組方法Array.prototype.forEach((ele) => {})
等等,它們也是同步執行的。回調函數只是一種處理異步的方式,屬於函數式編程中高階函數的一種,並不僅在處理異步問題中使用。github
舉個栗子🌰:ajax
// 最多見的ajax回調 this.ajax('/path/to/api', { params: params }, (res) => { // do something... })
你可能以爲這樣並無什麼不妥,可是如有多個ajax或者異步操做須要依次完成呢?編程
this.ajax('/path/to/api', { params: params }, (res) => { // do something... this.ajax('/path/to/api', { params: params }, (res) => { // do something... this.ajax('/path/to/api', { params: params }, (res) => { // do something... }) ... }) })
回調地獄就出現了。。。😢api
爲了解決這個問題,社區中提出了Promise方案,而且該方案在ES6中被標準化,現在已普遍使用。數組
使用Promise的好處就是讓開發者遠離了回調地獄的困擾,它具備以下特色:promise
對象的狀態不受外界影響:瀏覽器
一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。
上面的代碼能夠改寫成以下:
this.ajax('/path/to/api', { params: params }).then((res) => { // do something... return this.ajax('/path/to/api', { params: params }) }).then((res) => { // do something... return this.ajax('/path/to/api', { params: params }) }) ...
看起來就直觀多了,就像一個鏈條同樣將多個操做依次串了起來,不再用擔憂回調了~😄
同時Promise還有許多其餘API,如Promise.all
、Promise.race
、Promise.resolve/reject
等等(能夠參考阮老師的文章),在須要的時候配合使用都是極好的。
API無需多說,不過這裏我總結了一下本身以前使用Promise踩到的坑以及我對Promise理解不夠透徹的地方,但願也能幫助你們更好地使用Promise:
then的返回結果:我以前天真的覺得then
要想鏈式調用,必需要手動返回一個新的Promise才行
Promise.resolve('first promise') .then((data) => { // return Promise.resolve('next promise') // 實際上兩種返回是同樣的 return 'next promise' }) .then((data) => { console.log(data) })
總結以下:
then
方法中返回了一個值,那麼返回一個「新的」resolved的Promise,而且resolve回調函數的參數值是這個值then
方法中拋出了一個異常,那麼返回一個「新的」rejected狀態的Promisethen
方法返回了一個未知狀態(pending)的Promise新實例,那麼返回的新Promise就是未知狀態then
方法沒有返回值時,那麼會返回一個「新的」resolved的Promise,但resolve回調函數沒有參數一個Promise可設置多個then回調,會按定義順序執行,以下
const p = new Promise((res) => { res('hahaha') }) p.then(console.log) p.then(console.warn)
這種方式與鏈式調用不要搞混,鏈式調用其實是then方法返回了新的Promise,而不是原有的,能夠驗證一下:
const p1 = Promise.resolve(123) const p2 = p1.then(() => { console.log(p1 === p2) // false })
then
或catch
返回的值不能是當前promise自己,不然會形成死循環:
const promise = Promise.resolve() .then(() => { return promise })
then
或者catch
的參數指望是函數,傳入非函數則會發生值穿透:
Promise.resolve(1) .then(2) .then(Promise.resolve(3)) .then(console.log) // 1
process.nextTick
和promise.then
都屬於microtask,而setImmediate
、setTimeout
屬於macrotask
process.nextTick(() => { console.log('nextTick') }) Promise.resolve() .then(() => { console.log('then') }) setImmediate(() => { console.log('setImmediate') }) console.log('end') // end nextTick then setImmediate
有關microtask及macrotask能夠看這篇文章,講得很細緻。
但Promise也存在弊端,那就是若步驟不少的話,須要寫一大串.then()
,儘管步驟清晰,可是對於咱們這些追求極致優雅的前端開發者來講,代碼全都是Promise的API(then
、catch
),操做的語義太抽象,仍是讓人不夠滿意呀~
Generator是ES6規範中對協程的實現,但目前大多被用於異步模擬同步上了。
執行它會返回一個遍歷器對象,而每次調用next
方法則將函數執行到下一個yield
的位置,若沒有則執行到return或末尾。
依舊是再也不贅述API,對它還不瞭解的能夠查閱阮老師的文章。
經過Generator實現異步:
function* main() { const res = yield getData() console.log(res) } // 異步方法 function getData() { setTimeout(() => { it.next({ name: 'yuanye', age: 22 }) }, 2000) } const it = main() it.next()
先無論下面的next
方法,單看main
方法中,getData
模擬的異步操做已經看起來很像同步了。可是追求完美的咱們確定是沒法忍受每次還要手動調用next
方法來繼續執行流程的,爲此TJ大神爲社區貢獻了co模塊來自動化執行Generator,它的實現原理很是巧妙,源碼只有短短的200多行,感興趣能夠去研究下。
const co = require('co') co(function* () { const res1 = yield ['step-1'] console.log(res1) // 若yield後面返回的是promise,則會等待它resolved後繼續執行以後的流程 const res2 = yield new Promise((res) => { setTimeout(() => { res('step-2') }, 2500) }) console.log(res2) return 'end' }).then((data) => { console.log('end: ' + data) })
這樣就讓異步的流程徹底以同步的方式展現出來啦😋~
ES7標準中引入的async函數,是對js異步解決方案的進一步完善,它有以下特色:
then
來指定回調進一步說,async函數徹底能夠看做多個異步操做,包裝成的一個Promise對象,而await命令就是內部then命令的語法糖。
改寫後代碼以下:
async function testAsync() { const res1 = await new Promise((res) => { setTimeout(() => { res('step-1') }, 2000) }) console.log(res1) const res2 = await Promise.resolve('step-2') console.log(res2) const res3 = await new Promise((res) => { setTimeout(() => { res('step-3') }, 2000) }) console.log(res3) return [res1, res2, res3, 'end'] } testAsync().then((data) => { console.log(data) })
這樣不只語義仍是流程都很是清晰,即使是不熟悉業務的開發者也能一眼看出哪裏是異步操做。
本文彙總了當前主流的JS異步解決方案,其實沒有哪種方法最好或很差,都是在不一樣的場景下能發揮出不一樣的優點。並且目前都是Promise與其餘兩個方案配合使用的,因此不存在你只學會async/await或者generator就能夠玩轉異步。沒準之後又會出現一個新的方案,將已有的這幾種方案顛覆呢 ~
在這不斷變化、發展的時代,咱們前端要放開本身的眼界,擁抱變化,持續學習,才能成長,寫出優質的代碼😜~