随笔:Node的流程控制

399 阅读3分钟

Node的流程控制

什么是流程控制

在Node中,流程控制是让一组异步任务顺序执行的概念,流程控制分为串行并行
串行示意图:

graph TD
Start --> work-1  --> work-2 --> ... --> work-n --> Stop

并行示意图:

graph LR
Start --> work-1 --> Stop
Start --> work-2 --> Stop
Start --> ...    --> Stop
Start --> work-n --> Stop

串行流程控制

串行的应用场景

串行流程控制是在某些情况下,相关操作必须按顺序执行而产生的一种控制方式。比如我要在一个新目录test里创建一个新文件test1.txt,并且在test1.txt中写入hello world,然后再创建一个新文件test2.js读取test1.txt中的hello word一样,这里的顺序只能有一个,尽管文件的读写是异步,但是我们只能等待上一步完成,才能继续进行下一步。

graph LR
创建目录test --> 创建新文件test1.txt --> 在test1.txt中写入helloword --> 创建新文件test2.js --> 写下读取test1.txt的代码

实现串行的方法

  • 使用定时器setTimeout来模拟
    这里使用三个定时器来模拟,当第一个定时器回调函数执行后会将第二个定时器挂起等待回调执行,然后执行第二个回调,挂起第三个定时器等待回调执行,最后执行第三个回调,依次输出:
graph LR
first --> next  --> last
setTimeout(() => {
    console.log('I execute first.');
    setTimeout(() => {
        console.log('I execute next.');
        setTimeout(() => {
            console.log('I execute last.');
        }, 100);
    }, 500);
}, 1000);
  • 使用async库实现
    利用第三方开源库async可以轻松实现顺序执行,只需要把顺序执行的函数放入数组中即可。 使用前先安装async
npm i --save async
const async = require('async');
async.series([
  callback => {
    setTimeout(() => {
      console.log('I execute first.');
      callback();
    }, 1000);
  },
  callback => {
    setTimeout(() => {
      console.log('I execute next.');
      callback();
    }, 500);
  },
  callback => {
    setTimeout(() => {
      console.log('I execute last.');
      callback();
    }, 100);
  }
]);
  • 手动实现
    串行化流程控制本质上是在需要时让回调进场,而不是简单地把它们嵌套起来。这里我们定义一个tasks,用它来保存每一个异步函数的操作,通过next()来依次调用,并将结果作为参数传入下一个函数中去,从而保证执行的顺序。
const fs = require('fs');
const request = require('request');
const htmlparser = require('htmlparser');
const configFilename = './rss_feeds.txt';

function checkForRSSFile() {
  fs.exists(configFilename, (exists) => {
    if (!exists)
      return next(new Error(`Missing RSS file: ${configFilename}`));
    next(null, configFilename);
  });
}

function readRSSFile(configFilename) {
  fs.readFile(configFilename, (err, feedList) => {
    if (err) return next(err);
    feedList = feedList
      .toString()
      .replace(/^\s+|\s+$/g, '')
      .split('\n');
    const random = Math.floor(Math.random() * feedList.length);
    next(null, feedList[random]);
  });
}

function downloadRSSFeed(feedUrl) {
  request({
    uri: feedUrl
  }, (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) {
  const handler = new htmlparser.RssHandler();
  const parser = new htmlparser.Parser(handler);
  parser.parseComplete(rss);
  if (!handler.dom.items.length)
    return next(new Error('No RSS items found'));
  const item = handler.dom.items.shift();
  console.log(item.title);
  console.log(item.link);
}
const tasks = [
  checkForRSSFile,
  readRSSFile,
  downloadRSSFeed,
  parseRSSFeed
];

function next(err, result) {
  if (err) throw err;
  const currentTask = tasks.shift();
  if (currentTask) {
    currentTask(result);
  }
}
next();

并行流程控制

并行的应用场景

同时请求多个资源、读取多个文件

实现并行的方法

为了让异步任务并行执行,仍然是要把任务放到数组中,但任务的存放顺序无关紧要。每个任务都应该调用处理器函数增加已完成任务的计数值。当所有任务都完成后,处理器函数应该执行后续的逻辑。
用并行化流程控制实现对几个文件中单词频度的计数:

graph TD
得到目录中的文件列表 --> 读取文件1  --> 显示单词的计数值  
得到目录中的文件列表 --> 读取文件2  --> 显示单词的计数值
得到目录中的文件列表 --> ...  --> 显示单词的计数值
得到目录中的文件列表 -->  读取文件n --> 显示单词的计数值

手动实现并行

const fs = require('fs');
const tasks = [];
const wordCounts = {};
const filesDir = './text';
let completedTasks = 0;

function checkIfComplete() {
  completedTasks++;
  if (completedTasks === tasks.length) {
    for (let index in wordCounts) {
      console.log(`${index}: ${wordCounts[index]}`);
    }
  }
}

function addWordCount(word) {
  wordCounts[word] = (wordCounts[word]) ? wordCounts[word] + 1 : 1;
}

function countWordsInText(text) {
  const words = text
    .toString()
    .toLowerCase()
    .split(/\W+/)
    .sort();
  words
    .filter(word => word)
    .forEach(word => addWordCount(word));
}

fs.readdir(filesDir, (err, files) => {
  if (err) throw err;
  files.forEach(file => {
    const task = (file => {
      return () => {
        fs.readFile(file, (err, text) => {
          if (err) throw err;
          countWordsInText(text);
          checkIfComplete();
        });
      };
    })(`${filesDir}/${file}`);
    tasks.push(task);
  })
  tasks.forEach(task => task());
});