JavaScript 中回撥地獄的今生前世
1. 講個笑話
JavaScript 是一門程式語言
2. 非同步程式設計
JavaScript 由於某種原因是被設計為單執行緒的,同時由於 JavaScript 在設計之初是用於瀏覽器的 GUI 程式設計,這也就需要執行緒不能進行阻塞。
所以在後續的發展過程中基本都採用非同步非阻塞的程式設計模式。
簡單來說,非同步程式設計就是在執行一個指令之後不是馬上得到結果,而是繼續執行後面的指令,等到特定的事件觸發後,才得到結果。
也正是因為這樣,我們常常會說: JavaScript 是由事件驅動的。
3. 非同步實現
用 JavaScript 構建一個應用的時候經常會遇到非同步程式設計,不管是 Node 服務端還是 Web 前端。
那如何去進行非同步程式設計呢?就目前的標準以及草案來看,主要有下面的幾種方式:
- 回撥
- promise
- Generator
- await/async
3.1 回撥
這種非同步的方式是最基礎的實現,如果你曾經寫過一點的 Node, 可能經常會遇到這樣的程式碼:
connection.query(sql, (err, result) => { if(err) { console.err(err) } else { connection.query(sql, (err, result) => { if(err) { console.err(err) } else { ... } }) } })
如此,connection.query()
是一個非同步的操作,我們在呼叫他的時候,不會馬上得到結果,而是會繼續執行後面的程式碼。這樣,如果我們需要在查到結果之後才做某些事情的話,就需要把相關的程式碼寫在回撥裡面,如果涉及到多個這樣的非同步操作,就勢必會陷入到回撥地獄中去。
這種回撥地獄不僅看起來很不舒服,可讀性比較差;除此之外還有比較重要的一點就是對異常的捕獲無法支援。
3.2 Promise
Promise 是 ES 2015 原生支援的,他把原來巢狀的回撥改為了級聯的方式。
一般著,我們對一個 Promise
可以這樣寫:
var a = new Promise(function(resolve, reject) { setTimeout(function() { resolve('1') }, 2000) }) a.then(function(val) { console.log(val) })
如果要涉及到多個非同步操作的順序執行問題,我們可以這樣寫:
var a = new Promise(function(resolve, reject) { setTimeout(function() { resolve('1') }, 2000) }) a .then(function(val){ console.log(val) return new Promise(function(resolve, reject) { setTimeout(function() { resolve('2') }, 2000) }) }) .then(function(val) { console.log(val) })
也可以把函式抽離出來
var a = new Promise(function(resolve, reject) { setTimeout(function() { resolve('1') }, 2000) }) function b(val) { console.log(val) return new Promise(function(resolve, reject) { setTimeout(function() { resolve('2') }, 2000) }) } a.then(b).then(function(val) { console.log(val) })
我們只需要 return
一個 Promise
即可實現這種多個非同步操作的順序執行。
粗略來看,這是一個比較優雅的非同步解決方案了,並且在 Promise
中我們也可以實現分級的 catch
。
但對於之前接觸過其他語言的同學來說還是比較彆扭的。那能否用同步的方式來書寫非同步呢?
3.3 Generator
在 ES 2015 中,出現了 Generator 的語法,熟悉 Python
的同學肯定對這種語法有點了解。
簡單來說,Generator 可以理解為一個可以遍歷的狀態機,呼叫 next
就可以切換到下一個狀態。
在 JavaScript 中,Generator 的 function 與 函式名之間有一個 *, 函式內部使用 yield 關鍵詞,定義不同的狀態。
先看一段程式碼:
function a() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(1) }, 2000) }); }; var b = co(function *() { var val = yield a(); console.log(val) }) b()
上面的這段程式碼是藉助 TJ 的 co 實現的,依照約定,co 中 yield 後面只能跟 Thunk 或者 Promise.
co 的實現程式碼很短,簡單來說大體是這樣:
// http://www.alloyteam.com/2015/04/solve-callback-hell-with-generator/ function co(genFun) { // 通過呼叫生成器函式得到一個生成器 var gen = genFun(); return function(fn) { next(); function next(err, res) { if(err) return fn(err); // 將res傳給next,作為上一個yield的返回值 var ret = gen.next(res); // 如果函式還沒迭代玩,就繼續迭代 if(!ret.done) return ret.value(next); // 返回函式最後的值 fn && fn(null, res); } } }
簡單來說就是一直藉助 generator 的 next 進行迭代,直到完成這個非同步操作才返回。當前人家官方的 co 是 200 行程式碼,支援非同步操作的並行:
co(function *() { var val = yield [ yield asyn1(), yield asyn2() ] })()
但如果我們使用 co,強迫症們就會覺得這不是標準的寫法,有點 hack 小子的感覺。
幸運的是,在 ES 2016 的草案中,終於提出了標準的寫法。
3.4 await/async
這是在 ES 2016 中引入的新關鍵詞,這將在語言層面徹底解決 JavaScript 的非同步回撥問題,目前可以藉助 babel 在生產環境中使用。使用 await/async 可以讓非同步的操作以同步的方式來寫。
使用方法和 co 非常類似,同時也支援同步寫法的異常捕獲。
function a() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(1) }, 2000) }) } var b = async function() { var val = await a() console.log(val) } b()
如果上述的程式碼完全用 Promise 實現,極有可能是下面的程式碼:
function a() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(1); }, 2000); }); }; var b = function() { a().then(val) { console.log(val) } console.log(val) }; b();
相比較來說,await/async 解決了完全使用 Promise 的一個極大痛點——不同Promise之間共享資料問題:
Promise 需要設定外層資料開始共享,這樣就需要在每個then裡面進行賦值,而 await/async 就不存在這樣的問題,只需要以同步的方式去寫就可以了。
await/async 對異常的支援也是特別好的:
function a() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(1) }, 2000) }); }; var b = async function() { try { var val = await a() console.log(val) } catch (err) { console.log(err) } }; b();
參考文獻
- 回撥地獄 @ 奇舞團部落格
- ES6 Generator介紹 @ 騰訊全端 AlloyTeam 團隊 Blog
- 使用Generator解決回撥地獄 @ 騰訊全端 AlloyTeam 團隊 Blog
- 「大概可能也許是」目前最好的 JavaScript 非同步方案 async/await @ LearnClound
相關文章
- 回撥地獄-編寫非同步JavaScript指南非同步JavaScript
- [JS]回撥函式和回撥地獄JS函式
- promise解決回撥地獄;啥?前端還有“地獄?”Promise前端
- JavaScript的前世今生JavaScript
- 【真知拙見】回撥地獄和PromisePromise
- Flutter Future 回撥地獄的一種解決思路Flutter
- JavaScript – 非同步的前世今生JavaScript非同步
- JavaScript 包管理的前世今生JavaScript
- 面試官:你知道Callback Hell(回撥地獄)嗎?面試
- JavaScript 模組化前世今生JavaScript
- rxjava回撥地獄-kotlin協程來幫忙RxJavaKotlin
- iOS 如何優雅的處理“回撥地獄Callback hell”(一)iOS
- 回顧&展望:防毒軟體的“前世今生”防毒
- JavaScript中回撥的示例理解JavaScript
- 從地獄到天堂,Node 回撥向 async/await 轉變AI
- iOS如何優雅的處理“回撥地獄Callback hell”(二)——使用SwiftiOSSwift
- iOS如何優雅的處理“回撥地獄Callback hell”(一)——使用PromiseKitiOSPromise
- 後端程式設計師的 Js 之旅 : 回撥地獄終結者後端程式設計師JS
- JavaScript非同步流程控制的前世今生JavaScript非同步
- Javascript非同步程式設計的前世今生JavaScript非同步程式設計
- 用Promise建構函式來解決地獄回撥問題Promise函式
- js 幾種網路請求方式梳理——擺脫回撥地獄JS
- 灰色地帶:淺談盜版遊戲的“前世今生”遊戲
- MySQL 的前世今生MySql
- 遊戲的前世今生遊戲
- Mybatis的前世今生MyBatis
- IPD的前世今生
- RabbitMQ的前世今生MQ
- Serverless 的前世今生Server
- WebP 的前世今生Web
- RunLoop的前世今生OOP
- Webpack前世今生Web
- 理解javascript中的回撥函式(callback)【轉】JavaScript函式
- Unicode的前世今生Unicode
- HTTP/2.0的前世今生HTTP
- 元件化的前世今生元件化
- React ref 的前世今生React
- 外掛的前世今生