使用 node,异步处理是无论如何都规避不了的点,如果只是为了实现功能大可以使用层层回调 (回调地狱),但我们是有追求的程序员...
本文以一个简单的文件读写为例,讲解了异步的不同写法,包括 普通的 callback、ES2016 中的 Promise 和 Generator、 Node 用于解决回调的 co 模块、ES2017 中的 async/await。适合初步接触 Node.js 以及少量 ES6 语法的同学阅读。
一个范例
以一个范例做为例,我们要实现的功能如下:
- 读取 a.md 文件,得到内容
- 把内容转换成 html 字符串
- 把 HTML 字符串写入 b.html
一、callback 回调地狱
- 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')
- })
- })
看起来还不错哦,那是因为我们的回调只有两层,如果是七层、十层呢?这不是开玩笑。
二、Promise 处理回调
关于 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 化包装,其实可以使用一些现成的模块来做这个事情,继续改写代码
- 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
Generator 函数是 ES6 提供的一种异步编程解决方案,也是刚刚接触的同学难以理解的点之一,在看下面的代码之前可以参考阮老师的 教程 ECMAScript 6 入门 , 当然这里也会先用一些简单的范例做引导便于大家去理解.
先看一个范例:
- 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
为了便于描述,我们在代码的关键行加了行号标记,代码执行流程如下:
- line1: 执行 Generator,创建一个状态对象,此时函数内部并没有执行
- line2: 调用 g.next(),gen 函数开始执行,此时会执行 line3 的 readFile 函数,而 gen 函数的控制权交出代码暂停
- line4: 当文件读取后会调用 g.next(str), 此时会把控制权再次交给 gen,并把文件结果 str 做为参数交给 Generator 状态对象 g
- line3: 此时 yield 的结果就是刚刚传递的 str,赋值给 mdStr
- ... , 写文件的逻辑类似
- line6: 当中间出现错误时, g 会抛出异常,控制权交给 gen 后会捕获异常,处理报错
如果能看懂上面的代码,说明对 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')
- })
- })
- line1: 执行 Generator,创建一个状态对象,此时函数内部并没有执行, 此时状态对象 {value:undefined, done: false}
- line2: 执行 g.next() 的时候开始执行 gen 函数,此时会执行 readFile(), 而这个函数的执行会返回一个匿名函数。遇到 yield 后 gen 函数暂停,把 readFile() 返回的匿名函数存储到状态对象的 value 里。所以 g.next().value() 其实就是执行那个匿名函数,即 调用 fs.readFile。当文件读取后,会调用 fs.readFile 里的 callback,而这个 callback 就是刚刚 g.next().value() 的参数
- line3: 调用 g.next(str) 让 gen 函数继续执行,同时把 yield 语句的结果用 str 来替换,代码继续往下走,到 writeFile 停止执行... 同步骤 2
真的是很绕,头都绕晕了。上面的写法除了稍微解耦以为,仍然很丑陋,主功能异步的执行需要 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 模块
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()
- })
- }
- }
就像回到了男耕女织的田园生活,感觉世界一下子清爽了许多。
五、async/await
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 或者长按识别下方二维码进入前端技术交流群 ,暗号:写代码啦
每日一题,每周资源推荐,精彩博客推荐,工作、笔试、面试经验交流解答,免费直播课,群友轻分享... ,数不尽的福利免费送