本文爲飢人谷講師若愚原創文章,首發於 前端學習指南。html
使用 node,異步處理是不管如何都規避不了的點,若是隻是爲了實現功能大可使用層層回調(回調地獄),但咱們是有追求的程序員...
本文以一個簡單的文件讀寫爲例,講解了異步的不一樣寫法,包括 普通的 callback、ES2016中的Promise和Generator、 Node 用於解決回調的co 模塊、ES2017中的async/await。適合初步接觸 Node.js以及少許 ES6語法的同窗閱讀。前端
以一個範例作爲例,咱們要實現的功能以下:node
var fs = require('fs') var markdown = require( "markdown" ).markdown fs.readFile('a.md','utf-8', function(err, str){ if(err){ return console.log(err) } var html = markdown.toHTML(str) fs.writeFile('b.html', html, function(err){ if(err){ return console.log(err) } console.log('write success') }) })
既然在 Node 環境下執行,那咱們就儘可能多使用 ES6的語法,好比let
、const
、箭頭函數
,上述代碼改寫以下程序員
const fs = require('fs') const markdown = require( "markdown" ).markdown fs.readFile('a.md','utf-8', (err, str)=>{ if(err){ return console.log(err) } let html = markdown.toHTML(str) fs.writeFile('b.html', html, (err)=>{ if(err){ return console.log(err) } console.log('write success') }) })
看起來還不錯哦,那是由於咱們的回調只有兩層,若是是七層、十層呢?這不是開玩笑。es6
關於 Promise 規範你們能夠參考阮一峯老師的教程ECMAScript 6入門,這裏不做贅述。
這裏咱們把上述代碼改寫爲 Promise 規範的調用方式,其中文件的讀寫須要進行包裝,調用後返回 Promise 對象面試
const fs = require('fs') const markdown = require( "markdown" ).markdown readFile("a.md") .then((mdStr)=>{ return markdown.toHTML(mdStr) //返回的結果做爲下個回調的參數 }).then(html=>{ writeFile('b.html', html) }).catch((e)=>{ console.log(e) }); function readFile(url) { var promise = new Promise((resolve, reject)=>{ fs.readFile(url,'utf-8', (err, str)=>{ if(err){ reject(new Error('readFile error')) }else{ resolve(str) } }) }) return promise } function writeFile(url, data) { var promise = new Promise((resolve, reject)=>{ fs.writeFile(url, data, (err, str)=>{ if(err){ reject(new Error('writeFile error')) }else{ resolve() } }) }) return promise }
上述代碼把 callback 的嵌套執行改成 then 的串聯執行,看起來舒服了一些。代碼中咱們對文件的讀寫函數進行了 Promise 化包裝,其實可使用一些現成的模塊來作這個事情,繼續改寫代碼npm
const markdown = require('markdown').markdown const fsp = require('fs-promise') //用於把 fs 變爲 promise 化,內部處理邏輯和上面的例子相似 let onerror = err=>{ console.error('something wrong...') } fsp.readFile('a.md', 'utf-8') .then((mdStr)=>{ return markdown.toHTML(mdStr) //返回的結果做爲下個回調的參數 }).then(html=>{ fsp.writeFile('b.html', html) }).catch(onerror);
代碼一會兒少了不少,結構清晰,但一堆的 then 看着仍是礙眼...編程
Generator 函數是 ES6 提供的一種異步編程解決方案,也是剛剛接觸的同窗難以理解的點之一,在看下面的代碼以前能夠參考阮老師的教程ECMAScript 6入門, 固然這裏也會先用一些簡單的範例作引導便於你們去理解.c#
先看一個範例:promise
function fn(a,b){ console.log('fn..') return a + b } function* gen(x) { console.log(x) let y = yield fn(x,100) + 3 console.log(y) return 200 }
上述聲明瞭一個普通函數 fn,和一個 Generator 函數 gen,先執行以下代碼
let g = gen(1)
調用Generator 函數,返回一個存儲狀態對象的引用,這個時候 gen 這個函數是沒執行的,因此當你執行上面這行代碼不會有任何輸出
console.log( g.next() )
當調用g.next()
時,gen 函數開始執行,執行到第一個yield 爲止,並把 yield 表達式的值做爲狀態對象的值。更具體一點,上例先輸出x
也就是1
,而後執行 fn(x, 100)
輸出 fn..
並返回101, 而後加3。這時候中止執行,把結果103賦值給狀態對象 g,g 的結果變 {value: 103, done: false}。須要注意,yied表達式的優先級極其低,yield fn(x,100) + 3
至關於 yield (fn(x,100) + 3)
console.log( g.next() )
此次執行g.next()
的時候,代碼由上次暫停處開始執行,但此時 yield 表達式的值並非使用剛剛計算的結果,而是使用 g.next
的參數undefined
, 因此 y的值變爲undefined
,輸出undeined
。執行到return 200
時,狀態對象知道執行結束了,會把return的200賦值到狀態對象,結果爲 { value: 200, done: true }
有同窗會問,如何把剛剛計算的中間值103給下個yield來用呢?好問題,咱們能夠這樣
g.next(g.next().value)
想一想爲何。如今能夠回到咱們的主題了,看看實現代碼
const fs = require('fs') const markdown = require("markdown").markdown function readFile(url) { fs.readFile(url, 'utf8', (err, str)=>{ if(err){ g.throw('read error'); }else{ g.next(str) //line4 } }) } function writeFile(url, data) { fs.writeFile(url, data, (err, str)=>{ if(err){ g.throw('write error'); }else{ g.next() //line5 } }) } let gen = function* () { try{ let mdStr = yield readFile('aa.md', 'utf-8') //line3 console.log(mdStr) let html = markdown.toHTML(mdStr) yield fs.writeFile('b.html', html) }catch(e){ console.log('error occur...') //line6 } } let g = gen() //line1 let result = g.next() //line2
爲了便於描述,咱們在代碼的關鍵行加了行號標記,代碼執行流程以下:
若是能看懂上面的代碼,說明對 Generator函數就理解了
但雖然感受用了更「高級」的技術,但與前面兩種方法相比這種寫法反而更醜陋難用。狀態對象居然在 readFile 和 writeFile 這兩個普通函數裏面調用...
咱們能夠先作一些優化
function readFile(url) { return (callback)=>{ fs.readFile(url, 'utf-8', (err, str)=>{ if(err) throw err callback(str) }) } } //readFile('a.md')( (err, str)=>{ console.log(str)} ) //將多個參數的調用轉換成單個參數的調用,回想一想那些經常提到的概念,如閉包、函數柯里化 function writeFile(url, data){ return (callback)=>{ fs.writeFile(url, data, (err, str)=>{ if(err) throw err callback() }) } } // writeFile('b.html')( (err)=>{console.log('write ok')} ) let gen = function* () { try{ let mdStr = yield readFile('a.md', 'utf-8') //line4 let html = markdown.toHTML(mdStr) yield writeFile('b.html', html) }catch(e){ console.log('error occur...') } } let g = gen() //line1 g.next().value(str=>{ //line2 g.next(str).value(()=>{ //line3 console.log('write success') }) })
真的是很繞,頭都繞暈了。上面的寫法除了稍微解耦覺得,仍然很醜陋,主功能異步的執行須要 Generator不斷的回調調用next才能夠,若是有七層十層...
下面作個個簡單的優化,讓Generator自動調用,知道狀態變爲done,原理你們本身好好想一想
function run(fn) { let gen = fn() function next(data) { let result = gen.next(data) if (result.done) return console.log(result.value) result.value(next) } next() } run(gen)
不再想用 Generator 了!
co 模塊是用於處理異步的一個node包,用於 Generator 函數的自動執行。NPM 地址co,模塊內部原理可參考這裏ECMAScript 6入門-模塊, 本質上就是 Promise 和 Generator 的結合,和咱們上個範例仍是很像的。
相似處理異步的比較出名的模塊還有 async模塊(注意不是ES2017的async語法)、bluebird
const fs = require('fs') const markdown = require('markdown').markdown const co = require('co') const thunkify = require('thunkify') let readFile = thunkify(fs.readFile) let writeFile = thunkify(fs.writeFile) let onerror = err=>{ console.error('something wrong...') } let gen = function* () { let mdStr = yield readFile('a.md', 'utf-8') let html = markdown.toHTML(mdStr) yield writeFile('b.html', html) } co(gen).catch(onerror)
例子中 thunkify模塊用於把一個函數thunk化,也就是咱們上例中以下形式對異步函數進行包裝。gen 的啓動由 co(gen)
來開啓,和咱們上一個範例相似
function writeFile(url, data){ return (callback)=>{ fs.writeFile(url, data, (err, str)=>{ if(err) throw err callback() }) } }
就像回到了男耕女織的田園生活,感受世界一會兒清爽了許多。
ES2017 標準引入了 async 函數,用於更方便的處理異步。 這個特性太新了,真要用須要babel來轉碼。
const markdown = require('markdown').markdown const fsp = require('fs-promise') let onerror = err=>{ console.error('something wrong...') } async function start () { let mdStr = await fsp.readFile('a.md', 'utf-8') let html = markdown.toHTML(mdStr) await fsp.writeFile('b.html', html) } start().catch(onerror)
async函數是對 Generator 函數的改進,實際上就是把Generator自動執行給封裝起來,同時返回的是 Promise 對象更便於操做。
用的時候須要注意await命令後面是一個 Promise 對象。
上例中 fsp的做用是把內置的fs模塊Promise 化,這個其實剛剛作過。
var readFile = function (fileName) { return new Promise(function (resolve, reject) { fs.readFile(fileName,'utf-8', function(error, data) { if (error) reject(error); resolve(data); }); }); }
上面幾個例子其實是異步處理的發展過程,從醜陋到精美,從引入各類亂七八糟的無關代碼到精簡到只保留核心業務功能,這也是任何框架和標準發展的趨勢。
有什麼預見和期待?
能夠預見的是async/await慢慢會變成主流,現階段用 co 也挺方便的,由於它們都很美。
期待node內置的涉及異步操做的模塊都逐步提供對Promise的規範的支持,期待 ES2017的快速普及,那世界就美好了。
上面咱們的功能不須要任何『外掛』將簡化成
let mdStr = await fs.readFile('a.md', 'utf-8') let html = markdown.toHTML(mdStr) await fs.writeFile('b.html', html) fs.onerror = ()=>{console.log('error')}
加微信號: astak10或者長按識別下方二維碼進入前端技術交流羣 ,暗號:寫代碼啦
每日一題,每週資源推薦,精彩博客推薦,工做、筆試、面試經驗交流解答,免費直播課,羣友輕分享... ,數不盡的福利免費送