跳到内容

异步流控制

【Asynchronous flow control】

本文的内容主要灵感来源于 Mixu 的 Node.js 书

从本质上讲,JavaScript 旨在在“主”线程上非阻塞运行,这里正是视图被渲染的地方。你可以想象这一点在浏览器中的重要性。当主线程被阻塞时,就会导致终端用户害怕的臭名昭著的“卡顿”,并且无法调度其他事件,从而导致数据获取的丢失,例如。

【At its core, JavaScript is designed to be non-blocking on the "main" thread, this is where views are rendered. You can imagine the importance of this in the browser. When the main thread becomes blocked it results in the infamous "freezing" that end users dread, and no other events can be dispatched resulting in the loss of data acquisition, for example.】

这产生了一些只有函数式编程风格才能解决的独特约束。这就是回调出现的地方。

【This creates some unique constraints that only a functional style of programming can cure. This is where callbacks come in to the picture.】

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

【However, callbacks can become challenging to handle in more complicated procedures. This often results in "callback hell" where multiple nested functions with callbacks make the code more challenging to read, debug, organize, etc.】

async1(function (, ) {
  async2(function () {
    async3(function () {
      async4(function () {
        async5(function () {
          // do something with output
        });
      });
    });
  });
});

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

【Of course, in real life there would most likely be additional lines of code to handle result1, result2, etc., thus, the length and complexity of this issue usually results in code that looks much more messy than the example above.】

这就是 函数 大显身手的地方。更复杂的操作是由许多函数组成的:

  1. 发起者类型 / 输入
  2. 中间件
  3. 终端

“启动器样式/输入”是序列中的第一个功能。此功能将接受操作的原始输入(如果有的话)。该操作是一系列可执行的函数,而原始输入主要将用于:

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

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

【Network requests can be incoming requests initiated by a foreign network, by another application on the same network, or by the app itself on the same or foreign network.】

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

【A middleware function will return another function, and a terminator function will invoke the callback. The following illustrates the flow to network or file system requests. Here the latency is 0 because all these values are available in memory.】

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

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

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

();

状态管理

【State management】

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

【Functions may or may not be state dependent. State dependency arises when the input or other variable of a function relies on an outside function.】

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

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

注意,我没有提到全局变量。使用全局变量来管理状态通常是一种草率的反模式,会使保证状态变得困难甚至不可能。在复杂程序中,应尽量避免使用全局变量。

【Note, I did not mention global variable. Managing state with global variables is often a sloppy anti-pattern that makes it difficult or impossible to guarantee state. Global variables in complex programs should be avoided when possible.】

控制流

【Control flow】

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

【If an object is available in memory, iteration is possible, and there will not be a change to control flow:】

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

  return ;
}

function () {
  if (!) {
    throw new ("song is '' empty, FEED ME A SONG!");
  }

  .();
}

const  = ();
// this will work
();

但是,如果数据存在于内存之外,迭代将不再起作用:

【However, if the data exists outside of memory the iteration will no longer work:】

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

  return ;
}

function () {
  if (!) {
    throw new ("song is '' empty, FEED ME A SONG!");
  }

  .();
}

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

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

【Why did this happen? setTimeout instructs the CPU to store the instructions elsewhere on the bus, and instructs that the data is scheduled for pickup at a later time. Thousands of CPU cycles pass before the function hits again at the 0 millisecond mark, the CPU fetches the instructions from the bus and executes them. The only problem is that song ('') was returned thousands of cycles prior.】

在处理文件系统和网络请求时也会出现同样的情况。主线程不能被无期限地阻塞 - 因此,我们使用回调函数来以可控的方式安排代码的执行时间。

【The same situation arises in dealing with file systems and network requests. The main thread simply cannot be blocked for an indeterminate period of time-- therefore, we use callbacks to schedule the execution of code in time in a controlled manner.】

你将能够使用以下 3 种模式执行几乎所有操作:

【You will be able to perform almost all of your operations with the following 3 patterns:】

  1. 顺序执行: 函数将按严格的顺序依次执行,这种方式最类似于 for 循环。
// operations defined elsewhere and ready to execute
const  = [
  { : function1, : args1 },
  { : function2, : args2 },
  { : function3, : args3 },
];

function (, ) {
  // executes function
  const { ,  } = ;
  (, );
}

function () {
  if (!) {
    .(0); // finished
  }

  (, function () {
    // continue AFTER callback
    (.());
  });
}

(.());
  1. 系列限制: 函数将按严格的顺序执行,但执行次数有限。当你需要处理一个很大的列表,但要限制成功处理的项目数量时,这非常有用。
let  = 0;

function () {
  .(`dispatched ${} emails`);
  .('finished');
}

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

function () {
  getListOfTenMillionGreatEmails(function (, ) {
    if () {
      throw ;
    }

    function () {
      if (! ||  >= 1000000) {
        return ();
      }

      (, function () {
        if (!) {
           += 1;
        }

        (.pop());
      });
    }

    (.pop());
  });
}

();
  1. 完全并行: 当顺序不是问题时,例如向 1,000,000 个电子邮件收件人发送邮件列表。
let  = 0;
let  = 0;
const  = [];
const  = [
  { : 'Bart', : 'bart@tld' },
  { : 'Marge', : 'marge@tld' },
  { : 'Homer', : 'homer@tld' },
  { : 'Lisa', : 'lisa@tld' },
  { : 'Maggie', : 'maggie@tld' },
];

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

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

.(function () {
  (, function () {
    if (!) {
       += 1;
    } else {
      .(.);
    }
     += 1;

    if ( === .) {
      ({
        ,
        ,
        ,
      });
    }
  });
});

每种都有其自身的使用场景、优点和可以进一步实验和阅读的相关问题。最重要的是,记得将你的操作模块化并使用回调!如果有任何疑问,就将一切都当作中间件来处理!

【Each has its own use cases, benefits, and issues you can experiment and read about in more detail. Most importantly, remember to modularize your operations and use callbacks! If you feel any doubt, treat everything as if it were middleware!】

阅读时间
11 min
目录
  1. 状态管理
  2. 控制流