Node.js 异步流控制

简介: Node.js 异步流控制

1、简介

在其核心,JavaScript被设计为在“主”线程上是非阻塞的,这是呈现视图的位置。你可以想象这在浏览器中的重要性。例如,当主线程被阻塞时,会导致最终用户害怕的臭名昭著的“冻结”,并且无法调度其他事件,最终,导致数据丢失。

这就产生了一些只有函数式编程才能解决的独特约束。然而,在更复杂的过程中,回调可能会变得很难处理。这通常会导致“回调地狱”,其中带有回调的多个嵌套函数使代码在读取、调试、组织等方面更具挑战性。

例如:

1. async1(function (input, result1) {
2.  async2(function (result2) {
3.    async3(function (result3) {
4.      async4(function (result4) {
5.        async5(function (output) {
6.            // do something with output
7.        });
8.      });
9.    });
10.   });
11. });

当然,在现实生活中,很可能会有额外的代码行来处理result1、result2等,因此,这个问题的长度和复杂性通常会导致代码看起来比上面的例子混乱得多。

这就是函数的用武之地。更复杂的操作由许多功能组成:

  1. 调用方式 input
  2. 中间件
  3. 终止器

“调用方式 input”是对列中的第一个函数。此功能将接受操作的原始输入(如果有)。操作是一系列可执行的功能,原始输入主要是:

  1. 全局环境中的变量
  2. 带参数或不带参数的直接调用
  3. 通过文件系统或网络请求获得的值

网络请求可以是由外部网络、同一网络上的另一应用程序或同一网络或外部网络上的应用程序本身发起的传入请求。

中间件函数将返回另一个函数,终止器函数将调用回调。以下说明了网络或文件系统请求的流程。这里的延迟是0,因为所有这些值都在内存中可用。

1. function final(someInput, callback) {
2.  callback(`${someInput} and terminated by executing callback `);
3. }
4. function middleware(someInput, callback) {
5.  return final(`${someInput} touched by middleware `, callback);
6. }
7. function initiate() {
8. const someInput = 'hello this is a function ';
9. middleware(someInput, function (result) {
10.   console.log(result);
11.   // requires callback to `return` result
12.   });
13. }
14. initiate();

2、状态管理

函数可能与状态相关,也可能不与状态相关。当函数的输入或其他变量依赖于外部函数时,就会产生状态依赖性。

通过这种方式,有两种主要的状态管理策略:

  1. 将变量直接传递给函数
  2. 从缓存、会话、文件、数据库、网络或其他外部源获取变量值。

注意,我没有提到全局变量。用全局变量管理状态通常是一种草率的反模式,这使得很难或不可能保证状态。在可能的情况下,应避免使用复杂程序中的全局变量。

3、控制流

如果一个对象在内存中可用,则可以进行迭代,并且不会对控制流进行更改:

1. function getSong() {
2. let _song = '';
3. let i = 100;
4. for (i; i > 0; i -= 1) {
5.     _song += `${i} beers on the wall, you take one down and pass it around, ${
6.      i - 1
7.    } bottles of beer on the wall\n`;
8.  if (i === 1) {
9.         _song += "Hey let's get some more beer";
10.     }
11.   }
12. return _song;
13. }
14. function singSong(_song) {
15.   if (!_song) throw new Error("song is '' empty, FEED ME A SONG!");
16.   console.log(_song);
17. }
18. const song = getSong();
19. // this will work
20. singSong(song);

但是,如果数据在内存中不存在,则迭代将停止:

1. function getSong() {
2. let _song = '';
3. let i = 100;
4. for (i; i > 0; i -= 1) {
5. /* eslint-disable no-loop-func */
6. setTimeout(function () {
7.     _song += `${i} beers on the wall, you take one down and pass it around, ${
8.     i - 1
9.     } bottles of beer on the wall\n`;
10. if (i === 1) {
11.     _song += "Hey let's get some more beer";
12.     }
13.     }, 0);
14. /* eslint-enable no-loop-func */
15.   }
16. return _song;
17. }
18. function singSong(_song) {
19. if (!_song) throw new Error("song is '' empty, FEED ME A SONG!");
20. console.log(_song);
21. }
22. const song = getSong('beer');
23. // this will not work
24. singSong(song);
25. // Uncaught Error: song is '' empty, FEED ME A SONG!

为什么会发生这种情况?setTimeout指示CPU将指令存储在总线上的其他位置,并指示将数据安排为稍后处理。在函数在0毫秒标记处再次命中之前,经过了数千个CPU周期,CPU从总线中获取指令并执行它们。唯一的问题是song(“”)在数千个循环之前被返回。

在处理文件系统和网络请求时也会出现同样的情况。主线程不能在不确定的时间段内被阻塞——因此,我们使用回调来以可控的方式及时调度代码的执行。

我们可以使用以下3种模式执行几乎所有的操作:

3.1、串联

函数将以严格的顺序执行,这一顺序与循环最相似。

1. // operations defined elsewhere and ready to execute
2. const operations = [
3.   { func: function1, args: args1 },
4.   { func: function2, args: args2 },
5.   { func: function3, args: args3 },
6. ];
7. function executeFunctionWithArgs(operation, callback) {
8. // executes function
9. const { args, func } = operation;
10.   func(args, callback);
11. }
12. function serialProcedure(operation) {
13. if (!operation) process.exit(0); // finished
14. executeFunctionWithArgs(operation, function (result) {
15. // continue AFTER callback
16. serialProcedure(operations.shift());
17.   });
18. }
19. serialProcedure(operations.shift());

