美文网首页全栈工程师修炼指南
打开潘多拉盒子:JavaScript异步编程

打开潘多拉盒子:JavaScript异步编程

作者: 码农架构 | 来源:发表于2020-11-22 10:11 被阅读0次

用 Promise 优化嵌套回调

setTimeout(
  () => {
    console.log(1);
    setTimeout(
      () => {
        console.log(2);
        setTimeout(
          () => {
            console.log(3);
          },
          1000
        );
      },
      1000
    );
  },
  1000
);

们用了 3 次 setTimeout,每次都接受两个参数,第一个参数是一个函数,用以打印当前跑的距离,以及递归调用奔跑逻辑,第二个参数用于模拟奔跑耗时 1000 毫秒。这个问题其实代表了实际编程中一类很常见的 JavaScript 异步编程问题。

抽取相同逻辑:

var run = (steps, callback) => {
  setTimeout(
    () => {
      console.log(steps);
      callback();
    },
    1000
  );
};

run(1, () => {
  run(2, () => {
    run(3, () => {});
  });
});

Promise,就如同字面意思“承诺”一样,定义在当前,但行为发生于未来。它的构造方法中接受一个函数,并且这个函数接受 resolve 和 reject 两个参数,前者在未来的执行成功时会被调用,后者在未来的执行失败时会被调用。

这个 Promise 对象,并不是在程序一开始就初始化的,而是在未来的某一时刻,前一步操作完成之后才会得到执行,这一点非常关键,并且这是一种通过给原始代码添加函数包装的方式实现了这里的“定义、传递、但不执行”的要求。

var run = steps => 
  () => 
    new Promise((resolve, reject) => {
      setTimeout(
        () => {
          console.log(steps);
          resolve(); // 一秒后的未来执行成功,需要调用
        },
        1000
      );
    });

Promise.resolve()
  .then(run(1))
  .then(run(2))
  .then(run(3));

async/await:

  • async 用于标记当前的函数为异步函数;
  • await 用于表示它的后面要返回一个 Promise 对象,在这个 Promise 对象得到异步结果以后,再继续往下执行。
var run = async steps => {
  await wait(1000);
  console.log(steps);
}

await run(1);
await run(2);
await run(3);

纵观这个小狗奔跑的问题,我们一步一步把晦涩难懂的嵌套回调代码,优化成了易读、易理解的“假同步”代码。聪明的程序员总在努力地创造各种工具,去改善代码异步调用的表达能力,但是越是深入,就越能发现,最自然的表达,似乎来自于纯粹的同步代码。

用生成器来实现协程

协程,Coroutine,简单说就是一种通用的协作式多任务的子程序,它通过任务执行的挂起与恢复,来实现任务之间的切换。

这里提到的“协作式”,是一种多任务处理的模式,它和“抢占式”相对。如果是协作式,每个任务处理的逻辑必须主动放弃执行权(挂起),将继续执行的资源让出来给别的任务,直到重新获得继续执行的机会(恢复);而抢占式则完全将任务调度交由第三方,比如操作系统,它可以直接剥夺当前执行任务的资源,分配给其它任务。

JavaScript 的协程是通过生成器来实现的,执行的主流程在生成器中可以以 yield 为界,进行协作式的挂起和恢复操作,从而在外部函数和生成器内部逻辑之间跳转,而 JavaScript 引擎会负责管理上下文的切换。

JavaScript 和迭代有关的两个协议:

  • 第一个是可迭代协议,它允许定义对象自己的迭代行为,比如哪些属性方法是可以被 for 循环遍历到的;
  • 第二个是迭代器协议,它定义了一种标准的方法来依次产生序列的下一个值(next() 方法),如果序列是有限长的,并且在所有的值都产生后,将有一个默认的返回值。

在 JavaScript 中,生成器对象是由生成器函数 function* 返回,且符合“可迭代协议”和“迭代器协议”两者。function* 和 yield 关键字通常一起使用,yield 用来在生成器的 next() 方法执行时,标识生成器执行中断的位置,并将 yield 右侧表达式的值返回。见下面这个简单的例子:

function* IdGenerator() {
  let index = 1;
  while (true)
    yield index++;
}

var idGenerator = IdGenerator();

console.log(idGenerator.next());
console.log(idGenerator.next());

输出:

{value: 1, done: false}
{value: 2, done: false}

