欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

避免回调地狱的解决方案 async/await:用同步的方式去写异步代码

程序员文章站 2024-01-28 09:21:28
...


前言

这篇文章主要给大家分享一下,自己关于异步编程方面的一些见解,和实际开发项目过程中使用到的一些技术以及技巧


一、引入异步编程

对异步编程编程不太熟悉的小伙伴,可以通过下面这些简单的例子,先建立对异步编程的基本认知,首先作为一名前端程序员,联调接口是我们最熟悉不过的技能。下面这种代码,这样的回调函数,大家肯定是再熟悉不过。

ajax(url, (res) => {
  console.log(res);
})

但是大家是否有想过,如果某一个业务场景比较复杂,需要一个类似链式调用的操作,那么,如果没猜错我相信大多数人的代码,可能会变成这样。

ajax(url, (res) => {
  console.log(res);
  // ...处理代码
  ajax(url2, (res2) => {
    console.log(res2);
    // ...处理代码
    ajax(url3, (res3) => {
      console.log(res3);
      // ...处理代码
      // 若干个回调
    })
  })
})

如果有若干个,当然我相信,现实业务中这种情况肯定是不会出现的,我们只是为了说明这种写法这理论是不严谨,容易进入 回调地狱
回调地狱:
上面那个就是典型的回调地狱,那么肯定有人会问回调地狱会有哪些缺点呢,其实回调地狱不光只是缺点,也还是有些优点的,下面是个人的一些理解。

  • 优点:解决了 同步阻塞 的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行)
  • 缺点:回调地狱;不能用 try catch 捕获错误;不能 return

二、常见处理异步编程的几种方式

1.Generator函数

  • 介绍:ES6 新引入了 Generator 函数(生成器函数),可以通过 yield 关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供解决方案。最大的特点就是 可以控制函数的执行。
  • Generator和函数不同的是,generator由function *定义(注意多出的星号),并且,除了return语句,还可以用yield返回多次
    利用 yield返回多次这个特性,我们就可以实现有同步的方式写出异步的代码
//菲波那切数列,简单介绍一下它,n=(n-1) + (n-2)
function* fib(max) {
    var
        t,
        a = 0,
        b = 1,
        n = 0;
    while (n < max) {
        yield a;
        [a, b] = [b, a + b];
        n ++;
    }
    return;
}

可能有人对[a, b] = [b, a+b],不太理解。其实[a, b] = [b, a+b]起到了var t = a + b;a = b;b = t;的作用,或者说是这段代码的精简版(省略了中间变量t的申请),实际上,它的运算顺序是从右到左的,先计算b和a+b,然后将b的值赋予a,再将a+b的值赋予b,以此来达到预期的计算结果。

next()方法会执行generator的代码,然后,每次遇到yield x;就返回一个对象{value: x, done: true/false},然后“暂停”。返回的value就是yield的返回值,done表示这个generator是否已经执行结束了。如果done为true,则value就是return的返回值。
当执行到done为true时,这个generator对象就已经全部执行完毕,不要再继续调用next()了。最终我们可以将复杂的回调函数改成这样

try {
    r1 = yield ajax('http://url-1', data1);
    r2 = yield ajax('http://url-2', data2);
    r3 = yield ajax('http://url-3', data3);
    success(r3);
}
catch (err) {
    handle(err);
}

2.Promise函数

这个函数我相信大家都很熟悉,在这里就不过多介绍

3.async/await

async 是一个通过 异步执行并隐式返回 Promise 作为结果的函数。

async function async1() {
  return '测试';
}
console.log(async1());

执行这段代码,可以看到调用 async 声明的 async1 函数返回了一个 Promise 对象,状态是 resolved,返回结果如下所示:Promise {: “测试”}。和 Promise 的链式调用 then 中处理返回值一样。这就完美的解释了,MDN对async的定义,即 异步执行并隐式返回 Promise 作为结果的函数。


await 需要跟 async 搭配使用,结合下面这段代码来看看 await 到底是什么
async function foo() {
  console.log(1)
  let a = await 100
  console.log(a)
  console.log(2)
}
console.log(0)
foo()
console.log(3)

分析:

  1. 首先,执行 console.log(0) 这个语句,打印出来 0。
  2. 紧接着就是执行 foo 函数,由于 foo 函数是被 async 标记过的,所以当进入该函数的时候,JS 引擎会保存当前的调用栈等信息,然后执行 foo 函数中的 console.log(1) 语句,并打印出 1。
  3. 当执行到 await 100 时,会默认创建一个 Promise 对象,进入微任务队列, 然后 JS 引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将 promise_ 对象返回给父协程。
  4. 主线程的控制权已经交给父协程了,这时候父协程要做的一件事是调用 promise_.then 来监控 promise 状态的改变。
  5. 接下来继续执行父协程的流程,执行 console.log(3),并打印出来 3。
  6. 随后父协程将执行结束,在结束之前,会进入微任务的检查点,然后执行微任务队列,微任务队列中有 resolve(100) 的任务等待执行,执行到这里的时候,会触发 promise_.then 中的回调函数
    该回调函数被**以后,会将主线程的控制权交给 foo 函数的协程,并同时将 value 值传给该协程。foo 协程**之后,会把刚才的 value 值赋给了变量 a,然后 foo 协程继续执行后续语句

总结

  • 早期的异步回调函数虽然解决了同步阻塞的问题,但是容易写出回调地狱。
  • Generator 生成器最大的特点是可以控制函数的执行,也能勉强实现同步方式实现异步事件。
  • 使用 Promise 能很好地解决回调地狱的问题,但是这种方式充满了 Promise 的 then() 方法,如果处理流程比较复杂的话,那么整段代码将充斥着 then,语义化不明显,代码不能很好地表示执行流程。后期移交维护的成本比较大。
  • async/await 可以算是异步编程的终极解决方案,它通过同步的方式写异步代码,可以把 await 看作是让出线程的标志,先去执行 async 函数外部的代码,等调用栈为空再回来调用 await 后面的代码。