3.2、完全并行

用于同时运行异步任务

1. let count = 0;
2. let success = 0;
3. const failed = [];
4. const recipients = [
5.   { name: 'Bart', email: 'bart@tld' },
6.   { name: 'Marge', email: 'marge@tld' },
7.   { name: 'Homer', email: 'homer@tld' },
8.   { name: 'Lisa', email: 'lisa@tld' },
9.   { name: 'Maggie', email: 'maggie@tld' },
10. ];
11. 
12. function dispatch(recipient, callback) {
13. // `sendEmail` is a hypothetical SMTP client
14. sendMail(
15.     {
16. subject: 'Dinner tonight',
17. message: 'We have lots of cabbage on the plate. You coming?',
18. smtp: recipient.email,
19.     },
20.     callback
21.   );
22. }
23. 
24. function final(result) {
25. console.log(`Result: ${result.count} attempts \
26.       & ${result.success} succeeded emails`);
27. if (result.failed.length)
28. console.log(`Failed to send to: \
29.         \n${result.failed.join('\n')}\n`);
30. }
31. 
32. recipients.forEach(function (recipient) {
33. dispatch(recipient, function (err) {
34. if (!err) {
35.       success += 1;
36.     } else {
37.       failed.push(recipient.name);
38.     }
39.     count += 1;
40. 
41. if (count === recipients.length) {
42. final({
43.         count,
44.         success,
45.         failed,
46.       });
47.     }
48.   });
49. });

3.3、有限并行

一种异步、并行、并发受限的循环,例如成功地向10E7用户列表中的1000000个收件人发送电子邮件。

1. let successCount = 0;
2. function final() {
3. console.log(`dispatched ${successCount} emails`);
4. console.log('finished');
5. }
6. function dispatch(recipient, callback) {
7. // `sendEmail` is a hypothetical SMTP client
8. sendMail(
9.   {
10. subject: 'Dinner tonight',
11. message: 'We have lots of cabbage on the plate. You coming?',
12. smtp: recipient.email,
13.   },
14.   callback
15. );
16. }
17. function sendOneMillionEmailsOnly() {
18. getListOfTenMillionGreatEmails(function (err, bigList) {
19. if (err) throw err;
20. function serial(recipient) {
21. if (!recipient || successCount >= 1000000) return final();
22. dispatch(recipient, function (_err) {
23. if (!_err) successCount += 1;
24. serial(bigList.pop());
25.       });
26.   }
27. serial(bigList.pop());
28.   });
29. }
30. sendOneMillionEmailsOnly();


相关文章
|
4月前
|
存储 前端开发 JavaScript
node中循环异步的问题[‘解决方案‘]_源于map循环和for循环对异步事件配合async、await的支持
本文探讨了在Node.js中处理循环异步操作的问题,比较了使用map和for循环结合async/await处理异步事件的差异,并提供了解决方案。
51 0
|
6月前
|
数据采集 JavaScript 前端开发
NodeJS技巧:在循环中管理异步函数的执行次数
在Node.js网络爬虫开发中,管理异步函数执行次数是关键。利用Promise.all、async/await或async库能优雅地控制并发。示例展示如何用async/await配合代理IP抓取数据,避免触发反爬策略。在循环中,每个异步请求只执行一次,保证请求有序进行,提高爬虫的稳定性和效率。通过正确的方法,可以有效应对网络爬虫的挑战。
NodeJS技巧:在循环中管理异步函数的执行次数
|
5月前
|
存储 JavaScript API
Node.js中的异步API
【8月更文挑战第16天】
43 1
|
8月前
|
JavaScript 大数据 开发者
Node.js的异步I/O模型与事件循环:深度解析
【4月更文挑战第29天】本文深入解析Node.js的异步I/O模型和事件循环机制。Node.js采用单线程与异步I/O,遇到I/O操作时立即返回并继续执行,结果存入回调函数队列。事件循环不断检查并处理I/O事件,通过回调函数通知结果,实现非阻塞和高并发。这种事件驱动编程模型简化了编程,使开发者更专注业务逻辑,为高并发场景提供高效解决方案。
|
前端开发 JavaScript
Node——fs模块、异步
Node——fs模块、异步
Node——fs模块、异步
|
JavaScript 前端开发 网络协议
|
SQL 存储 缓存
重学Node系列02-异步实现与事件驱动
Node异步实现与事件驱动 这是重新阅读《深入浅出NodeJS》的相关笔记,这次阅读发现自己依旧收获很多,而第一次阅读的东西也差不多忘记完了,所以想着这次过一遍脑子,用自己的理解输出一下,方便记忆以及以后回忆...
94 0
node笔记记录5同步和异步1
node笔记记录5同步和异步1
76 0
node笔记记录5同步和异步1
node笔记记录6同步和异步2
node笔记记录6同步和异步2
60 0
node笔记记录6同步和异步2
node笔记记录7同步和异步3回调函数
node笔记记录7同步和异步3回调函数
60 0
node笔记记录7同步和异步3回调函数