《Node学习指南》一1.3 异步函数及Node事件循环

简介:

本节书摘来自异步社区《Node学习指南》一书中的第1章,第1.3节,作者【美】Shelley Powers,更多章节内容可以访问云栖社区“异步社区”公众号查看

1.3 异步函数及Node事件循环

Node学习指南
Node的基本设计原则是将应用程序放置在单线程(或单进程)中执行,同时异步处理所有事件。

考虑下典型的Web服务器(如Apache)是如何工作的。Apache可以采用两种不同的方式处理传入的请求:一种方式是将传入的每个请求分配到独立的进程中直至请求被处理完毕;另一种方式则是为每一个请求生成单独的处理线程。

第一种方式(也称为prefork multiprocessing model,或prefork MPM)可以根据Apache配置文件中指定的值创建多个子进程。使用进程的优势在于被请求的应用(如PHP应用)无需考虑线程安全问题;缺点是每个进程占用独立内存,内存消耗大,应用的扩展性也不是很好。

第二种方式(也称为worker MPM)是进程-线程混合方式。Apache为传入的每个请求创建一个新的处理线程,这样对内存的使用更加有效,但这种方式要求应用必须是线程安全的。虽然现在流行的PHP语言是线程安全的,但却无法保证和它一起被使用的各种库也是线程安全的。

不管哪种方法,它们都可以应对并发请求。如果五个用户在同一时间访问一个Web应用,并且服务器也进行了相应设置,那么Web服务器就可以同时处理五个请求。

Node的处理方式与上面两种不同。当您启动Node应用程序时,它会被创建并运行在一个单线程上。Node会等待应用程序启动完成并开始捕获请求。在未处理完当前请求时,其他请求是不能被处理的。

这种处理方式听起来并不是很有效率,如果Node是通过事件循环和回调函数实现异步运行(在Node中,事件循环一般指轮询指定事件类型并在合适的时间调用事件处理程序,而回调函数就是事件处理程序)的话,它是不应该低效的。

实际上,与一般单线程应用不同,当Node应用程序接收到用户请求时,虽然它会严格按照请求顺序初始化这些资源请求操作(如数据库请求或文件访问),但并不会一直等待操作完成或结果返回。相反,它会在操作请求中附加回调函数。当任何被请求的资源准备好或被请求的操作完成时,特定的事件会被触发,关联的回调函数也会被执行,回调函数会用请求到的资源或操作结果来做另一些事情。

如果五个用户在同一时间访问Node应用程序,并且该应用程序需要访问同一个文件中的资源时,Node会为每个文件访问请求附加一个回调函数但并不等待返回。当资源变为可用时,回调函数会被调用,最终依次满足每个用户的需求。在此期间,Node应用仍然可以处理其他同样或不同类型的用户请求。

尽管Node应用程序不是真正的并行处理用户请求,但其设计方式使得应用能繁忙且高效地处理用户请求,所以大多数人通常不会察觉到任何的响应延迟。最重要的是,它能非常有效地使用内存和其他有限的计算机资源。

1.3.1 使用异步方式读取文件

为了描述Node的异步特性,示例1-2修改了之前章节使用的Hello World程序。它不再输出“Hello,World!”,而是打开先前创建的helloworld.js文件并将其内容输出给客户端。

示例1-2 异步方式地打开文件并写入数据


08cb6a120d74bdac83579a44c0a42314b6cd4fbe

本示例中使用了一个新的文件系统模块(fs)。该模块对标准的POSIX文件操作进行了封装,提供了包括打开文件和访问文件内容等操作。示例1-2使用了模块中的readFile方法,并传入了多个参数,包括文件名称、文件编码方式以及匿名回调函数。

在示例1-2中,我想指出两个有关异步行为的实例,它们分别是附加在readFile方法和listen方法上的回调函数。

正如前面所讨论的,使用listen方法可以告诉HTTP server对象监听指定端口上的连接。Node不会阻塞并等待连接建立,所以如果我们需要在连接建立时做些事情,就需要提供了一个回调函数,如示例1-2所示。

