摘要:普通的回調(diào)函數(shù)調(diào)用執(zhí)行后續(xù)邏輯使用了以后的復(fù)雜邏輯獲取到正確的結(jié)果輸出兩個(gè)文件拼接后的內(nèi)容雖說(shuō)解決了的問(wèn)題,不會(huì)出現(xiàn)一個(gè)函數(shù)前邊有二三十個(gè)空格的縮進(jìn)。所以直接使用關(guān)鍵字替換原有的普通回調(diào)函數(shù)即可。
從今年過(guò)完年回來(lái),三月份開始,就一直在做重構(gòu)相關(guān)的事情。為什么要升級(jí)
就在今天剛剛上線了最新一次的重構(gòu)代碼,希望高峰期安好,接近半年的Node.js代碼重構(gòu)。
包含從callback+async.waterfall到generator+co,統(tǒng)統(tǒng)升級(jí)為了async,還順帶推動(dòng)了TypeScript在我司的使用。
這些日子也踩了不少坑,也總結(jié)了一些小小的優(yōu)化方案,進(jìn)行精簡(jiǎn)后將一些比較關(guān)鍵的點(diǎn),拿出來(lái)分享給大家,希望有同樣在做重構(gòu)的小伙伴們可以繞過(guò)這些。
首先還是要談?wù)劯拇a的理由,畢竟重構(gòu)肯定是要有合理的理由的。
如果單純想看升級(jí)相關(guān)事項(xiàng)可以直接選擇跳過(guò)這部分。
從最原始的開始說(shuō)起,期間確實(shí)遇到了幾個(gè)年代久遠(yuǎn)的項(xiàng)目,Node 0.x,使用的普通callback,也有一些會(huì)應(yīng)用上async.waterfall這樣在當(dāng)年看起來(lái)很優(yōu)秀的工具。
// 普通的回調(diào)函數(shù)調(diào)用 var fs = require("fs") fs.readFile("test1.txt", function (err, data1) { if (err) return console.error(err) fs.readFile("test2.txt", function (err, data2) { if (err) return console.error(err) // 執(zhí)行后續(xù)邏輯 console.log(data1.toString() + data2.toString()) // ... }) }) // 使用了async以后的復(fù)雜邏輯 var async = require("fs") async.waterfall([ function (callback) { fs.readFile("test1.txt", function (err, data) { if (err) callback(err) callback(null, data.toString()) }) }, function (result, callback) { fs.readFile("test2.txt", function (err, data) { if (err) callback(err) callback(null, result + data.toString()) }) } ], function (err, result) { if (err) return console.error(err) // 獲取到正確的結(jié)果 console.log(result) // 輸出兩個(gè)文件拼接后的內(nèi)容 })
雖說(shuō)async.waterfall解決了callback hell的問(wèn)題,不會(huì)出現(xiàn)一個(gè)函數(shù)前邊有二三十個(gè)空格的縮進(jìn)。
但是這樣的流程控制在某些情況下會(huì)讓代碼變得很詭異,例如我很難在某個(gè)函數(shù)中選擇下一個(gè)應(yīng)該執(zhí)行的函數(shù),而是只能按照順序執(zhí)行,如果想要進(jìn)行跳過(guò),可能就要在中途的函數(shù)中進(jìn)行額外處理:
async.waterfall([ function (callback) { if (XXX) { callback(null, null, null, true) } else { callback(null, data1, data2) } }, function (data1, data2, isPass, callback) { if (isPass) { callback(null, null, null, isPass) } else { callback(null, data1 + data2) } } ])
所以很可能你的代碼會(huì)變成這樣,里邊存在大量的不可讀的函數(shù)調(diào)用,那滿屏充斥的null占位符。
所以callback這種形式的,一定要進(jìn)行修改, __這屬于難以維護(hù)的代碼__。
Generator實(shí)際上generator是依托于co以及類似的工具來(lái)實(shí)現(xiàn)的將其轉(zhuǎn)換為Promise,從編輯器中看,這樣的代碼可讀性已經(jīng)沒(méi)有什么問(wèn)題了,但是問(wèn)題在于他始終是需要額外引入co來(lái)幫忙實(shí)現(xiàn)的,generator本身并不具備幫你執(zhí)行異步代碼的功能。
不要再說(shuō)什么async/await是generator的語(yǔ)法糖了
因?yàn)槲宜?b>Node版本已經(jīng)統(tǒng)一升級(jí)到了8.11.x,所以async/await語(yǔ)法已經(jīng)可用。
這就像如果document.querySelectorAll、fetch已經(jīng)可以滿足需求了,為什么還要引入jQuery呢。
所以,將generator函數(shù)改造為async/await函數(shù)也是勢(shì)在必行。
期間遇到的坑將callback的升級(jí)為async/await其實(shí)并沒(méi)有什么坑,反倒是在generator + co 那里遇到了一些問(wèn)題:
數(shù)組執(zhí)行的問(wèn)題在co的代碼中,大家應(yīng)該都見到過(guò)這樣的:
const results = yield list.map(function * (item) { return yield getData(item) })
在循環(huán)中發(fā)起一些異步請(qǐng)求,有些人會(huì)告訴你,從yield改為async/await僅僅替換關(guān)鍵字就好了。
那么恭喜你得到的results實(shí)際上是一個(gè)由Promise實(shí)例組成的數(shù)組。
const results = await list.map(async item => { return await getData(item) }) console.log(results) // [Promise, Promise, Promise, ...]
因?yàn)?b>async并不會(huì)判斷你后邊的是不是一個(gè)數(shù)組(這個(gè)是在co中有額外的處理)而僅僅檢查表達(dá)式是否為一個(gè)Promise實(shí)例。
所以正確的做法是,添加一層Promise.all,或者說(shuō)等新的語(yǔ)法await*,Node.js 10.x貌似還不支持。。
// 關(guān)于這段代碼的優(yōu)化方案在下邊的建議中有提到 const results = await Promise.all(list.map(async item => { return await getData(item) })) console.log(results) // [1, 2, 3, ...]await / yield 執(zhí)行順序的差異
這個(gè)一般來(lái)說(shuō)遇到的概率不大,但是如果真的遇到了而栽了進(jìn)去就欲哭無(wú)淚了。
首先這樣的代碼在執(zhí)行上是沒(méi)有什么區(qū)別的:
yield 123 // 123 await 123 // 123
這樣的代碼也是沒(méi)有什么區(qū)別的:
yield Promise.resolve(123) // 123 await Promise.resolve(123) // 123
但是這樣的代碼,問(wèn)題就來(lái)了:
yield true ? Promise.resolve(123) : Promise.resolve(233) // 123 await true ? Promise.resolve(123) : Promise.resolve(233) // Promise<123>
從字面上我們其實(shí)是想要得到yield那樣的效果,結(jié)果卻得到了一個(gè)Promise實(shí)例。
這個(gè)是因?yàn)?b>yield、await兩個(gè)關(guān)鍵字執(zhí)行順序不同所導(dǎo)致的。
在MDN的文檔中可以找到對(duì)應(yīng)的說(shuō)明:MDN | Operator precedence
可以看到yield的權(quán)重非常低,僅高于return,所以從字面上看,這個(gè)執(zhí)行的結(jié)果很符合我們想要的。
而await關(guān)鍵字的權(quán)重要高很多,甚至高于最普通的四則運(yùn)算,所以必然也是高于三元運(yùn)算符的。
也就是說(shuō)await版本的實(shí)際執(zhí)行是這樣子的:
(await true) ? Promise.resolve(123) : Promise.resolve(233) // Promise<123>
那么我們想要獲取預(yù)期的結(jié)果,就需要添加()來(lái)告知解釋器我們想要的執(zhí)行順序了:
await (true ? Promise.resolve(123) : Promise.resolve(233)) // 123一定不要漏寫 await 關(guān)鍵字
這個(gè)其實(shí)算不上升級(jí)時(shí)的坑,在使用co時(shí)也會(huì)遇到,但是這是一個(gè)很嚴(yán)重,而且很容易出現(xiàn)的問(wèn)題。
如果有一個(gè)異步的操作用來(lái)返回一個(gè)布爾值,告訴我們他是否為管理員,我們可能會(huì)寫這樣的代碼:
async function isAdmin (id) { if (id === 123) return true return false } if (await isAdmin(1)) { // 管理員的操作 } else { // 普通用戶的操作 }
因?yàn)檫@種寫法接近同步代碼,所以遺漏關(guān)鍵字是很有可能出現(xiàn)的:
if (isAdmin(1)) { // 管理員的操作 } else { // 普通用戶的操作 }
因?yàn)?b>async函數(shù)的調(diào)用會(huì)返回一個(gè)Promise實(shí)例,得益于我強(qiáng)大的弱類型腳本語(yǔ)言,Promise實(shí)例是一個(gè)Object,那么就不為空,也就是說(shuō)會(huì)轉(zhuǎn)換為true,那么所有調(diào)用的情況都會(huì)進(jìn)入if塊。
那么解決這樣的問(wèn)題,有一個(gè)比較穩(wěn)妥的方式,強(qiáng)制判斷類型,而不是簡(jiǎn)單的使用if else,使用類似(a === 1)、(a === true)這樣的操作。_eslint、ts 之類的都很難解決這個(gè)問(wèn)題_
一些建議 何時(shí)應(yīng)該用 async ,何時(shí)應(yīng)該直接用 Promise首先,async函數(shù)的執(zhí)行返回值就是一個(gè)Promise,所以可以簡(jiǎn)單地理解為async是一個(gè)基于Promise的包裝:
function fetchData () { return Promise().resolve(123) } // ==> async function fetchData () { return 123 }
所以可以認(rèn)為說(shuō)await后邊是一個(gè)Promise的實(shí)例。
而針對(duì)一些非Promise實(shí)例則沒(méi)有什么影響,直接返回?cái)?shù)據(jù)。
在針對(duì)一些老舊的callback函數(shù),當(dāng)前版本的Node已經(jīng)提供了官方的轉(zhuǎn)換工具util.promisify,用來(lái)將符合Error-first callback規(guī)則的異步操作轉(zhuǎn)換為Promise實(shí)例:
而一些沒(méi)有遵守這樣規(guī)則的,或者我們要自定義一些行為的,那么我們會(huì)嘗試手動(dòng)實(shí)現(xiàn)這樣的封裝。
在這種情況下一般會(huì)采用直接使用Promise,因?yàn)檫@樣我們可以很方便的控制何時(shí)應(yīng)該reject,何時(shí)應(yīng)該resolve。
但是如果遇到了在回調(diào)執(zhí)行的過(guò)程中需要發(fā)起其他異步請(qǐng)求,難道就因?yàn)檫@個(gè)Promise導(dǎo)致我們?cè)趦?nèi)部也要使用.then來(lái)處理么?
function getList () { return new Promise((resolve, reject) => { oldMethod((err, data) => { fetch(data.url).then(res => res.json()).then(data => { resolve(data) }) }) }) } await getList()
但上邊的代碼也太丑了,所以關(guān)于上述問(wèn)題,肯定是有更清晰的寫法的,不要限制自己的思維。
__async也是一個(gè)普通函數(shù)__,完全可以放在任何函數(shù)執(zhí)行的地方。
所以關(guān)于上述的邏輯可以進(jìn)行這樣的修改:
function getList () { return new Promise((resolve, reject) => { oldMethod(async (err, data) => { const res = await fetch(data.url) const data = await res.json() resolve(data) }) }) } await getList()
這完全是一個(gè)可行的方案,對(duì)于oldMethod來(lái)說(shuō),我按照約定調(diào)用了傳入的回調(diào)函數(shù),而對(duì)于async匿名函數(shù)來(lái)說(shuō),也正確的執(zhí)行了自己的邏輯,并在其內(nèi)部觸發(fā)了外層的resolve,實(shí)現(xiàn)了完整的流程。
代碼變得清晰很多,邏輯沒(méi)有任何修改。
合理的減少 await 關(guān)鍵字await只能在async函數(shù)中使用,await后邊可以跟一個(gè)Promise實(shí)例,這個(gè)是大家都知道的。
但是同樣的,有些await其實(shí)并沒(méi)有存在的必要。
首先有一個(gè)我面試時(shí)候經(jīng)常會(huì)問(wèn)的題目:
Promise.resolve(Promise.resolve(123)).then(console.log) // ?
最終輸出的結(jié)果是什么。
這就要說(shuō)到resolve的執(zhí)行方式了,如果傳入的是一個(gè)Promise實(shí)例,亦或者是一個(gè)thenable對(duì)象(_簡(jiǎn)單的理解為支持.then((resolve, reject) => {})調(diào)用的對(duì)象_),那么resolve實(shí)際返回的結(jié)果是內(nèi)部執(zhí)行的結(jié)果。
也就是說(shuō)上述示例代碼直接輸出123,哪怕再多嵌套幾層都是一樣的結(jié)果。
通過(guò)上邊所說(shuō)的,不知大家是否理解了 合理的減少 await 關(guān)鍵字 這句話的意思。
結(jié)合著前邊提到的在async函數(shù)中返回?cái)?shù)據(jù)是一個(gè)類似Promise.resolve/Promise.reject的過(guò)程。
而await就是類似監(jiān)聽then的動(dòng)作。
所以像類似這樣的代碼完全可以避免:
const imgList = [] async function getImage (url) { const res = await fetch(url) return await res.blob() } await Promise.all(imgList.map(async url => await getImage(url))) // ==> async function getImage (url) { const res = fetch(url) return res.blob() } await Promise.all(imgList.map(url => getImage(url)))
上下兩種方案效果完全相同。
Express 與 koa 的升級(jí)首先,Express是通過(guò)調(diào)用response.send來(lái)完成請(qǐng)求返回?cái)?shù)據(jù)的。
所以直接使用async關(guān)鍵字替換原有的普通回調(diào)函數(shù)即可。
而Koa也并不是說(shuō)你必須要升級(jí)到2.x才能夠使用async函數(shù)。
在Koa1.x中推薦的是generator函數(shù),也就意味著其內(nèi)部是調(diào)用了co來(lái)幫忙做轉(zhuǎn)換的。
而看過(guò)co源碼的小伙伴一定知道,里邊同時(shí)存在對(duì)于Promise的處理。
也就是說(shuō)傳入一個(gè)async函數(shù)完全是沒(méi)有問(wèn)題的。
但是1.x的請(qǐng)求上下文使用的是this,而2.x則是使用的第一個(gè)參數(shù)context。
所以在升級(jí)中這里可能是唯一需要注意的地方,__在1.x不要使用箭頭函數(shù)來(lái)注冊(cè)中間件__。
// express express.get("/", async (req, res) => { res.send({ code: 200 }) }) // koa1.x router.get("/", async function (next) { this.body = { code: 200 } }) // koa2.x router.get("/", async (ctx, next) => { ctx.body = { code: 200 } })小結(jié)
重構(gòu)項(xiàng)目是一件很有意思的事兒,但是對(duì)于一些注釋文檔都很缺失的項(xiàng)目來(lái)說(shuō),重構(gòu)則是一件痛苦的事情,因?yàn)槟阈枰獜拇a中獲取邏輯,而作為動(dòng)態(tài)腳本語(yǔ)言的JavaScript,其在大型項(xiàng)目中的可維護(hù)性并不是很高。
所以如果條件允許,還是建議選擇TypeScript之類的工具來(lái)幫助更好的進(jìn)行開發(fā)。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/98033.html
摘要:前幾天在幫后端排查一個(gè)的問(wèn)題的時(shí)候發(fā)現(xiàn)的一些小坑特此記錄的本質(zhì)是出于安全原因,瀏覽器限制從腳本內(nèi)發(fā)起的跨源請(qǐng)求。排查發(fā)現(xiàn)訪問(wèn)失敗的都是需要用戶的登錄態(tài)的。 前幾天在幫后端排查一個(gè)cors的問(wèn)題的時(shí)候發(fā)現(xiàn)的一些小坑特此記錄 ** cors的本質(zhì)是出于安全原因,瀏覽器限制從腳本內(nèi)發(fā)起的跨源HTTP請(qǐng)求。 例如,XMLHttpRequest和FetchAPI遵循同源策略。 這意味著使用這些A...
摘要:前幾天在幫后端排查一個(gè)的問(wèn)題的時(shí)候發(fā)現(xiàn)的一些小坑特此記錄的本質(zhì)是出于安全原因,瀏覽器限制從腳本內(nèi)發(fā)起的跨源請(qǐng)求。排查發(fā)現(xiàn)訪問(wèn)失敗的都是需要用戶的登錄態(tài)的。 前幾天在幫后端排查一個(gè)cors的問(wèn)題的時(shí)候發(fā)現(xiàn)的一些小坑特此記錄 ** cors的本質(zhì)是出于安全原因,瀏覽器限制從腳本內(nèi)發(fā)起的跨源HTTP請(qǐng)求。 例如,XMLHttpRequest和FetchAPI遵循同源策略。 這意味著使用這些A...
摘要:介紹微信風(fēng)格的,與客戶端體驗(yàn)一致,這個(gè)自己去微信上看吧,略。微信調(diào)試一件套,網(wǎng)頁(yè)授權(quán)模擬集成代理遠(yuǎn)程調(diào)試。這些在微信開發(fā)者中心有介紹,略。年微信開發(fā)經(jīng)驗(yàn)的人,終于又成為了零年開發(fā)經(jīng)驗(yàn)的人,重新走上了踩坑之路。 showImg(https://segmentfault.com/img/bVtEd1);活動(dòng)地址:http://fequan.com/2016/ 注意:英文不好,小記也帶有自己...
摘要:前言最近將公司項(xiàng)目的從版本升到了版本,跟完全不兼容,是一次徹底的重寫。升級(jí)過(guò)程中踩了不少的坑,也有一些值得分享的點(diǎn)。沒(méi)有就會(huì)匹配所有路由最后不得不說(shuō)升級(jí)很困難,坑也很多。 前言 最近將公司項(xiàng)目的 react-router 從 v3 版本升到了 v4 版本,react-router v4 跟 v3 完全不兼容,是一次徹底的重寫。這也給升級(jí)造成了極大的困難,與其說(shuō)升級(jí)不如說(shuō)是對(duì) route...
閱讀 2812·2019-08-30 15:55
閱讀 2858·2019-08-30 15:53
閱讀 2296·2019-08-26 13:47
閱讀 2558·2019-08-26 13:43
閱讀 3157·2019-08-26 13:33
閱讀 2805·2019-08-26 11:53
閱讀 1798·2019-08-23 18:35
閱讀 801·2019-08-23 17:16