生成器可不是只能往外返回,还能往里传值。具体说,yield 右侧的表达式会返回,但是在调用 next() 方法时,入参会被替代掉 yield 及右侧的表达式而参与代码运算。我们将上面的例子小小地改动一下:

function* IdGenerator() {
  let index = 1, factor = 1;
  while (true) {
    factor = yield index; // 位置①
    index = yield factor * index; // 位置②
  }
}

调用:

var calculate = (idGenerator) => {
  console.log(idGenerator.next());
  console.log(idGenerator.next(1));
  console.log(idGenerator.next(2));
  console.log(idGenerator.next(3));
};

calculate(IdGenerator());
image.png

异步错误处理

Promise 的异常处理

还记得上面介绍的 Promise 吗?它除了支持 resolve 回调以外,还支持 reject 回调,前者用于表示异步调用顺利结束,而后者则表示有异常发生,中断调用链并将异常抛出:

var exe = (flag) =>
  () => new Promise((resolve, reject) => {
    console.log(flag);
    setTimeout(() => { flag ? resolve("yes") : reject("no"); }, 1000);
  });

上面的代码中,flag 参数用来控制流程是顺利执行还是发生错误。在错误发生的时候,no 字符串会被传递给 reject 函数,进一步传递给调用链:

Promise.resolve()
  .then(exe(false))
  .then(exe(true));

你看,上面的调用链,在执行的时候,第二行就传入了参数 false,它就已经失败了,异常抛出了,因此第三行的 exe 实际没有得到执行,你会看到这样的执行结果:

false
Uncaught (in promise) no

但是,有时候我们需要捕获错误,而继续执行后面的逻辑,该怎样做?这种情况下我们就要在调用链中使用 catch 了:

Promise.resolve()
  .then(exe(false))
  .catch((info) => { console.log(info); })
  .then(exe(true));

这种方式下,异常信息被捕获并打印,而调用链的下一步,也就是第四行的 exe(true) 可以继续被执行。我们将看到这样的输出:

var run = async () => {
  try {
    await exe(false)();
    await exe(true)();
  } catch (e) {
    console.log(e);
  }
}

run();

async/await 下的异常处理

利用 async/await 的语法糖,我们可以像处理同步代码的异常一样,来处理异步代码:
简单说明一下 ,定义一个异步方法 run,由于 await 后面需要直接跟 Promise 对象,因此我们通过额外的一个方法调用符号 () 把原有的 exe 方法内部的 Thunk 包装拆掉,即执行 exe(false)() 或 exe(true)() 返回的就是 Promise 对象。在 try 块之后,我们使用 catch 来捕捉。运行代码,我们得到了这样的输出:

false
no

相关文章

  • 打开潘多拉盒子:JavaScript异步编程

    用 Promise 优化嵌套回调 们用了 3 次 setTimeout,每次都接受两个参数,第一个参数是一个函数,...

  • JavaScript异步编程好文摘要

    JavaScript之异步编程简述JavaScript异步编程

  • part1整理

    函数式编程:JavaScript函数式编程指南 异步编程:异步编程 Promise源码 JavaScript基础知...

  • 一篇看完JS异步编程的进阶史

    一、Javascript实现异步编程的过程以及原理 1、为什么要用Javascript异步编程 众所周知,Java...

  • 打开潘多拉盒子

    从昨晚上到今天我一直处于平静与祥和之中,仿佛全身被一层淡淡的暖包围着,非常享受这种状态,甚至有点儿窃喜自己...

  • ES6 之 Promise

    Promise是JavaScript异步编程中的重要概念,异步抽象处理对象,是目前比较流行Javascript异步...

  • JavaScript(ES6) - Async

    异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的,如果没有异步编程,根本...

  • 观影《45周年》,说说婚姻

    潘多拉的盒子一打开,坏事自然来 如同打开潘多拉的盒子,45年后,所有有关邪恶的——贪婪、虚伪、诽谤、争竞、嫉妒、谎...

  • 异步编程控制方法

    javascript 具有的一个特性就是异步编程。异步编程具有的优势本文不做细说,本文主要是总结如何异步编程中出现...

  • Javascript------异步编程的4种方法

    Javascript异步编程的4种方法

网友评论

    本文标题:打开潘多拉盒子:JavaScript异步编程

    本文链接:https://www.haomeiwen.com/subject/oequiktx.html