Node.js编程之异步

异步操作

Node采用V8引擎处理JavaScript脚本,最大特点就是单线程运行,一次只能运行一个任务。这导致Node大量采用异步操作(asynchronous opertion),即任务不是马上执行,而是插在任务队列的尾部,等到前面的任务运行完后再执行。

由于这种特性,某一个任务的后续操作,往往采用回调函数(callback)的形式进行定义。

var isTrue = function(value, callback) {
  if (value === true) {
    callback(null, "Value was true.");
  }
  else {
    callback(new Error("Value is not true!"));
  }
}

上面代码就把进一步的处理,交给回调函数callback。

如果没有发生错误,回调函数的第一个参数就传入null。这种写法有一个很大的好处,就是说只要判断回调函数的第一个参数,就知道有没有出错,如果不是null,就肯定出错了。另外,这样还可以层层传递错误。

Node约定,如果某个函数需要回调函数作为参数,则回调函数是最后一个参数。另外,回调函数本身的第一个参数,约定为上一步传入的错误对象。

var callback = function (error, value) {
  if (error) {
    return console.log(error);
  }
  console.log(value);
}

异步开发的难题

在创建异步程序时,你必须密切关注程序的执行流程,并盯牢程序状态:事件轮训的条件、程序变量以及其他随着程序逻辑执行而发生变化的资源。如果不小心,程序的变量也可能会出现意想不到的变化。下面这段代码是一段因为执行顺序而导致混乱的异步代码。

如果例子中的代码能够同步执行,可以肯定输出的应该是"The color is blue",可这个例子是异步的,在console.log执行前color的值还在变化,所以输出是"The color is green".

function asyncFunction(callback) {
  setTimeout(callback, 200)
}

var color = ‘blue‘

asyncFunction(function(){
  console.log(‘The color is ‘ + color)  // The color is green.(这个最后执行(200ms之后))
})

color = ‘green‘

用JavaScript闭包可以"冻结"color的值,在如下代码中,对asyncFunction的调用被封装到了一个以color为参数的匿名函数里,这样就可以马上执行这个匿名函数,把当前的color的值传给它。而color变成了匿名函数的参数,也就是这个匿名函数内部的本地变量,当匿名函数外面的color值发生变化时,本地版的color不会受影响。

function asyncFunction(callback) {
  setTimeout(callback, 200)
}

var color = ‘blue‘

(function(color) {
  asyncFunction(function(){
    console.log(‘The color is ‘ + color)  // The color is blue.
  })
})(color);

color = ‘green 

在Node开发中需要用到很多JavaScript编程技巧,这只是其中之一。

现在我们知道怎么用闭包控制程序的状态了,接下来我们看看怎么让异步逻辑顺序执行。

异步流程的顺序化

让一组异步任务顺序执行的概念被Node社区称为流程控制。这种控制分为两类:串行和并行,

什么时候使用串行流程控制

可以使用回调让几个异步任务按顺序执行,但如果任务很多,必须组织一下,否则会陷入回调地狱。

下面这段代码就是用回调让任务顺序执行的。

setTimeout(function(){
   console.log(‘I execute first.‘)
   setTimeout(function(){
      console.log(‘I execute next.‘)
      setTimeout(function(){
         console.log(‘I execute last.‘)
      }, 100)
   }, 500)
}, 1000)    

此外,也可以用Promise这样的流程控制工具来执行这些代码

 promise.then(function(result){
        // dosomething
        return result;
    }).then(function(result) {
        // dosomething
        return promise1;
    }).then(function(result) {
        // dosomething
    }).catch(function(ex) {
        console.log(ex);
    }).finally(function(){
        console.log("final");
    });

接着我们通过例子,自己来实现串行化流程控制和并行化流程控制

实现串行化流程控制

为了用串行化流程控制让几个异步任务按顺序执行,需要先把这些任务按预期的执行顺序放到一个数组中。如下图所示:

下面是一个串行化流程控制的demo,实现了从随机选择的RSS预定源中获取一篇文章的标题和URL,源文件

// 在一个简单的程序中实现串行化流程控制
var fs = require(‘fs‘)
var request = require(‘request‘) // 用它获取RSS数据
var htmlparser = require(‘htmlparser‘) // 把原始的RSS数据转换成JavaScript结构
var configFilename = ‘./rss_feeds.txt‘

function checkForRSSFile() { // 任务1:确保包含RSS预定源URL列表的文件存在
  fs.exists(configFilename, function(exists) {
    if (!exists) {
      return next(new Error(‘Missing RSS file: ‘ + configFilename)) // 只要有错误就尽早返回
    }
    next(null, configFilename)
  })
}

