ECMAScript 6,令Node.js也可以写出同步执行的代码(上)
2014-04-14 18:05
609 查看
从MOsky的博客阅读此文
本人学习Node.js已有两周了,有点心得,写成文章,一方面便于今后自己查阅,另一方面巩固自己所学。如有错误,请诸位赏脸批评指教。
Node.js给我的第一印象就是,它的I/O操作是非阻塞的。非阻塞I/O带来了性能上的优势。与Java的阻塞式I/O操作做对比,Java程序需要从网络下载资源的时候,阻塞线程,当查询数据库的时候,阻塞线程,当读取文件的时候,阻塞线程。诸如此类的来自I/O的阻塞将浪费不少CPU的时间。如果心疼这些浪费的时间,那好,你就多开几个线程或进程。但这又引入了线程之间切换的代价。
Node.js使用事件驱动机制,当涉及I/O操作时,代码异步执行。这样带来的另一个好处就是,不需要考虑麻烦的线程安全问题,因为到Node.js即便是单线程,也不会因为I/O阻塞而出现性能瓶颈。同样和Java做比较。Java因为性能问题而开多线程,因为开多线程而需要考虑线程安全问题。Node.js可不这样。君不知曾有人在多线程的情况下使用HashMap类而导致服务器崩溃,虽然这只能算自作自受(因为HashMap的API上明确告诉你HashMap是非线程安全的),但如果语言本身就令程序员无需担心线程安全问题,那么这种情况就不会因为程序员的粗心而发生了。
但非阻塞I/O操作也有令人不爽的地方,进行I/O操作的时候,只能异步调用。
异步调用依赖回调函数,而回调函数的缺点更加显而易见。那就是代码难写,写出来以后可维护性差。如果不有意识地避免,很可能写出这种东西:
这种回调函数中嵌套回调函数的方法写起来麻烦,别人要读你的代码更加不知所云。因此,也许可以把流程拆解成若干函数,在异步调用时,将函数名作为回调函数参数传进去。
这样看起来似乎好多了,代码从上到下,就是流程中的一个又一个步骤。不过还是有一点不好,你要为每一个步骤取一个名字,而这些名字仅仅是为了写回调函数参数时可以有所指代罢了。
如果不想为每一个步骤命个没太大意义的名,可以试试Promise方法:
这样就既避免了嵌套,又省去为函数命名的苦恼。但是依然有问题:
流程被分割了,确切的说,流程并非依照逻辑的相关程度,而仅仅按照是否有异步操作而被分割成不同部分。
后面的匿名函数无法直接引用之前匿名函数中的变量。
想象一下,有这么一个过程,它需要查询n次数据库,每次都会根据查询的结果进行不同的操作,在此过程中它需要维护一组变量,随时需要读和修改这组变量的值。如果是阻塞式I/O,同步调用,那么直接将此过程写成一个函数,需要维护的变量就写成一组局部变量好了。写起来简单,阅读起来一目了然。但如果I/O是非阻塞式的,调用是异步的,那么不论怎么写,都不能和同步代码那样简单明了。
如果Node.js也能写出同步代码就好了,哪怕是看起来像同步代码也好。是的,这篇文章的目的,就是探讨如何用Node.js写出看起来像同步调用的代码,我称之为伪同步代码。
目前V8引擎已经支持ECMAScript 6的部分特性,比如我即将介绍的generator。如果想使用这些特性,请至少使用Node.js 0.11及以上版本,并且在运行node的时候加上--harmony参数。
如果你不了解generator,强烈建议你先看看《JavaScript标准参考教程(alpha)》3.7部分。我将假定你已经阅读过该部分了。
如何理解generator,我们可以把它理解成一种代码的等效替换。例如有如下一段代码。
可以用等效的generator代替。
第二段代码,和第一段代码的效果是完全一样的。显然第二段代码更易阅读,也更好写。
此外,对于generator又有另一种理解方式。在generator中使用yield关键字,可以让程序流执行至此时被中断,此时现场被保护起来,直到下次调用next()的时候,现场还能恢复,程序流从之前中断的地方继续执行。
利用generator,我们可以写出看起来很像同步代码,但却是异步执行的代码。
这段小程序按顺序先后访问www.baidu.com、www.google.com、taozeyu.com这三个网站,然后分别打印出服务器返回的statusCode。其中http.get()函数一定是异步执行的,但是程序视觉上却有些同步执行的感觉。
但是,仅仅使用generator并不能在所有情况下都能写出伪同步代码。例如,如果有两个函数A,B。其中A必须调用B。且A与B中都有多个地方需要执行异步操作。这种情况下仅用generator就写不出来(或写不好)。此时,我们需要再做一层封装。
作为实验,我写了一个叫做dollar的库,可以去github.com/taozeyu/dollar查看源代码。我将演示下用dollar库如何写出伪同步代码。在下集中,我将讲下dollar库的思路。
使用dollar库时,建议使用大写的D来表示,因为D可能写在代码的各个地方,如果用某个较长的词代替,这个词可能填充得到处都是。
使用D.async()包装某个只能异步调用的函数,使之变成同步函数。
这样,我们就将http.get这个异步函数,包装成了一个名为get$的同步函数。建议将包装后的函数名字结尾加上$符,这样一眼就可以看出哪个函数是包装过的。
当我们想要调用get$()函数时,在之前加上yield关键字,且保证调用在D.start(function* (){...})之中即可。
如此一来,我们就写出了看起来像同步执行的实际却是异步执行的伪同步代码。写代码时,只要记得一看见名字以$结尾的函数,调用时就在它前面加上一个yield关键字,那么写代码来就和同步无异。
现在考虑之前说的那种情况,有两个函数A,B。其中A必须调用B。且A与B中都有多个地方需要执行异步操作。此时,我们只需把B函数包装一下即可。
view
source
print?
请注意这种用法,同包装http.get这种函数一样,包装过的函数命名时也建议在末尾加上$符,因为调用这种函数也必须在之前加yield关键字。
OK,至此,我们似乎得到了一个可以将Node.js在各种情况下都能写出同步代码的方案,真是如此吗?不是的,还有异常处理呢。这部分我留在再下一篇文章中再写吧。
从MOsky的博客阅读此文
引言
本人学习Node.js已有两周了,有点心得,写成文章,一方面便于今后自己查阅,另一方面巩固自己所学。如有错误,请诸位赏脸批评指教。Node.js给我的第一印象就是,它的I/O操作是非阻塞的。非阻塞I/O带来了性能上的优势。与Java的阻塞式I/O操作做对比,Java程序需要从网络下载资源的时候,阻塞线程,当查询数据库的时候,阻塞线程,当读取文件的时候,阻塞线程。诸如此类的来自I/O的阻塞将浪费不少CPU的时间。如果心疼这些浪费的时间,那好,你就多开几个线程或进程。但这又引入了线程之间切换的代价。
Node.js使用事件驱动机制,当涉及I/O操作时,代码异步执行。这样带来的另一个好处就是,不需要考虑麻烦的线程安全问题,因为到Node.js即便是单线程,也不会因为I/O阻塞而出现性能瓶颈。同样和Java做比较。Java因为性能问题而开多线程,因为开多线程而需要考虑线程安全问题。Node.js可不这样。君不知曾有人在多线程的情况下使用HashMap类而导致服务器崩溃,虽然这只能算自作自受(因为HashMap的API上明确告诉你HashMap是非线程安全的),但如果语言本身就令程序员无需担心线程安全问题,那么这种情况就不会因为程序员的粗心而发生了。
但非阻塞I/O操作也有令人不爽的地方,进行I/O操作的时候,只能异步调用。
异步调用的缺点
异步调用依赖回调函数,而回调函数的缺点更加显而易见。那就是代码难写,写出来以后可维护性差。如果不有意识地避免,很可能写出这种东西:01 | function add(x1, x2, callback) { |
02 | callback(x1 + x2); |
03 | } |
04 |
05 | ( function (){ |
06 | var a = 1; |
07 | add(a, 1, function (res) { |
08 | var a = res; |
09 | add(a, 2, function (res) { |
10 | var a = res; |
11 | add(a, 3, function (res) { |
12 | var a = res; |
13 | add(a, 4, function (res) { |
14 | console.log(res); |
15 | }); |
16 | }); |
17 | }); |
18 | }); |
19 | })(); |
01 | step0(); |
02 |
03 | function step0() { |
04 | var a = 1; |
05 | add(a, 1,step2); |
06 | }; |
07 |
08 | function step1(res) { |
09 | var a = res; |
10 | add(a, 2,step2); |
11 | } |
12 |
13 | function step2(res) { |
14 | var a = res; |
15 | add(a, 3,step2); |
16 | } |
17 |
18 | function step3(res) { |
19 | var a = res; |
20 | add(a, 2,step4); |
21 | } |
22 |
23 | function step4(res) { |
24 | console.log(res); |
25 | } |
如果不想为每一个步骤命个没太大意义的名,可以试试Promise方法:
01 | new Promise( function (resolve, reject) { |
02 | var a = 1; |
03 | add(a, 1,resolve); |
04 | }).then( function (res){ |
05 | return new Promise( function (resolve, reject){ |
06 | var a = res; |
07 | add(a, 2,resovle); |
08 | }); |
09 | }).then( function (res){ |
10 | console.log(res); |
11 | }); |
流程被分割了,确切的说,流程并非依照逻辑的相关程度,而仅仅按照是否有异步操作而被分割成不同部分。
后面的匿名函数无法直接引用之前匿名函数中的变量。
想象一下,有这么一个过程,它需要查询n次数据库,每次都会根据查询的结果进行不同的操作,在此过程中它需要维护一组变量,随时需要读和修改这组变量的值。如果是阻塞式I/O,同步调用,那么直接将此过程写成一个函数,需要维护的变量就写成一组局部变量好了。写起来简单,阅读起来一目了然。但如果I/O是非阻塞式的,调用是异步的,那么不论怎么写,都不能和同步代码那样简单明了。
如果Node.js也能写出同步代码就好了,哪怕是看起来像同步代码也好。是的,这篇文章的目的,就是探讨如何用Node.js写出看起来像同步调用的代码,我称之为伪同步代码。
ECMAScript 6 的新特性
目前V8引擎已经支持ECMAScript 6的部分特性,比如我即将介绍的generator。如果想使用这些特性,请至少使用Node.js 0.11及以上版本,并且在运行node的时候加上--harmony参数。如果你不了解generator,强烈建议你先看看《JavaScript标准参考教程(alpha)》3.7部分。我将假定你已经阅读过该部分了。
如何理解generator,我们可以把它理解成一种代码的等效替换。例如有如下一段代码。
01 | function g(name, age) { |
02 | var flow = [ |
03 | function (){ |
04 | console.log( "Name:" +name); |
05 | return name; |
06 | }, |
07 | function (){ |
08 | console.log( "Age:" +age); |
09 | return age; |
10 | }, |
11 | function (){ |
12 | console.log(age > 18 ? "old" : "young" ); |
13 | }, |
14 | ]; |
15 | var index = 0; |
16 | var next = function (){ |
17 | if (index >= flow.length) { throw "error" ;} |
18 | var fun = flow[index]; |
19 | index++; |
20 | return { |
21 | done |
22 | value null , arguments), |
23 | } |
24 | } |
25 | return {next |
26 | } |
1 | function * g(name, age) { |
2 | console.log( "Name:" +name); |
3 | yield name; |
4 | console.log( "Age:" +age); |
5 | yield age; |
6 | console.log(age > 18 ? "old" : "young" ); |
7 | } |
此外,对于generator又有另一种理解方式。在generator中使用yield关键字,可以让程序流执行至此时被中断,此时现场被保护起来,直到下次调用next()的时候,现场还能恢复,程序流从之前中断的地方继续执行。
写出伪同步代码
利用generator,我们可以写出看起来很像同步代码,但却是异步执行的代码。01 | var http = require( 'http' ); |
02 |
03 | var options = [ |
04 | { |
05 | host 'www.baidu.com' , |
06 | port |
07 | page '/' |
08 | }, |
09 | { |
10 | host 'www.google.com' , |
11 | port |
12 | page '/' |
13 | }, |
14 | { |
15 | host 'taozeyu.com' , |
16 | port |
17 | page '/' |
18 | }, |
19 | ]; |
20 |
21 | function * printStatusCode() { |
22 | for ( var i=0; i<options.length; ++i) { |
23 | var res = yield http.get(options, function (res){g.next(res); }); |
24 | console.log(res.statusCode); |
25 | } |
26 | }; |
27 |
28 | var g = printStatusCode(); |
29 | g.next(); |
但是,仅仅使用generator并不能在所有情况下都能写出伪同步代码。例如,如果有两个函数A,B。其中A必须调用B。且A与B中都有多个地方需要执行异步操作。这种情况下仅用generator就写不出来(或写不好)。此时,我们需要再做一层封装。
作为实验,我写了一个叫做dollar的库,可以去github.com/taozeyu/dollar查看源代码。我将演示下用dollar库如何写出伪同步代码。在下集中,我将讲下dollar库的思路。
01 | var http = require( 'http' ); |
02 | var D = require( 'dollar' ); |
03 |
04 | var get$ = D.async(http, http.get); |
05 |
06 | D.start( function * (){ |
07 | console.log( "ready to load..." ); |
08 | var options = { |
09 | host 'www.douban.com' , |
10 | port |
11 | page '/good-good-study-day-day-up' |
12 | }; |
13 | var res = yield get$(options); |
14 | if (res.statusCode == 200) { |
15 | console.log( "success" ); |
16 | } else { |
17 | console.log( "fail +res.statusCode); |
18 | } |
19 | console.log( "complite" ); |
20 | }); |
1 | var D = require( 'dollar' ); |
1 | var get$ = D.async(http, http.get); |
当我们想要调用get$()函数时,在之前加上yield关键字,且保证调用在D.start(function* (){...})之中即可。
1 | var res = yield get$(options); |
现在考虑之前说的那种情况,有两个函数A,B。其中A必须调用B。且A与B中都有多个地方需要执行异步操作。此时,我们只需把B函数包装一下即可。
view
source
print?
01 | var http = require( 'http' ); |
02 | var D = require( 'dollar' ); |
03 |
04 | var get$ = D.async(http, http.get); |
05 |
06 | var getStateCode$ = D( function * (host){ |
07 | var options = { |
08 | host |
09 | port |
10 | page '/good-good-study-day-day-up' |
11 | }; |
12 | var res = yield get$(options); |
13 | console.log( "get code:" +res.statusCode); |
14 | return res.statusCode; |
15 | }); |
16 |
17 | D.start( function * (){ |
18 | console.log( "ready to load..." ); |
19 | console.log( "status code :" +(yield getStateCode$( "www.baidu.com" ))); |
20 | console.log( "status code :" +(yield getStateCode$( "www.douban.com" ))); |
21 | console.log( "status code :" +(yield getStateCode$( "taozeyu.com" ))); |
22 | console.log( "done" ); |
23 | }); |
1 | var getStateCode$ = D( function * (...){...}); |
从MOsky的博客阅读此文
相关文章推荐
- ECMAScript 6,令Node.js也可以写出同步执行的代码(上)
- Js setInterval与setTimeout(定时执行与循环执行)的代码(可以传入参数)
- Js setInterval与setTimeout(定时执行与循环执行)的代码(可以传入参数)
- 我的Node.js学习之路(三)--node.js作用、回调、同步和异步代码 以及事件循环
- Js setInterval与setTimeout(定时执行与循环执行)的代码(可以传入参数)
- Node.js的那些坑(四)——如何让异步并发方法同步顺序执行
- Js setInterval与setTimeout(定时执行与循环执行)的代码(可以传入参数
- DIV模拟LED,js控制显示时间,大家可以复制代码到HTML文件,执行HTML就行了
- node.js的作用、回调、同步异步代码、事件循环
- Js setInterval与setTimeout(定时执行与循环执行)的代码(可以传入参数)
- 意外作出了一个javascript的服务器,可以通过js调用并执行任何java(包括 所有java 内核基本库)及C#类库,并最终由 C# 执行你提交的javascript代码! 不敢藏私,特与大家分
- 我的Node.js学习之路(三)--node.js作用、回调、同步和异步代码 以及事件循环
- c#和node.js交互,edge库的使用,node.js执行c#代码
- 在C# WebBrowser控件插入JS代码并执行,可以修改js就能对html执行任意操作
- JS异步代码执行和同步代码之间的关系
- Node.js 反序列化漏洞远程执行代码(CVE-2017-5941)
- Node.js 反序列化漏洞远程执行代码(CVE-2017-5941)
- 意外作出了一个javascript的服务器,可以通过js调用并执行任何java(包括 所有java 内核基本库)及C#类库,并最终由 C# 执行你提交的javascript代码! 不敢藏私,特与大家分
- 我的Node.js学习之路(三)--node.js作用、回调、同步和异步代码 以及事件循环