摘要:異步編程解決方案筆記最近讀了樸靈老師的深入淺出中異步編程一章,并參考了一些有趣的文章。另外回調(diào)函數(shù)中的也失去了意義,這會(huì)使我們的程序必須依賴于副作用。
JavaScript 異步編程解決方案筆記
最近讀了樸靈老師的《深入淺出NodeJS》中《異步編程》一章,并參考了一些有趣的文章。
在此做個(gè)筆記,記錄并鞏固學(xué)到的知識。
異步I/O、事件驅(qū)動(dòng)使得單線程的JavaScript得以在不阻塞UI的情況下執(zhí)行網(wǎng)絡(luò)、文件訪問功能,
且使之在后端實(shí)現(xiàn)了較高的性能。然而異步風(fēng)格也引來了一些麻煩,其中比較核心的問題是:
函數(shù)嵌套過深
JavaScript的異步調(diào)用基于回調(diào)函數(shù),當(dāng)多個(gè)異步事務(wù)多級依賴時(shí),回調(diào)函數(shù)會(huì)形成多級的嵌套,代碼變成
金字塔型結(jié)構(gòu)。這不僅使得代碼變難看難懂,更使得調(diào)試、重構(gòu)的過程充滿風(fēng)險(xiǎn)。
異常處理
回調(diào)嵌套不僅僅是使代碼變得雜亂,也使得錯(cuò)誤處理更復(fù)雜。
異步編程中可能拋出錯(cuò)誤的情況有兩種:
異步函數(shù)錯(cuò)誤
由于異步函數(shù)是立刻返回的,異步事務(wù)中發(fā)生的錯(cuò)誤是無法通過try-catch來捕捉的,只能采用由調(diào)用方提供錯(cuò)誤處理回調(diào)的方案來解決。
例如Node中常見的function (err, ...) {...}回調(diào)函數(shù),就是Node中處理錯(cuò)誤的約定:
即將錯(cuò)誤作為回調(diào)函數(shù)的第一個(gè)實(shí)參返回。
再比如HTML5中FileReader對象的onerror函數(shù),會(huì)被用于處理異步讀取文件過程中的錯(cuò)誤。
回調(diào)函數(shù)錯(cuò)誤
由于回調(diào)函數(shù)執(zhí)行時(shí),異步函數(shù)的上下文已經(jīng)不存在了,通過try-catch無法捕捉回調(diào)函數(shù)內(nèi)的錯(cuò)誤。
可見,異步回調(diào)編程風(fēng)格基本上廢掉了try-catch和throw。另外回調(diào)函數(shù)中的return也失去了意義,這會(huì)使我們的程序必須依賴于副作用。
這使得JavaScript的三個(gè)語義失效,同時(shí)又得引入新的錯(cuò)誤處理方案,如果沒有像Node那樣統(tǒng)一的錯(cuò)誤處理約定,問題會(huì)變得更加麻煩。
下面對幾種解決方案的討論主要集中于上面提到的兩個(gè)核心問題上,當(dāng)然也會(huì)考慮其他方面的因素來評判其優(yōu)缺點(diǎn)。
Async.js首先是Node中非常著名的Async.js,這個(gè)庫能夠在Node中展露頭角,恐怕也得歸功于Node統(tǒng)一的錯(cuò)誤處理約定。
而在前端,一開始并沒有形成這么統(tǒng)一的約定,因此使用Async.js的話可能需要對現(xiàn)有的庫進(jìn)行封裝。
Async.js的其實(shí)就是給回調(diào)函數(shù)的幾種常見使用模式加了一層包裝。比如我們需要三個(gè)前后依賴的異步操作,采用純回調(diào)函數(shù)寫法如下:
asyncOpA(a, b, (err, result) => { if (err) { handleErrorA(err); } asyncOpB(c, result, (err, result) => { if (err) { handleErrorB(err); } asyncOpB(d, result, (err, result) => { if (err) { handlerErrorC(err); } finalOp(result); }); }); });
如果我們采用async庫來做:
async.waterfall([ (cb) => { asyncOpA(a, b, (err, result) => { cb(err, c, result); }); }, (c, lastResult, cb) => { asyncOpB(c, lastResult, (err, result) => { cb(err, d, result); }) }, (d, lastResult, cb) => { asyncOpC(d, lastResult, (err, result) => { cb(err, result); }); } ], (err, finalResult) => { if (err) { handlerError(err); } finalOp(finalResult); });
可以看到,回調(diào)函數(shù)由原來的橫向發(fā)展轉(zhuǎn)變?yōu)榭v向發(fā)展,同時(shí)錯(cuò)誤被統(tǒng)一傳遞到最后的處理函數(shù)中。
其原理是,將函數(shù)數(shù)組中的后一個(gè)函數(shù)包裝后作為前一個(gè)函數(shù)的末參數(shù)cb傳入,同時(shí)要求:
每一個(gè)函數(shù)都應(yīng)當(dāng)執(zhí)行其cb參數(shù);
cb的第一個(gè)參數(shù)用來傳遞錯(cuò)誤。
我們可以自己寫一個(gè)async.waterfall的實(shí)現(xiàn):
let async = { waterfall: (methods, finalCb = _emptyFunction) => { if (!_isArray(methods)) { return finalCb(new Error("First argument to waterfall must be an array of functions")); } if (!methods.length) { return finalCb(); } function wrap(n) { if (n === methods.length) { return finalCb; } return function (err, ...args) { if (err) { return finalCb(err); } methods[n](...args, wrap(n + 1)); } } wrap(0)(false); } };
Async.js還有series/parallel/whilst等多種流程控制方法,來實(shí)現(xiàn)常見的異步協(xié)作。
Async.js的問題是:
在外在上依然沒有擺脫回調(diào)函數(shù),只是將其從橫向發(fā)展變?yōu)榭v向,還是需要程序員熟練異步回調(diào)風(fēng)格。
錯(cuò)誤處理上仍然沒有利用上try-catch和throw,依賴于“回調(diào)函數(shù)的第一個(gè)參數(shù)用來傳遞錯(cuò)誤”這樣的一個(gè)約定。
Promise方案ES6的Promise來源于Promise/A+。使用Promise來進(jìn)行異步流程控制,有幾個(gè)需要注意的問題,
在We have a problem with promises一文中有很好的總結(jié)。
把前面提到的功能用Promise來實(shí)現(xiàn),需要先包裝異步函數(shù),使之能返回一個(gè)Promise:
function toPromiseStyle(fn) { return (...args) => { return new Promise((resolve, reject) => { fn(...args, (err, result) => { if (err) reject(err); resolve(result); }) }); }; }
這個(gè)函數(shù)可以把符合下述規(guī)則的異步函數(shù)轉(zhuǎn)換為返回Promise的函數(shù):
回調(diào)函數(shù)的第一個(gè)參數(shù)用于傳遞錯(cuò)誤,第二個(gè)參數(shù)用于傳遞正常的結(jié)果。
接著就可以進(jìn)行操作了:
let [opA, opB, opC] = [asyncOpA, asyncOpB, asyncOpC].map((fn) => toPromiseStyle(fn)); opA(a, b) .then((res) => { return opB(c, res); }) .then((res) => { return opC(d, res); }) .then((res) => { return finalOp(res); }) .catch((err) => { handleError(err); });
通過Promise,原來明顯的異步回調(diào)函數(shù)風(fēng)格顯得更像同步編程風(fēng)格,我們只需要使用then方法將結(jié)果傳遞下去即可,同時(shí)return也有了相應(yīng)的意義:
在每一個(gè)then的onFullfilled函數(shù)(以及onRejected)里的return,都會(huì)為下一個(gè)then的onFullfilled函數(shù)(以及onRejected)的參數(shù)設(shè)定好值。
如此一來,return、try-catch/throw都可以使用了,但catch是以方法的形式出現(xiàn),還是不盡如人意。
Generator方案ES6引入的Generator可以理解為可在運(yùn)行中轉(zhuǎn)移控制權(quán)給其他代碼,并在需要的時(shí)候返回繼續(xù)執(zhí)行的函數(shù)。利用Generator可以實(shí)現(xiàn)協(xié)程的功能。
將Generator與Promise結(jié)合,可以進(jìn)一步將異步代碼轉(zhuǎn)化為同步風(fēng)格:
function* getResult() { let res, a, b, c, d; try { res = yield opA(a, b); res = yield opB(c, res); res = yield opC(d); return res; } catch (err) { return handleError(err); } }
然而我們還需要一個(gè)可以自動(dòng)運(yùn)行Generator的函數(shù):
function spawn(genF, ...args) { return new Promise((resolve, reject) => { let gen = genF(...args); function next(fn) { try { let r = fn(); if (r.done) { resolve(r.value); } Promise.resolve(r.value) .then((v) => { next(() => { return gen.next(v); }); }).catch((err) => { next(() => { return gen.throw(err); }) }); } catch (err) { reject(err); } } next(() => { return gen.next(undefined); }); }); }
用這個(gè)函數(shù)來調(diào)用Generator即可:
spawn(getResult) .then((res) => { finalOp(res); }) .catch((err) => { handleFinalOpError(err); });
可見try-catch和return實(shí)際上已經(jīng)以其原本面貌回到了代碼中,在代碼形式上也已經(jīng)看不到異步風(fēng)格的痕跡。
類似的功能有co/task.js等庫實(shí)現(xiàn)。
ES7的async/awaitES7中將會(huì)引入async function和await關(guān)鍵字,利用這個(gè)功能,我們可以輕松寫出同步風(fēng)格的代碼,
同時(shí)依然可以利用原有的異步I/O機(jī)制。
采用async function,我們可以將之前的代碼寫成這樣:
async function getResult() { let res, a, b, c, d; try { res = await opA(a, b); res = await opB(c, res); res = await opC(d); return res; } catch (err) { return handleError(err); } } getResult();
和Generator & Promise方案看起來沒有太大區(qū)別,只是關(guān)鍵字換了換。
實(shí)際上async function就是對Generator方案的一個(gè)官方認(rèn)可,將之作為語言內(nèi)置功能。
async function的缺點(diǎn)是:
await只能在async function內(nèi)部使用,因此一旦你寫了幾個(gè)async function,
或者使用了依賴于async function的庫,那你很可能會(huì)需要更多的async function。
目前處于提案階段的async function還沒有得到任何瀏覽器或Node.JS/io.js的支持。
Babel轉(zhuǎn)碼器也需要打開實(shí)驗(yàn)選項(xiàng),并且對于不支持Generator的瀏覽器來說,
還需要引進(jìn)一層厚厚的regenerator runtime,想在前端生產(chǎn)環(huán)境得到應(yīng)用還需要時(shí)間。
1. A Study on Solving Callbacks with JavaScript Generators
2. Async Functions
3. 異步操作
4. Promise - JavaScript MDN
5. We have a problem with promises
6. Taming the asynchronous beast with ES7
7. Managing Node.js Callback Hell
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/85897.html
摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復(fù)雜性。異步編程入門的全稱是前端經(jīng)典面試題從輸入到頁面加載發(fā)生了什么這是一篇開發(fā)的科普類文章,涉及到優(yōu)化等多個(gè)方面。 TypeScript 入門教程 從 JavaScript 程序員的角度總結(jié)思考,循序漸進(jìn)的理解 TypeScript。 網(wǎng)絡(luò)基礎(chǔ)知識之 HTTP 協(xié)議 詳細(xì)介紹 HTT...
摘要:異步請求線程在在連接后是通過瀏覽器新開一個(gè)線程請求將檢測到狀態(tài)變更時(shí),如果設(shè)置有回調(diào)函數(shù),異步線程就產(chǎn)生狀態(tài)變更事件,將這個(gè)回調(diào)再放入事件循環(huán)隊(duì)列中。 基礎(chǔ):瀏覽器 -- 多進(jìn)程,每個(gè)tab頁獨(dú)立一個(gè)瀏覽器渲染進(jìn)程(瀏覽器內(nèi)核) 每個(gè)瀏覽器渲染進(jìn)程是多線程的,主要包括:GUI渲染線程 JS引擎線程 也稱為JS內(nèi)核,負(fù)責(zé)處理Javascript腳本程序。(例如V8引擎) JS引擎線程負(fù)...
摘要:回調(diào)函數(shù)模式類似于事件模型,因?yàn)楫惒酱a也會(huì)在后面的一個(gè)時(shí)間點(diǎn)才執(zhí)行如果回調(diào)過多,會(huì)陷入回調(diào)地獄基礎(chǔ)可以當(dāng)做是一個(gè)占位符,表示異步操作的執(zhí)行結(jié)果。函數(shù)可以返回一個(gè),而不必訂閱一個(gè)事件或者向函數(shù)傳遞一個(gè)回調(diào)函數(shù)。 主要知識點(diǎn):Promise生命周期、Promise基本操作、Promise鏈、響應(yīng)多個(gè)Promise以及集成PromiseshowImg(https://segmentfaul...
摘要:為什么要異步編程我們在寫前端代碼時(shí),經(jīng)常會(huì)對做事件處理操作,比如點(diǎn)擊激活焦點(diǎn)失去焦點(diǎn)等再比如我們用請求數(shù)據(jù),使用回調(diào)函數(shù)獲取返回值。這些都屬于異步編程?;卣{(diào)有多個(gè)狀態(tài),當(dāng)響應(yīng)成功和失敗都有不同的回調(diào)函數(shù)。 為什么要異步編程 我們在寫前端代碼時(shí),經(jīng)常會(huì)對dom做事件處理操作,比如點(diǎn)擊、激活焦點(diǎn)、失去焦點(diǎn)等;再比如我們用ajax請求數(shù)據(jù),使用回調(diào)函數(shù)獲取返回值。這些都屬于異步編程。 也許你...
閱讀 1861·2021-09-22 15:10
閱讀 1392·2021-09-07 09:58
閱讀 2415·2019-08-30 15:44
閱讀 1729·2019-08-26 18:29
閱讀 2152·2019-08-26 13:35
閱讀 849·2019-08-26 13:31
閱讀 799·2019-08-26 11:42
閱讀 1165·2019-08-23 18:39