当网络连接建立时会触发监听事件,该事件会触发listen方法绑定的回调函数,进而将信息输出到控制台。

第二,也是更重要的实例是附加在readFile上的回调函数。相对来说,访问文件是一个耗时的操作。如果一个单线程应用程序被多个客户同时访问,而该应用处理每一个请求时都需要进行文件访问操作的话,它可能很快就会陷入瘫痪而无法使用。

解决方法就是采用异步方式打开文件和读取文件内容。只有当内容已经读入数据缓冲区(或读取失败时),附加在readFile方法上的回调函数才会被调用。错误信息(如果有的话)和读取到的数据(如果没有错误发生时)会作为参数传送给回调函数。

在回调函数中需要进行错误检查,如果不存在错误,则将读取到的数据返回给客户端。

1.3.2 观察异步程序流程

大多数人使用JavaScript编写客户端应用程序,这些程序只能被用户在单个浏览器中运行。而在服务端使用JavaScript编写程序可能看上去会有些古怪和陌生。创建允许多人同时访问的JavaScript服务应用可能就更让人觉得陌生了。

Node的事件循环和异步函数调用可以帮助我们,这让编写服务端JavaScript程序变得容易且更有信心。但一定要注意的是,我们正在一个新的不同以往的环境中做JavaScript开发。

为了更好地描述新环境的不同,我创建了两个新的应用:一个提供服务,另一个用于测试服务。示例1-3显示了服务程序的代码。

在代码中,一个函数被调用,以同步方式按顺序输出从1~100的数字。然后程序以类似于示例1-2的方式打开一个文件,但这次文件名是以字符串参数的形式传递给函数的。此外,程序还使用了一个定时器,文件打开操作被安排在定时器超时之后执行。

示例1-3 输出数字序列和文件内容的服务程序


68e82829757e44fc26862af1108f6632a481de00

输出数字的循环体起到了延迟应用程序执行的效果,以便模拟密集计算过程,该过程会引起应用程序阻塞直到计算完成。在这里setTimeout是另一个异步函数,它会紧接着调用第二个异步函数:readFile。所以该应用程序结合了异步和同步流程。

创建一个名为main.txt的文本文件,可以包含任何你想要的内容。运行应用程序并通过Chrome浏览器访问,访问时使用的url需要带有file=name的查询字段,应用程序将生成如下控制台输出:

Server running at 8124/
opening main.txt
opening undefined.txt

前两行输出信息很容易理解。第一行由程序末尾console.log输出,第二行是在文件被打开时输出的。但是,第三行的undefined.txt是怎么回事?

其实,当处理来自浏览器的Web请求时,浏览器可能会发送多个请求。例如,一般浏览器可以发送第二个请求,寻找一个叫favicon.ico的文件。正因为如此,当你在处理查询字符串时,你必须检查看看需要的数据是否被提供,并忽略没有数据的请求。

警告:
当期望从查询字符串中获取某些参数时,浏览器发送多个请求的特点可能会影响到你的应用程序。因此,必须相应的调整应用,并在几个不同的浏览器上进行测试。
到目前为止,我们对Node应用程序所做的所有测试都是从浏览器中进行的。这样我们无法对其进行压力测试来体现Node应用程序的异步特性。

示例1-4是一段非常简单的测试代码。它使用HTTP模块多次向服务程序发送请求。这些请求并不是按异步方式发送的。然而,我们同时也可以使用浏览器访问该服务。两者相结合,就可以达到异步测试应用程序的目的。

提示:
14章将介绍如何创建异步测试应用程序。
示例1-4 测试小程序,调用Node服务程序2000次


dd9f851edc8273580d12aef4cac067b364379755

创建第二个文本文件,并命名为secondary.txt。内容与main.txt有显著不同即可。

在确定Node服务程序运行起来后,启动测试程序:

node test.js

