Node.js异步漫谈

使用 node,异步处理是无论如何都规避不了的点,如果只是为了实现功能大可以使用层层回调(回调地狱),但我们是有追求的程序员…

本文以一个简单的文件读写为例,讲解了异步的不同写法,包括 普通的 callback、ES2016中的Promise和Generator、 Node 用于解决回调的co 模块、ES2017中的async/await。适合初步接触 Node.js以及少量 ES6语法的同学阅读。

一个范例

以一个范例做为例,我们要实现的功能如下:

  1. 读取 a.md 文件,得到内容
  2. 把内容转换成 HTML 字符串
  3. 把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的语法,比如letconst箭头函数,上述代码改写如下

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

为了便于描述,我们在代码的关键行加了行号标记,代码执行流程如下:

  1. line1: 执行Generator,创建一个状态对象,此时函数内部并没有执行
  2. line2: 调用g.next(),gen函数开始执行,此时会执行line3的readFile函数,而 gen 函数的控制权交出代码暂停
  3. line4: 当文件读取后会调用 g.next(str), 此时会把控制权再次交给 gen,并把文件结果str做为参数交给Generator状态对象g
  4. line3: 此时yield的结果就是刚刚传递的str,赋值给mdStr
  5. … ,写文件的逻辑类似
  6. 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')
  })
})
  1. line1: 执行Generator,创建一个状态对象,此时函数内部并没有执行,此时状态对象{value:undefined, done: false}
  2. line2: 执行g.next()的时候开始执行gen函数,此时会执行readFile(), 而这个函数的执行会返回一个匿名函数。遇到yield 后gen函数暂停,把readFile()返回的匿名函数存储到状态对象的value里。所以g.next().value() 其实就是执行那个匿名函数,即 调用fs.readFile。当文件读取后,会调用fs.readFile里的 callback,而这个 callback 就是刚刚 g.next().value()的参数
  3. 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 地址,模块内部原理可[参考这里](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')}

作者:若愚

37 thoughts on “Node.js异步漫谈”

  1. Wow that was strange. I just wrote an really long comment but after I clicked submit
    my comment didn’t show up. Grrrr… well I’m not writing all that over again. Anyway, just wanted
    to say great blog!

  2. I don’t know whether it’s just me or if everybody else encountering issues with your website.
    It seems like some of the written text in your content are running off the screen. Can someone else please comment and let me know if this is happening
    to them too? This may be a issue with my internet browser because I’ve had this happen before.
    Thanks

  3. Greetings from Ohio! I’m bored to tears at work so I decided to check out your site on my iphone during lunch break.
    I enjoy the information you provide here and can’t wait to take a look
    when I get home. I’m amazed at how quick your blog loaded on my phone ..
    I’m not even using WIFI, just 3G .. Anyways, excellent
    site!

  4. hello there and thank you for your info – I have
    certainly picked up anything new from right here.
    I did however expertise several technical issues
    using this website, since I experienced to reload the site many times previous to I
    could get it to load properly. I had been wondering if your hosting is OK?

    Not that I’m complaining, but slow loading instances times will very frequently affect your placement in google and could damage your high quality score if advertising and marketing
    with Adwords. Anyway I’m adding this RSS to my e-mail and could
    look out for a lot more of your respective fascinating content.

    Ensure that you update this again very soon.

  5. Hey there are using WordPress for your blog platform?
    I’m new to the blog world but I’m trying to get started and set up
    my own. Do you need any coding expertise
    to make your own blog? Any help would be greatly appreciated!

  6. What i don’t understood is in fact how you’re not really
    a lot more smartly-preferred than you might be now. You are so intelligent.

    You realize thus considerably in relation to this matter,
    made me individually consider it from so many numerous angles.
    Its like men and women aren’t involved unless it’s one thing to do with
    Lady gaga! Your individual stuffs excellent. Always maintain it up!

  7. I needed to put you that little note to help thank you once again relating to the awesome advice you’ve shared in this case. This is so remarkably generous of people like you to make easily all that a number of people could have advertised for an e book in order to make some bucks for their own end, certainly seeing that you could possibly have tried it if you ever considered necessary. These smart ideas as well acted like the great way to be aware that most people have similar interest really like my personal own to find out a lot more concerning this condition. I am sure there are several more fun periods ahead for those who look over your website.

  8. My spouse and i felt really ecstatic when Albert could do his basic research using the ideas he grabbed from your own web page. It’s not at all simplistic to just find yourself freely giving instructions which often many others might have been selling. So we know we have the website owner to be grateful to because of that. The entire illustrations you have made, the straightforward blog menu, the relationships you can help to engender – it’s got everything fantastic, and it’s really assisting our son in addition to our family understand that concept is interesting, and that’s rather pressing. Many thanks for all!

  9. I wish to show some appreciation to you for rescuing me from this type of circumstance. As a result of surfing around through the world-wide-web and getting concepts which are not beneficial, I figured my life was gone. Existing minus the approaches to the problems you have fixed by means of the article content is a crucial case, and those that could have negatively affected my entire career if I hadn’t come across your blog. Your own capability and kindness in playing with all the stuff was tremendous. I am not sure what I would have done if I had not encountered such a step like this. I can also at this moment look ahead to my future. Thanks for your time very much for this skilled and result oriented help. I won’t be reluctant to recommend your web site to any person who needs and wants tips about this matter.

  10. I wanted to write you this tiny remark to say thank you again over the breathtaking thoughts you have contributed in this case. This is tremendously open-handed with you giving unhampered what exactly a few people could possibly have distributed as an e-book to get some bucks for themselves, chiefly given that you could possibly have done it if you ever considered necessary. Those tips also worked as a easy way to understand that other people online have similar interest much like my very own to figure out many more with respect to this issue. I am certain there are numerous more fun occasions in the future for those who start reading your site.

  11. I just wanted to type a simple word in order to express gratitude to you for all of the magnificent techniques you are placing on this website. My long internet research has at the end of the day been rewarded with pleasant tips to go over with my two friends. I ‘d mention that we visitors actually are very much endowed to exist in a useful website with very many perfect individuals with beneficial techniques. I feel somewhat lucky to have seen the web pages and look forward to plenty of more awesome times reading here. Thanks a lot again for a lot of things.

  12. I wish to show appreciation to this writer just for rescuing me from this scenario. Because of looking through the the web and finding basics which are not beneficial, I figured my life was well over. Being alive minus the solutions to the problems you have sorted out by way of your main write-up is a critical case, as well as the ones which could have adversely damaged my career if I had not encountered the website. Your own personal natural talent and kindness in taking care of all the things was vital. I am not sure what I would have done if I had not come upon such a solution like this. I can also at this moment relish my future. Thanks very much for the expert and effective help. I will not think twice to propose your web sites to anyone who will need recommendations on this subject.

  13. If some one wants to be updated with latest technologies after that he must be go to
    see this site and be up to date every day. 2CSYEon cheap flights

  14. Heya i am for the first time here. I found this board and I find It truly useful & it helped me out much. I hope to give something back and help others like you helped me.

  15. Hi, Neat post. There’s a problem with your site in web explorer, could test this?
    IE still is the market leader and a large portion of other folks will
    pass over your great writing due to this problem.

  16. I enjoy you because of all your valuable efforts on this blog. Gloria enjoys setting aside time for internet research and it is easy to see why. Almost all know all relating to the compelling mode you convey sensible solutions through the website and in addition encourage participation from people on this subject while my princess is really learning a great deal. Take pleasure in the rest of the new year. You are always conducting a stunning job.

  17. I enjoy what you guys are up too. This type of
    clever work and coverage! Keep up the wonderful works guys I’ve incorporated you guys to
    my personal blogroll. cheap flights 3gqLYTc

  18. I truly love your website.. Pleasant colors & theme. Did you build this website yourself?

    Please reply back as I’m attempting to create my very own blog and would love to find out where you got this from or
    just what the theme is named. Many thanks!

  19. Thank you so much for providing individuals with remarkably nice possiblity to read critical reviews from this site. It can be very pleasurable and also full of amusement for me personally and my office acquaintances to search your site at a minimum thrice in 7 days to see the latest secrets you have got. And lastly, we’re at all times fulfilled concerning the remarkable thoughts you give. Certain 2 tips in this post are easily the finest I have had.

  20. Thank you a lot for providing individuals with such a wonderful possiblity to discover important secrets from this blog. It is usually very brilliant and also stuffed with a great time for me personally and my office fellow workers to search your website at a minimum thrice in 7 days to study the newest guidance you have. Of course, I am at all times motivated considering the dazzling secrets you serve. Certain 4 tips in this posting are rather the simplest we have all ever had.

  21. I not to mention my guys were analyzing the excellent points located on your web site and then then I had a terrible feeling I had not expressed respect to the web blog owner for those strategies. These people ended up totally passionate to read through all of them and have in effect actually been enjoying them. Thank you for simply being quite thoughtful and for obtaining such helpful subject areas most people are really desperate to be aware of. My honest apologies for not saying thanks to sooner.

  22. I wish to show my appreciation for your kindness in support of men and women that require assistance with this one topic. Your very own commitment to passing the message around came to be especially insightful and have made women like me to realize their aims. This warm and friendly suggestions can mean a lot to me and further more to my fellow workers. Thanks a ton; from each one of us.

  23. There are certainly numerous particulars like that to take into consideration. That is a nice point to convey up. I provide the ideas above as common inspiration but clearly there are questions like the one you deliver up where an important thing will likely be working in honest good faith. I don?t know if greatest practices have emerged round things like that, however I am certain that your job is clearly identified as a good game. Each boys and girls really feel the influence of just a second抯 pleasure, for the remainder of their lives.

  24. This design is incredible! You most certainly know how to keep a
    reader amused. Between your wit and your videos, I was almost moved to start
    my own blog (well, almost…HaHa!) Fantastic job.
    I really loved what you had to say, and more than that, how you presented it.
    Too cool!

  25. After research just a few of the blog posts in your website now, and I actually like your means of blogging. I bookmarked it to my bookmark web site checklist and will probably be checking back soon. Pls try my site as effectively and let me know what you think.

发表评论

电子邮件地址不会被公开。