美文网首页全栈工程师修炼指南
打开潘多拉盒子: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异步编程

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