异步流控-node.js

异步流控


JavaScript 的控制流是围绕着回调处理展开的。以下是一些有助于你开发的策略。


JavaScript 的核心设计思想是在“主”线程上是非阻塞的,也就是渲染视图的地方。你可以想象在浏览器中这一点的重要性。当主线程被阻塞时,会导致用户所不愿见到的“卡顿”,而且其他事件也无法被分派,例如导致数据获取失败。

这种情况下会产生一些独特的限制,只有函数式编程风格才能解决。这就是回调函数的用武之地。

然而,在更复杂的流程中处理回调可能会变得具有挑战性。这通常导致出现“回调地狱”,即多个嵌套的带有回调的函数使得代码更难以阅读、调试和组织等。

async1(function (input, result1) {
  async2(function (result2) {
    async3(function (result3) {
      async4(function (result4) {
        async5(function (output) {
          // do something with output
        });
      });
    });
  });
});

当然,在实际应用中,处理result1、result2等可能会有更多的代码行,因此,这个问题通常会导致代码比上面的例子更加凌乱。

这时就可以充分利用函数的优势。更复杂的操作由多个函数组成:

  1. 启动器样式/输入
  2. 中间件
  3. 终结者

“启动器样式/输入”是序列中的第一个函数。这个函数将接收操作的原始输入(如果有的话)。这个操作是一系列可执行的函数,原始输入主要有以下几种情况:

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

网络请求可以是外部网络发起的请求,也可以是同一网络上的其他应用程序发起的请求,还可以是应用程序自身在同一网络或外部网络上发起的请求。

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

function final(someInput, callback) {
  callback(`${someInput} and terminated by executing callback `);
}

function middleware(someInput, callback) {
  return final(`${someInput} touched by middleware `, callback);
}

function initiate() {
  const someInput = 'hello this is a function ';
  middleware(someInput, function (result) {
    console.log(result);
    // requires callback to `return` result
  });
}

initiate();

状态管理


函数可能依赖于状态,也可能不依赖于状态。当函数的输入或其他变量依赖于外部函数时,就会出现状态依赖。

因此,有两种主要的状态管理策略:

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

需要注意的是,我没有提到全局变量。使用全局变量来管理状态通常是一种不规范的反模式,会导致很难或者不可能保证状态的正确性。在复杂的程序中,应尽量避免使用全局变量。

控制流


如果对象在内存中可用,就可以进行迭代,并且不会改变控制流程:

function getSong() {
  let _song = '';
  let i = 100;
  for (i; i > 0; i -= 1) {
    _song += `${i} beers on the wall, you take one down and pass it around, ${
      i - 1
    } bottles of beer on the wall\n`;
    if (i === 1) {
      _song += "Hey let's get some more beer";
    }
  }

  return _song;
}

function singSong(_song) {
  if (!_song) throw new Error("song is '' empty, FEED ME A SONG!");
  console.log(_song);
}

const song = getSong();
// this will work
singSong(song);

但是,如果数据存在于内存之外,迭代将不再有效:

function getSong() {
  let _song = '';
  let i = 100;
  for (i; i > 0; i -= 1) {
    /* eslint-disable no-loop-func */
    setTimeout(function () {
      _song += `${i} beers on the wall, you take one down and pass it around, ${
        i - 1
      } bottles of beer on the wall\n`;
      if (i === 1) {
        _song += "Hey let's get some more beer";
      }
    }, 0);
    /* eslint-enable no-loop-func */
  }

  return _song;
}

function singSong(_song) {
  if (!_song) throw new Error("song is '' empty, FEED ME A SONG!");
  console.log(_song);
}

const song = getSong('beer');
// this will not work
singSong(song);
// Uncaught Error: song is '' empty, FEED ME A SONG!

为什么会发生这种情况呢?setTimeout 指令告诉 CPU 将指令存储在总线的其他位置,并指示在稍后的时间点上获取数据。在函数再次在0毫秒标记处执行之前,经过了数千个 CPU 周期,CPU 从总线上获取指令并执行它们。唯一的问题是在数千个周期之前,song('')就已经返回了。

处理文件系统和网络请求时也会遇到相同的情况。主线程不能被阻塞一段不确定的时间,因此我们使用回调函数以可控的方式安排代码的执行时间。

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

1、串行:函数将按照严格的顺序依次执行,这类似于for循环。

// operations defined elsewhere and ready to execute
const operations = [
  { func: function1, args: args1 },
  { func: function2, args: args2 },
  { func: function3, args: args3 },
];

function executeFunctionWithArgs(operation, callback) {
  // executes function
  const { args, func } = operation;
  func(args, callback);
}

function serialProcedure(operation) {
  if (!operation) process.exit(0); // finished
  executeFunctionWithArgs(operation, function (result) {
    // continue AFTER callback
    serialProcedure(operations.shift());
  });
}

serialProcedure(operations.shift());

2、完全并行:当顺序不重要时,比如向100万个电子邮件收件人发送电子邮件列表。

let count = 0;
let success = 0;
const failed = [];
const recipients = [
  { name: 'Bart', email: 'bart@tld' },
  { name: 'Marge', email: 'marge@tld' },
  { name: 'Homer', email: 'homer@tld' },
  { name: 'Lisa', email: 'lisa@tld' },
  { name: 'Maggie', email: 'maggie@tld' },
];

function dispatch(recipient, callback) {
  // `sendEmail` is a hypothetical SMTP client
  sendMail(
    {
      subject: 'Dinner tonight',
      message: 'We have lots of cabbage on the plate. You coming?',
      smtp: recipient.email,
    },
    callback
  );
}

function final(result) {
  console.log(`Result: ${result.count} attempts \
      & ${result.success} succeeded emails`);
  if (result.failed.length)
    console.log(`Failed to send to: \
        \n${result.failed.join('\n')}\n`);
}

recipients.forEach(function (recipient) {
  dispatch(recipient, function (err) {
    if (!err) {
      success += 1;
    } else {
      failed.push(recipient.name);
    }
    count += 1;

    if (count === recipients.length) {
      final({
        count,
        success,
        failed,
      });
    }
  });
});

3、有限并行:有限制的并行,例如从1000万个用户列表中成功向100万个收件人发送电子邮件。

let successCount = 0;

function final() {
  console.log(`dispatched ${successCount} emails`);
  console.log('finished');
}

function dispatch(recipient, callback) {
  // `sendEmail` is a hypothetical SMTP client
  sendMail(
    {
      subject: 'Dinner tonight',
      message: 'We have lots of cabbage on the plate. You coming?',
      smtp: recipient.email,
    },
    callback
  );
}

function sendOneMillionEmailsOnly() {
  getListOfTenMillionGreatEmails(function (err, bigList) {
    if (err) throw err;

    function serial(recipient) {
      if (!recipient || successCount >= 1000000) return final();
      dispatch(recipient, function (_err) {
        if (!_err) successCount += 1;
        serial(bigList.pop());
      });
    }

    serial(bigList.pop());
  });
}

sendOneMillionEmailsOnly();

每种模式都有自己的使用案例、优点和问题,你可以进行实验并详细阅读相关资料。最重要的是,记得将你的操作模块化并使用回调函数!如果你有任何疑问,把一切都当作中间件处理吧!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值