深入解析koa之異步回調(diào)處理
1. 回調(diào)金字塔及理想中的解決方案
我們都知道javascript是一門單線程異步非阻塞語言。異步非阻塞當(dāng)然是它的一個(gè)優(yōu)點(diǎn),但大量的異步操作必然涉及大量的回調(diào)函數(shù),特別是當(dāng)異步嵌套的時(shí)候,就會(huì)出現(xiàn)回調(diào)金字塔的問題,使得代碼的可讀性非常差。比如下面一個(gè)例子:
var fs = require('fs'); fs.readFile('./file1', function(err, data) { console.log(data.toString()); fs.readFile('./file2', function(err, data) { console.log(data.toString()); }) })
這個(gè)例子是先后讀取兩個(gè)文件內(nèi)容并打印,其中file2的讀取必須在file1讀取結(jié)束之后再進(jìn)行,因此其操作必須要在file1讀取的回調(diào)函數(shù)中執(zhí)行。這是一個(gè)典型的回調(diào)嵌套,并且只有兩層而已,在實(shí)際編程中,我們可能會(huì)遇到更多層的嵌套,這樣的代碼寫法無疑是不夠優(yōu)雅的。
在我們想象中,比較優(yōu)雅的一種寫法應(yīng)該是看似同步實(shí)則異步的寫法,類似下面這樣:
var data; data = readFile('./file1'); //下面的代碼是第一個(gè)readFile執(zhí)行完畢之后的回調(diào)部分 console.log(data.toString()); //下面的代碼是第二個(gè)readFile的回調(diào) data = readFile('./file2'); console.log(data.toString());
這樣的寫法,就完全避免回調(diào)地獄。事實(shí)上,koa就讓我們可以使用這樣的寫法來寫異步回調(diào)函數(shù):
var koa = require('koa'); var app = koa(); var request=require('some module'); app.use(function*() { var data = yield request('http://www.baidu.com'); //以下是異步回調(diào)部分 this.body = data.toString(); }) app.listen(3000);
那么,究竟是什么讓koa有這么神奇的魔力呢?
2. generator配合promise實(shí)現(xiàn)異步回調(diào)同步寫法
關(guān)鍵的一點(diǎn),其實(shí)前一篇也提到了,就是generator具有類似"打斷點(diǎn)"這樣的效果。當(dāng)遇到y(tǒng)ield的時(shí)候,就會(huì)暫停,將控制權(quán)交給yield后面的函數(shù),當(dāng)下次返回的時(shí)候,再繼續(xù)執(zhí)行。
而在上面的那個(gè)koa例子中,yield后面的可不是任何對(duì)象都可以哦!必須是特定類型。在co函數(shù)中,可以支持promise, thunk函數(shù)等。
今天的文章中,我們就以promise為例來進(jìn)行分析,看看如何使用generator和promise配合,實(shí)現(xiàn)異步同步化。
依舊以第一個(gè)讀取文件例子來分析。首先,我們需要將讀文件的函數(shù)進(jìn)行改造,將其封裝成為一個(gè)promise對(duì)象:
var fs = require('fs'); var readFile = function(fileName) { return new Promise(function(resolve, reject) { fs.readFile(fileName, function(err, data) { if (err) { reject(err); } else { resolve(data); } }) }) } //下面是readFile使用的示例 var tmp = readFile('./file1'); tmp.then(function(data) { console.log(data.toString()); })
關(guān)于promise的使用,如果不熟悉的可以去看看es6中的語法。(近期我也會(huì)寫一篇文章來教大家如何用es5的語法來自己實(shí)現(xiàn)一個(gè)具備基本功能的promise對(duì)象,敬請(qǐng)期待呦^_^)
簡(jiǎn)單來講,promise可以實(shí)現(xiàn)將回調(diào)函數(shù)通過 promise.then(callback)的形式來寫。但是我們的目標(biāo)是配合generator,真正實(shí)現(xiàn)如絲般順滑的同步化寫法,如何配合呢,看這段代碼:
var fs = require('fs'); var readFile = function(fileName) { return new Promise(function(resolve, reject) { fs.readFile(fileName, function(err, data) { if (err) { reject(err); } else { resolve(data); } }) }) } //將讀文件的過程放在generator中 var gen = function*() { var data = yield readFile('./file1'); console.log(data.toString()); data = yield readFile('./file2'); console.log(data.toString()); } //手動(dòng)執(zhí)行g(shù)enerator var g = gen(); var another = g.next(); //another.value就是返回的promise對(duì)象 another.value.then(function(data) { //再次調(diào)用g.next從斷點(diǎn)處執(zhí)行g(shù)enerator,并將data作為參數(shù)傳回 var another2 = g.next(data); another2.value.then(function(data) { g.next(data); }) })
上述代碼中,我們?cè)趃enerator中yield了readFile,回調(diào)語句代碼寫在yield之后的代碼中,完全是同步的寫法,實(shí)現(xiàn)了文章一開頭的設(shè)想。
而yield之后,我們得到的是一個(gè)another.value是一個(gè)promise對(duì)象,我們可以使用then語句定義回調(diào)函數(shù),函數(shù)的內(nèi)容呢,則是將讀取到的data返回給generator并繼續(xù)讓generator從斷點(diǎn)處執(zhí)行。
基本上這就是異步回調(diào)同步化最核心的原理,事實(shí)上如果大家熟悉python,會(huì)知道python中有"協(xié)程"的概念,基本上也是使用generator來實(shí)現(xiàn)的(我想當(dāng)懷疑es6的generator就是借鑒了python~)
不過呢,上述代碼我們依然是手動(dòng)執(zhí)行的。那么同上一篇一樣,我們還需要實(shí)現(xiàn)一個(gè)run函數(shù),用于管理generator的流程,讓它能夠自動(dòng)跑起來!
3. 讓同步化回調(diào)函數(shù)自動(dòng)跑起來:一個(gè)run函數(shù)的編寫
仔細(xì)觀察上一段代碼中手動(dòng)執(zhí)行g(shù)enerator的部分,也能發(fā)現(xiàn)一個(gè)規(guī)律,這個(gè)規(guī)律讓我們可以直接寫一個(gè)遞歸的函數(shù)來代替:
var run=function(gen){ var g; if(typeof gen.next==='function'){ g=gen; }else{ g=gen(); } function next(data){ var tmp=g.next(data); if(tmp.done){ return ; }else{ tmp.value.then(next); } } next(); }
函數(shù)接收一個(gè)generator,并讓其中的異步能夠自動(dòng)執(zhí)行。使用這個(gè)run函數(shù),我們來讓上一個(gè)異步代碼自動(dòng)執(zhí)行:
var fs = require('fs'); var run = function(gen) { var g; if (typeof gen.next === 'function') { g = gen; } else { g = gen(); } function next(data) { var tmp = g.next(data); if (tmp.done) { return; } else { tmp.value.then(next); } } next(); } var readFile = function(fileName) { return new Promise(function(resolve, reject) { fs.readFile(fileName, function(err, data) { if (err) { reject(err); } else { resolve(data); } }) }) } //將讀文件的過程放在generator中 var gen = function*() { var data = yield readFile('./file1'); console.log(data.toString()); data = yield readFile('./file2'); console.log(data.toString()); } //下面只需要將gen放入run當(dāng)中即可自動(dòng)執(zhí)行 run(gen);
執(zhí)行上述代碼,即可看到終端依次打印出了file1和file2的內(nèi)容。
需要指出的是,這里的run函數(shù)為了簡(jiǎn)單起見只支持promise,而實(shí)際的co函數(shù)還支持thunk等。
這樣一來,co函數(shù)的兩大功能基本就完整介紹了,一個(gè)是洋蔥模型的流程控制,另一個(gè)是異步同步化代碼的自動(dòng)執(zhí)行。在下一篇文章中,我將帶大家對(duì)這兩個(gè)功能進(jìn)行整合,寫出我們自己的一個(gè)co函數(shù)!
這篇文章的代碼同樣可以在github上面找到:https://github.com/mly-zju/async-js-demo,其中promise_generator.js就是本篇的示例源碼。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Node.js中使用Log.io在瀏覽器中實(shí)時(shí)監(jiān)控日志(等同tail -f命令)
這篇文章主要介紹了Node.js中使用Log.io在瀏覽器中實(shí)時(shí)監(jiān)控日志,Log.io等同于tail -f命令,但更強(qiáng)大,需要的朋友可以參考下2014-09-09Node.js中readline模塊實(shí)現(xiàn)終端輸入
本文主要介紹了Node.js中readline模塊實(shí)現(xiàn)終端輸入,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02詳解Nodejs中自動(dòng)化瀏覽器操作神器Puppeteer的使用
Puppeteer是一個(gè)JavaScript庫,它提供了一種方式來通過DevTools協(xié)議控制無頭瀏覽器,本文主要為大家介紹了Puppeteer的主要特性和使用方法,感興趣的可以了解下2024-01-01輕松創(chuàng)建nodejs服務(wù)器(6):作出響應(yīng)
這篇文章主要介紹了輕松創(chuàng)建nodejs服務(wù)器(6):作出響應(yīng),我們接著改造服務(wù)器,讓請(qǐng)求處理程序能夠返回一些有意義的信息,需要的朋友可以參考下2014-12-12使用NVM實(shí)現(xiàn)不同nodejs版本的自由切換
在工作中,我們可能需要同時(shí)進(jìn)行多個(gè)不同NodeJS版本的項(xiàng)目開發(fā),這種情況下,對(duì)于維護(hù)多個(gè)版本的node將會(huì)是一件非常麻煩的事情,NVM就是為解決這個(gè)問題而產(chǎn)生的,本文給出了下載、安裝及使用方法,需要的朋友可以參考下2024-02-02nodejs項(xiàng)目windows下開機(jī)自啟動(dòng)的方法
今天小編就為大家分享一篇nodejs項(xiàng)目windows下開機(jī)自啟動(dòng)的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2017-11-11Nodejs中使用phantom將html轉(zhuǎn)為pdf或圖片格式的方法
這篇文章主要介紹了Nodejs中使用phantom將html轉(zhuǎn)為pdf或圖片格式的方法,需要的朋友可以參考下2017-09-09