本文为饥人谷讲师若愚原创文章,首发于 前端学习指南。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或者长按识别下方二维码进入前端技术交流群 ,暗号:写代码啦
每日一题,每周资源推荐,精彩博客推荐,工做、笔试、面试经验交流解答,免费直播课,群友轻分享... ,数不尽的福利免费送