在测试程序运行的同时,使用浏览器手动访问服务程序。观察服务程序在控制台的输出信息,你会看到来自浏览器的手动请求和来自测试程序的自动请求都能被处理。并且,结果与我们所期望的一致,请求到的页面中包含了如下信息:

  • 1到100的数字;
  • 文本文件的内容,在本示例中是main.txt的内容。
    现在,让我们尝试做一些改动。在示例1-3中,将循环体中计数用的局部变量counter改为全局变量,并重新启动应用程序。然后运行测试程序,并在浏览器中访问该页面。

输出结果显然发生改变。返回的页面内容不再是从1开始到100的数字,而是返回从类似2601和26301这样的数字开始的,按顺序排列的连续99个数字,只是初始值不同。

原因必然是因为使用了全局变量counter。因为在浏览器中手动访问页面时,自动测试程序也在做同样的事,他们都会更新counter。另外由于手动和自动测试程序的请求被按照顺序一个个的处理,因此没有争用共享数据的情况发生(在多线程环境中,并行访问共享数据同时保证线程安全是最主要的问题),如果你之前有期望输出一致的起始值,这里的结果可能会让你感到些许意外。

现在再次更改应用程序,但这次我们删除变量app之前的var关键字(“不小心的”使其成为一个全局变量)。曾几何时,在编写客户端JavaScript时,我们总是忘记使用var关键字。或许也只有当我们程序中用到的某些库使用了相同的变量名时,才会发现这种错误。

运行测试程序同时通过浏览器手动访问Node服务程序多次。你会发现浏览器得到的页面中偶尔会包含secondary.txt文件的内容,而不是期望的main.txt文件内容。这是因为在应用程序处理请求(带有文件名)和真正执行文件打开操作之间有一段时间间隔,在此间隔期间测试程序的持续访问会使得服务程序修改app变量。测试程序之所以能够引起这样的问题,是因为我们做了一个异步功能调用,在异步调用开始执行而没有完成前,Node会放弃对当前请求处理过程的控制权来处理另一个用户请求。

提示:
这个示例说明了正确使用var关键字在Node中是至关重要的。

相关文章
|
3月前
|
存储 JavaScript 前端开发
Node.js 函数是什么样的?
Node.js 函数是什么样的?
43 0
|
7月前
|
前端开发 JavaScript
Node——fs模块、异步
Node——fs模块、异步
Node——fs模块、异步
|
9月前
|
JavaScript 前端开发
node.js入门学习(1): 让phpstorm配置支持ES语法,箭头函数正常代码格式化
node.js入门学习(1): 让phpstorm配置支持ES语法,箭头函数正常代码格式化
76 0
|
10月前
|
JavaScript 前端开发 网络协议
|
10月前
|
存储 缓存 JavaScript
Node.js 异步流控制
Node.js 异步流控制
|
11月前
|
SQL 存储 缓存
重学Node系列02-异步实现与事件驱动
Node异步实现与事件驱动 这是重新阅读《深入浅出NodeJS》的相关笔记,这次阅读发现自己依旧收获很多,而第一次阅读的东西也差不多忘记完了,所以想着这次过一遍脑子,用自己的理解输出一下,方便记忆以及以后回忆...
62 0
|
Web App开发 JavaScript
【Node.js】全局可用变量、函数和对象
在Node.js中提供了一些全局可用的变量、函数和对象,全局就是不需要进行模块加载,可以直接使用的。其中包括全局作用域的函数和对象。也包括不在全局作用域,而在每个模块作用域都存在的变量、函数和对象,在全局可用,但不是golbal对象的属性。
85 0
node笔记记录5同步和异步1
node笔记记录5同步和异步1
41 0
node笔记记录5同步和异步1
node笔记记录6同步和异步2
node笔记记录6同步和异步2
39 0
node笔记记录6同步和异步2
node笔记记录7同步和异步3回调函数
node笔记记录7同步和异步3回调函数
41 0
node笔记记录7同步和异步3回调函数