研究Promise 方式实现Node.js 实践应用论文

时间:2021-08-31

  Node.js 是建立在Chrome V8 引擎的javaScript 运行时之上的平台, 用于构建快速、可扩展的Web 应用程序. Node.js 采用单线程、事件驱动、非阻塞的I/O模型, 这些特性不仅带来了巨大的性能提升, 还减少了多线程程序设计的复杂性, 进而提高了开发效率,使其轻量又高效. 传统的Node.js 在处理异步问题时,一般采用的是callback 回调的方式. callback 回调存在一个很严重的金字塔问题——大量的回调函数慢慢向右侧屏幕延伸的一种状态.

研究Promise 方式实现Node.js 实践应用论文

  Promise 是异步编程的一种解决方案, 比传统的解决方案——回调函数和事件, 更合理和强大. 它最早由javascript 社区提出和实现, 目前最新的JavaScript语言标准ES6 已将其写进了标准中, 统一了用法, 原生提供了Promise 对象. 借助Promise 对象, 可以将异步操作以同步操作的流程表达出来, 避免了层层嵌套的回调函数.

  本文就是采用Promise 方式在Node.js 平台上搭建了一个网络爬虫的应用. 本文首先介绍了Node.js 平台以及其相关的一些特点和概念, 然后在此基础上, 针对其传统的callback 的回调方式的“回调地狱”等问题,引入了Promise 对象来处理这种异步回调的问题. 通过深入分析Promise 对象的理论知识以及规范, 将其合理地运用到网络爬虫的应用中去. 最后通过爬取一个课程网站的视频课程信息, 充分展示了Node.js 平台的强大和方便, 以及Promise 对象在处理异步回调问题上的优越性以及新思路.

  1.Node.js平台介绍

  Node.js 是一位叫Ryan Dahl 的程序员发明的. 他的工作是用C/C++写高性能Web 服务. 对于高性能,异步IO、事件驱动是基本原则, 但是用C/C++写就太痛苦了. 于是Ryan 开始设想用高级语言开发Web 服务.他评估了很多种高级语言, 发现很多语言虽然同时提供了同步IO 和异步IO, 但是开发人员一旦用了同步IO, 他们就再也懒得写异步IO 了, 所以, 最终, Ryan瞄向了JavaScript. 因为JavaScript 是单线程执行, 根本不能进行同步IO 操作, 所以, JavaScript 的这一“缺陷”导致了它只能使用异步IO.

  选定了开发语言, 还要有运行时引擎. Ryan 曾考虑过自己写一个, 不过明智地放弃了, 因为V8 就是开源的JavaScript 引擎. 让Google 投资去优化V8, 我们只管拿过来用就好了.于是在2009 年, Ryan 正式推出了基于JavaScript语言和V8 引擎的开源Web 服务器项目, 命名为Node.js. Node 第一次把JavaScript 带入到后端服务器开发, 加上世界上已经有无数的JavaScript 开发人员,所以Node.js 一下子就火了起来.Node.js 主要特点是(1)时间驱动、异步编程; (2)单进程单线程.

  1.1 事件驱动、异步编程

  事件驱动并不是Node.js 专属, 在某些传统语言的网络编程中, 我们会用到回调函数, 比如当socket 资源达到某种状态时, 注册的回调函数就会执行.Node.js 的设计思想中以事件驱动为核心, 它提供的绝大多数API 都是基于事件的、异步的风格. 以Net 模块为例, 其中的net.Socket 对象就有以下事件:connect、data、end、timeout、drain、error、close 等, 使用Node.js 的开发人员需要根据自己的业务逻辑注册相应的回调函数. 这些回调函数都是异步执行的, 这意味着虽然在代码结构中, 这些函数看似是依次注册的, 但是它们并不依赖于自身出现的顺序, 而是等待相应的事件触发. 事件驱动、异步编程的设计重要的优势在于, 充分利用了系统资源, 执行代码无须阻塞等待某种操作完成, 有限的资源可以用于其他的任务.此类设计非常适合于后端的网络服务编程, Node.js 的目标也在于此. 在服务器开发中, 并发的请求处理是个大问题, 阻塞式的函数会导致资源浪费和时间延迟.通过事件注册、异步函数, 开发人员可以提高资源的利用率, 性能也会改善.从Node.js 提供的支持模块中, 我们可以看到包括文件操作在内的许多函数都是异步执行的, 这和传统语言存在区别, 而且为了方便服务器开发, Node.js 的网络模块特别多, 包括HTTP、DNS、NET、UDP、HTTPS、TLS 等, 开发人员可以在此基础上快速构建Web 服务器.

  1.2 单进程单线程

  1.2.1 高性能

  Node.js 单线程模式避免了传统php 那样频繁创建、切换线程的花销, 执行速度更快. 而且, 资源占用小, Node.js 在大负荷下对内存占用任然很低.

  1.2.2 线程安全

  单线程的node.js 还保证了绝对的线程安全, 不用担心统一变量同时被多个线程进行读写而造成程序崩溃. 线程安全的同时也解放了开发人员, 免去了多线程编程中忘记对变量加锁或者解锁造成的隐患.

  2.Promise

  Promise 主要解决JavaScript 中异步的场景.Promise 是个对象, 同JavaScript 中其它对象没什么区别, 但同时它也是一个规范, 针对异步操作约定了统一的接口, 表示一个一步操作最终的结果, 以同步的方式来写代码, 执行的操作是异步的, 但是又保证程序的执行顺序是同步的. 这原本是JavaScript 社区的一个规范的构想, 现在已经被加入到了ES6 的语言标准中, Firefox 和Chrome 等浏览器已经对它进行了实现.

  2.1 同步与异步

  JS 引擎是单线程的. 这意味着在任何环境中, 只有一段JS 代码会被执行. 每个函数是一个不可分割的片段或者代码块. 当JS 引擎开始执行一个函数(比如回调函数)时, 它就会把这个函数执行完, 只有执行完这段代码才会继续执行后面的代码. 这就是JS 中的同步. Promise 对象的then()方法就是同步处理每个Promise 对象.异步是指在执行一段代码时, 这段代码依赖一些其他的操作或者数据, 这时就不用等待数据或者操作的返回, 直接执行下一段代码, 当有数据或操作返回时再去响应之前的代码, 从而提高代码执行的效率.

  2.2 Promise 对象的状态

  Promise 对象只有三种状态:

  (1) Pending: 初始状态, 进行中.

  (2) Resolved(或Fulfilled): 成功的操作.

  (3) Rejected: 失败的操作.

  (1) Promise 对象的状态不受外界影响.

  Promise 对象代表一个异步操作, 有三种状态:Pending(进行中)、Resolved(已完成, 又称Fulfilled)和Rejected(已失败). 只有异步操作的结果, 可以决定当前是哪一种状态, 任何其他操作都无法改变这个状态.

  (2) Promise 对象一旦状态改变, 就不会再变, 任何时候都可以得到这个结果.Promise 对象的状态改变, 只有两种可能: 从Pending 变为Resolved 和从Pending 变为Rejected. 只要这两种情况发生, 状态就凝固了, 不会再变了, 会一直保持这个结果. 就算改变已经发生了, 再对Promise 对象添加回调函数, 也会立即得到这个结果.

  2.3 Promise 的核心方法

  Promise 对象的核心部件是它的then 方法, 它的作用是为Promise 实例添加状态改变时的回调函数. then方法接受两个回调函数作为参数. 第一个回调函数是Promise 对象的状态变为Resolved 时调用, 第二个回调函数是Promise 对象的状态变为Rejected 时调用. 其中,第二个函数是可选的, 不一定要提供. 这两个函数都接受Promise 对象传出的值作为参数.Promise 对象另一个核心方法是它的catch 方法,用于指定发生错误时的回调函数, 是then(null,rejection)的别名. catch 方法可以捕捉promise 实例中的异常还能捕获在它之前太狠方法中发生的异常, 所以在实际的使用中, 多用catch 方法来取代then(null,rejection)处理异常.

  3.爬虫应用设计与实现

  3.1 模块加载

  新建一个promise_crawler.js 文件, 首先把需要的相应的模块加载进来.http 模块: 主要用于搭建 HTTP 服务端和客户端,使用 HTTP 服务器或客户端功能必须调用 http 模块;bluebird 模块: Promise 类库(在最新的Node.js 里已经引入了Promise 模块, 可直接使用, 但考虑到兼容性问题, 本例中采用bluebird 模块);cheerio 模块: 类似于前端的jQuery, 能够简单方便地操作装在后台的html.

  3.2 组织数据结构

  首先在chrome 浏览器中打开需要爬取的网页, 同时打开控制台查看网页html DOM 结构, 分析出所需信息, 组织好数据结构, 然后根据DOM结构去获取所需信息.

  3.3 Promise 主要流程

  本例中完成的主要功能是, 同时爬取一个课程网站的多个页面, 获取相关信息, 然后将数据按照组织好的数据结构打印出来.代码中所用到的Promise.all 方法用于将多个Promise 实例, 包装成一个新的Promise 实例.该方法接收一个Promise 对象数组作为参数, p1、p2、p3 都是Promise 对象的实例.p 的状态由p1、p2、p3 决定, 分成两种情况.

  (1) 只有p1、p2、p3 的状态都变成Resolved, p 的状态才会变成Resolved, 此时p1、p2、p3 的返回值组成一个数组, 传递给p 的回调函数.

  (2) 只要p1、p2、p3 之中有一个被rejected, p 的状态就变成Rejected, 此时第一个被Rejected 的实例的返回值, 会传递给p 的回调函数.

  3.4 相关函数实现

  3.4.1 爬取页面getPageAsync(url)

  通过http 模块的get 方法爬取页面数据, 最后返回一个Promise 对象, 方便异步处理.

  3.4.2 过滤数据filterChapters(html)

  过滤出每个页面所需的数据, 然后按一定的数据结构组织起来.

  3.4.3 打印数据printCourseInfo(coursesData)

  将爬取到的数据, 按照组织好的数据结构打印出来.

  3.4 实验结果

  执行promise_crawler.js 文件, 即可看到输出的相关信息实验中同爬取了4 个页面, 可以看到, 实验结果是按照代码中设定好的数据结构爬取并打印出来的,符合实验预期. Promise 对象是基于异步的方式来处理程序的. 爬取每个页面时, 不用等待页面的数据处理完毕再去爬取下一个页面, 而是无阻塞不间断的去爬取每个页面, 当有异步的数据返回时调用Promise 对象的resolve()方法去处理, 出现错误异常时调用reject()方法去解决. 当有多个Promise 对象时, 调用then(onFulfilled)方法, 同步处理每个Promise 对象, 一旦处理哪个Promise 对象出错时, 可以立即调用catch方法处理异常, 中止程序往下执行, 及时发现错误.而且onFulfilled()方法每次返回的是新的Promise 对象,这样保证了then()可以一直链式调用下去, 提高了程序的效率和可靠性.

  4.结语

  Node.js 作为一门新兴的技术, 打通了前后端的界限. 由于采用事件驱动和无阻塞模型, 可以很方便的构建高效、可扩展的网络应用, 这是Node.js 最大的一个优点, 同时也是最大的一个缺点, 由于事件驱动和无阻塞模型是建立在callback 这种回调方式上的, 随着回调的增加, 代码嵌套的层次就会增加, 这样很容易陷入“回调地狱”, 这种代码难以编写, 难以理解而且难以维护.Promise 对象是解决Node.js 中异步回调的一种很有效的方式. 借助Promise 对象, 可以将异步操作以同步操作的流程表达出来, 避免了层层嵌套的回调函数.在保证异步回调的基础上又实现了多个promise 对象之间的同步顺序, 使程序能快速高效的执行下去, 给我们的开发带来很大的便利.