本系列的例子主要针对node.js环境,但浏览器端的原理应该也是类似的。
本人也是Javascript新手,把自己这段时间学习积累的要点总结下来,希望可以对同样在学习Javascript/node.js的同学有一些参考价值。尽量用通俗的语言帮助大家理解,如果有描述或理解不准确的地方欢迎大家指正,交流。另外本文假定你已经对javascript的语法和异步有一些基本的概念。
本系列会按一般学习异步编程的顺序,首先介绍一下异步的原理,然后介绍各种异步编程的方法,从回调函数开始,然后慢慢进入Promise和Generator等对异步编程体验进行改进的技术。期间也会大概提一下事件监听方式的异步调用,但是因为太多的事件监听会让程序流程变的不清晰,所以不太推荐(其实事件模式本质上也是回调函数)。Promise和Generator才是到目前为止(ES6),最好的异步编程方式。后面也会分享一下自己所理解的这两种方式的对比(网上也有很多争论)。ES7提出了更好的改进方案,希望很快可以抽出时间研究一下,这是后话:)
好,进入本节的正文。
什么是异步?同步异步与阻塞非阻塞有什么关系?
node.js的“一切皆异步”的思想很有创意,目的是可以让开发者轻松编写高性能的web服务端,而不会“不小心”就用同步api阻塞了服务器从而影响性能。其他的语言比如php, python, java等基于同步的语言,虽然也有异步api,但毕竟编程人员的“思想上是同步的”,有时候不可避免的会写出阻塞的代码,node.js的目标是造就“思想上是完全异步”的编程人员和编程语言:)
异步跟同步最大的不同就是异步api或函数被“调用”后不会等它运行结束再执行它后面的代码,而是调用之后直接往下执行,异步函数的“执行”实际上是放在“其他地方”,待“执行”完成后再把结果通过回调函数来进行进一步的使用或处理(所以异步函数书写的时候不要用"return"来返回值哦,必须通过回调函数来返回值)。这里为什么强调“调用”和“执行”两个词呢?就是为了更好的理解异步的过程。
打个比喻,你的领导要做个电子报表,笨领导的做法是,在自己电脑上把该统计的统计了,该算的算了,最后生成一张报表,然后用这个报表做接下去的工作;聪明的领导,会发个邮件给下属,把报表的要求写清楚,下属在自己的电脑上把报表做完后,发个邮件把报表交回给领导。在下属做报表的时候,领导可以在自己电脑上继续做其他事情,比如玩游戏、看视频等等(你懂的)。
在上面这个例子中,领导是编程者(你),领导的电脑是当前线程,下属的电脑是另一个线程(如果有多个下属就相当于有个线程池)。做报表这件工作是个异步函数,发邮件给下属相当于调用这个函数,下属电脑上做报表相当于在另一个线程异步执行这个函数,执行完了发邮件把报表发回给领导相当于调用回调函数,领导就可以使用这个报表接着做下面的工作(相当于回掉函数里面的代码)。下属做报表的时候领导完全不用管而是可以继续干其他事情。
通过这个例子可以清楚的看到,领导只能在他自己的电脑(用户线程)上工作,异步的函数都是在下属的电脑上(异步线程)做的。这一点在很多文章当中并没有讲的很清楚,所以容易造成困扰,因为很多人只是一味的强调javascript是单线程的,但单线程怎么能实现异步呢?就并没有讲清楚。其实所谓的单线程是指用户线程是单线程,而另外还有一个或多个线程处理异步代码的执行。
接下来再说说阻塞的问题。很多文章里面在讲解的时候同步异步阻塞非阻塞混为一团,新手很难理解,最容易产生的误解就是同步=阻塞,异步=非阻塞。其实阻塞非阻塞跟同步异步没有任何关系。简单讲,阻塞就是一个api或者函数运行时间过长,而独占cpu导致其他代码不能运行,那么多长时间算阻塞呢?相信这只是一个相对的概念,只要明显影响到程序的性能和用户体验,就算阻塞吧。那跟同步异步是什么关系呢?阻塞的代码如果同步执行就会阻塞到自己后面代码的运行,所以自然而然的就想到异步来执行阻塞的代码,然而异步真能解决阻塞问题吗?下面继续讲。
node.js里面的异步
大家知道Javascript的基本语法跟其他编程语言大同小异,最大的不同就是把异步摆在首位,特别是node.js更是大部分api都是异步的,小量同步api。这与其他大部分语言刚好相反,也给习惯于同步编程思维的同学造成了很大的困扰,就是常说的转不过弯来,习惯性的认为代码是按顺序一行一行的往下执行。
上面介绍了异步非阻塞的概念,那么具体到node.js是怎么实现的呢?这里又会涉及到I/O的概念,具体不讲I/O的细节(操作系统原理都会讲),关键就一点,I/O操作通常比较耗时但不会独占CPU,典型的I/O比如文件读写,远程数据库读写,网络请求等。 先讲耗时,如果用同步API来进行I/O操作,在返回结果之前就只能等待,所以最好的办法上面已经讲过,就是进行异步操作。接着说一下不会霸占CPU的好处。在node.js进程里面,有一个用户线程(javascript所宣称的单线程)和一个异步线程池(用户无法直接访问), 如果跑在异步线程上的代码是阻塞的,那么这种异步根本就起不到消除阻塞的作用,为什么?原因就是阻塞代码会霸占cpu,导致本进程所有代码都等待不管是哪个线程。但是,,,刚刚讲的node.js里面的I/O API都是不会霸占CPU的,所以是非阻塞的,就不会出现这个问题。这就是node.js的最引以为傲的特性之一:异步非阻塞I/O.
上面只是强调异步非阻塞,那么对于真正的阻塞代码,node.js怎么办呢?不好意思。。。单个node.js进程真无能为力,跟同步编程语言相比没什么优势,只能用传统的方式,多进程,多开几个node.js进程,甚至多开几个服务器。任何语言都有它的强项和弱项,node.js的强项就是它本来的设计初衷:让开发者能够轻松的编写高性能的web服务器(进行的最多的就是网络和数据库的I/O操作),而不是做大量的CPU密集型的运算(不过我赶脚,就算要做很多阻塞操作,用多进程和多服务器又有何不可?node.js对此提供了足够的支持)。这样算是回答了上一小节最后的问题了吧:)
要理解javascript异步编程和其他语言同步编程的区别,可以从一个最简单的例子开始。在同步为主的语言中,如果需要等待10秒钟,通常是类似sleep 10之类的语句,在10秒之内整个进程挂起,也就是阻塞10秒。但javascript不是这样,它是使用setTimeout函数,从字面意思就已经可以看出区别了, 设一个timeout的时间, 在这段时间内cpu可以继续运行其他代码, 等10秒时间到了, 就用回调函数的形式来做应该10秒之后才做的事情。
到此,这一节异步编程的原理大概就概括了一下,在进一步深入学习异步编程之前一定要理解的概念。以上都是自己的理解,错误或不准确的地方还望不吝赐教:)
下一节会介绍最基础的javascript的异步编程方式:回调函数,所有其他方式都是基于它的改进。
附注:也许是我读得书少:) 这么久以来没有发现网上有对异步编程讲解的很透彻的文章,在自己学习过的资料当中,朴灵的《深入浅出node.js》是讲解的最深入透彻的,强烈推荐。