function readRSSFile (configFilename) { // 任务2:读取并解析包含预定源URL的文件
  fs.readFile(configFilename, function(err, feedList) {
    if (err) {
      return next(err)
    }

    feedList = feedList                          // 讲预定源URL列表转换成字符串,然后分隔成一个数组
                 .toString()
                 .replace(/^\s+|\s+$/g, ‘‘)
                 .split("\n");
    var random = Math.floor(Math.random()*feedList.length)  // 从预定源URL数组中随机选择一个预定源URL
    next(null, feedList[random])
  })
}
// console.log(‘进入‘)

function downloadRSSFeed(feedUrl) {  // 任务3:向选定的预定源发送HTTP请求以获取数据
  request({uri: feedUrl}, function(err, res, body) {
    if (err) {
      return next(err)
    }
    if (res.statusCode != 200) {
      return next(new Error(‘Abnormal response status code‘))
    }

    next(null, body)
  })
}

function parseRSSFeed(rss) {  // 任务4:将预定源数据解析到一个条目数组中
  var handler = new htmlparser.RssHandler()
  var parser = new htmlparser.Parser(handler)
  parser.parseComplete(rss)
  if (!handler.dom.items.length) {
    return next(new Error(‘No RSS items found‘))
  }
  console.log(handler.dom.items)
  var item = handler.dom.items.shift()
  console.log(item.title)
  console.log(item.link)
}
var tasks = [ checkForRSSFile,      // 把所有要做的任务按执行顺序添加到一个数组中
              readRSSFile,
              downloadRSSFeed,
              parseRSSFeed ]

function next(err, result) {
  if (err) {
    throw err
  }

  var currentTask = tasks.shift()   // 从任务数组中取出下个任务

  if (currentTask) {
    currentTask(result)   // 执行当前任务
  }
}

next()  // 开始任务的串行化执行

如本例所示,串行化流程控制本质上是在需要时让回调进场,而不是简单地把它们嵌套起来

实现并行化流程控制

为了让异步任务并行执行,仍然是要把任务放到数组中,但任务的存放顺序无关紧要。每个任务都应该调用处理器函数增加已完成任务的计数值。当所有任务都完成后,处理器函数应该执行后续的逻辑。

来看一个并行化流程控制的小demo,该demo实现了在控制台中统计打印出所有单词分别出现的总数。源文件

// 在一个简单的程序中实现并行流程控制
var fs = require(‘fs‘)
var completedTasks = 0
var tasks = []
var wordCounts = {}
var filesDir = ‘./text‘

function checkIfComplete() {   // 当所有任务全部完成后,列出文件中用到的每个单词以及用了多少次
  completedTasks++
  // console.log(completedTasks)
  console.log(tasks.length)
  if (completedTasks == tasks.length) {
    for(var index in wordCounts) {
      console.log(index + ‘: ‘ + wordCounts[index])
    }
  }
}

function countWordsInText(text) {
  var words = text
    .toString()
    .toLowerCase()
    .split(/\W+/)
    .sort()
  for (var index in words) {     // 对文本中出现的单词计数
    var word = words[index]
    if (word) {
      wordCounts[word] = (wordCounts[word]) ? wordCounts[word] + 1 : 1
    }
  }
}

fs.readdir(filesDir, function(err, files) {    // 得出text目录中的文件列表
  if (err) {
    throw err
  }
  for(var index in files) {
    var task = (function(file) {     // 定义处理每个文件的任务,每个任务中都会调用一个异步读取文件的函数并对文件中使用的单词计数
      return function() {
        fs.readFile(file, function(err, text) {   // 这里注意fs.readFile()是一个异步进程,countWordsInText(),checkIfComplete()方法会在tasks.push()方法后面进行
          if (err) {
            throw err
          }
          countWordsInText(text)
          checkIfComplete()
        })
      }
    })(filesDir + ‘/‘ + files[index])
    tasks.push(task)
  }
  for(var task in tasks) {
    tasks[task]()
  }
})

如上两个demos阐述了串行和并行化流程控制的底层机制。

总结

可以用回调、事件发射器和流程控制管理异步逻辑。回调适用于一次性异步逻辑;事件发射器对组织异步逻辑很有帮助,因为它们可以把异步逻辑跟一个概念实体关联起来,可以通过监听器轻松管理;流程控制可以管理异步任务的执行顺序,可以让它们一个接一个执行,也可以同步执行。你可以自己实现流程管理,但社区附加模块可以帮你解决这个麻烦。选择哪个流程控制附加模块很大程度取决于个人喜好以及项目或设计的需求。

时间: 2024-08-25 16:30:33

Node.js编程之异步的相关文章

Node.js入门:异步IO

