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等,因此,这个问题的长度和复杂性通常会导致代码看起来比上面的例子混乱得多。
这就是函数的用武之地。更复杂的操作由许多功能组成:
- 调用方式 input
- 中间件
- 终止器
“调用方式 input”是对列中的第一个函数。此功能将接受操作的原始输入(如果有)。操作是一系列可执行的功能,原始输入主要是:
- 全局环境中的变量
- 带参数或不带参数的直接调用
- 通过文件系统或网络请求获得的值
网络请求可以是由外部网络、同一网络上的另一应用程序或同一网络或外部网络上的应用程序本身发起的传入请求。
中间件函数将返回另一个函数,终止器函数将调用回调。以下说明了网络或文件系统请求的流程。这里的延迟是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、状态管理
函数可能与状态相关,也可能不与状态相关。当函数的输入或其他变量依赖于外部函数时,就会产生状态依赖性。
通过这种方式,有两种主要的状态管理策略:
- 将变量直接传递给函数
- 从缓存、会话、文件、数据库、网络或其他外部源获取变量值。
注意,我没有提到全局变量。用全局变量管理状态通常是一种草率的反模式,这使得很难或不可能保证状态。在可能的情况下,应避免使用复杂程序中的全局变量。
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();