异步IO 在操作系统中,程序运行的空间分为内核空间和用户空间.我们常常提起的异步I/O,其实质是用户空间中的程序不用依赖内核空间中的I/O操作实际完成,即可进行后续任务. 同步IO的并行模式 多线程单进程    多线程的设计之处就是为了在共享的程序空间中,实现并行处理任务,从而达到充分利用CPU的效果.多线程的缺点在于执行时上下文交换的开销较大,和状态同步(锁)的问题.同样它也使得程序的编写和调用复杂化. 单线程多进程 为了避免多线程造成的使用不便问题,有的语言选择了单线程保持调用简单化,采用启

node.js编程规范

B.1缩进 因为Node.js代码中很容易写出深层的函数嵌套,过多的空格会给阅读带来不便,因此我们选择两空格缩进 B.2行宽 为了保证在任何设备上都可以方便地阅读,我们建议把行宽限制为80个字符. B.3 语句分隔符 建议一律使用分号( ; ),哪怕一行只有一个语句,也不要省略分号. B.4 变量定义 永远使用var 定义变量,而不要通过赋值隐式定义变量.因为通过赋值隐式定义的变量总是全局变量,会造成命名空间污染. 使用var 定义变量时,确保每个语句定义一个变量,而不要通过逗号( , )把多个

Node.js 同步与异步编程

同步API: 只有当前API执行完成之后,才能继续执行下一行API.从上往下,一行一行的执行. console.log("one") console.log("two") 异步API: 当前的API执行不会阻塞后续代码的执行. console.log("one") setTimeout ( () => console.log("two"), 3000) console.log("three") 同步A

Node.js中的异步I/O是如何进行的?

Node.js的异步I/O通过事件循环的方式实现.其中异步I/O又分磁盘I/O和网络I/O.在磁盘I/O的调用中,当发起异步调用后,会将异步操作送进libuv提供的队列中,然后返回.当磁盘I/O执行完成之后,会形成一个事件,事件循环的过程中发现该事件后,会将其消费.消费过程就是将得到的数据和传入的回调函数执行. 网络I/O与磁盘I/O的差异在于它不需要线程池来进行处理,而是在每次时间循环的过程中通过IOCP/epoll/kqueue/event ports来获取网络I/O的 事件队列. 摘自:<

node.js同步及异步读取写入删除文件1

node.js初学中,在文件中同步及异步读取文档的过程: 1.同步读取: var fs=require("fs") //直接读取文档,并将同步返回值,赋值给变量 var data=fs.readFileSync("input.txt"); console.log(data.toString()); 2.异步读取: var fs=require("fs"); //通过回调函数返回获得的data值: fs.readFile("input.t

开始用Node.js编程

一切程序的开始都是hello world 哈哈 打开文本编辑器 在里面写入:console.log('Hello World'); 保存为 helloworld.js,打开终端,进入 helloworld.js 所在的目录 执行:node helloworld.js 如果一切正常,你将会在终端中看到输出 Hello World 太简单了吧,没什么技术含量,不过还得唠叨两句 console 是 Node.js 提供的控制台对象,其中包含了向标准输出写 入的操作,如 console.log.cons

【Node.js开发指南 BYVoid】3.1 开始使用 Node.js编程

3.1.1 Hello World 打开vscode, 输入console.log("helloworld"); 新建保存文件名为helloworld.js 打开终端,进入helloword.js所在的目录(shift+右键空白处,在此处打开命令行),执行 node helloworld.js 常用输出指令 console.log console.error console.log是最常用的指令,和C语言中的printf功能类似,也可以接受任意多个参数,支持%d %s变量的引用 3.1

深入理解node.js异步编程

1. 概述目前开源社区最火热的技术当属Node.js莫属了,作为使用Javascript为主要开发语言的服务器端编程技术和平台,一开始就注定会引人瞩目. 当然能够吸引众人的目光,肯定不是三教九流之辈,必然拥有独特的优势和魅力,才能引起群猿追逐.其中当属异步IO和事件编程模型,本文据Node.js的异步IO和事件编程做深入分析. ##2. 什么是异步同步和异步是一个比较早的概念,大抵在操作系统发明时应该就出现了.举一个最简单的生活中的例子,比如发短信的情况会比较好说明他们的区别:同步:正在处于苦逼

node.js 异步式I/O 与事件驱动

Node.js 最大的特点就是异步式 I/O(或者非阻塞 I/O)与事件紧密结合的编程模式.这种模式与传统的同步式 I/O 线性的编程思路有很大的不同,因为控制流很大程度上要靠事件和回调函数来组织,一个逻辑要拆分为若干个单元. 阻塞与线程什么是阻塞(block)呢?线程在执行中如果遇到磁盘读写或网络通信(统称为 I/O 操作),通常要耗费较长的时间,这时操作系统会剥夺这个线程的 CPU 控制权,使其暂停执行,同时将资源让给其他的工作线程,这种线程调度方式称为 阻塞.当 I/O 操作完毕时,操作系