玩转异步编程

1. 异步编程基础

1.1 同步与异步的区别

  • 同步:
    是指程序中的任务必须要按照顺序执行,前一个任务完成之后,后一个任务才能开始。在同步操作中,调用者是必须等待操作完成并返回结果才能继续执行接下来的代码。这就表明了当存在某个操作需要耗费大量的时间比如说(I/O操作,网络请求等任务),那么整个程序就会暂停等待这些任务处理完成。(这也正是出现异步的原因)
  • 异步:
    是指它允许程序在等待耗时操作的同时继续执行其他任务。当发生一个异步操作的时候,它不会阻塞调用者的执行流程,而是继续执行后续代码。

1.2 事件循环与回调

关于Javascript的事件循环机制的详细介绍,在(玩转事件循环机制)这一篇武功秘籍里面有详细的介绍。

1.3 回调地狱问题

当然在异步操作的增多的时候,随着回调函数的嵌套层数增多时,就会出现回调地狱的问题,它会导致性能问题以及代码结构变得难以阅读和维护。并且高度耦合的回调函数会使得代码难以被重构或重用于其他场景。

下面举个例子玩一下:

	setTimeout(() => {  
	    console.log('我爱吃')  
	    setTimeout(() => {  
	        console.log('肉夹馍')  
	        setTimeout(() => {  
	            console.log('葱油大饼')  
	        }, 1000)  
	    }, 2000)  
	}, 3000)

2. Promise

2.1 Promise的概念

既然遇到了回调地狱这个问题,我们就要去思考怎么去解决呢?

接下来就让我来介绍一下解决方案—PromisePromise是一个对象,用于异步计算。它代表了一个最终会完成(或失败)的异步操作,并且会返回一个值。Promise的设计目的是为了解决回调地狱问题,使得异步代码的结构更加线性,易于理解和维护。
Promise有三种基本状态:

  1. Pending(等待状态):这是Promise的初始状态,表示异步操作尚未完成。在此阶段,Promise既没有被成功解析(fulfilled)也没有被失败拒绝(rejected)。

  2. Fulfilled(完成状态):当异步操作成功完成时,Promise会从pending状态转变到fulfilled状态。在这一状态下,Promise会携带一个值,这个值可以通过.then()方法访问。

  3. Rejected(拒绝状态):如果异步操作失败,Promise会从pending状态转变到rejected状态。在这一状态下, Promise会携带一个错误信息,这个错误信息可以通过.catch()方法捕获和处理。

2.2 Promise的优点

Promise提供了几个方法来处理异步操作的结果:

  • .then():当Promise成功完成时调用的回调函数。可以接受两个参数,第一个参数是Promise成功时的回调,第二个参数(可选)是Promise失败时的回调。如果.then()中的函数返回一个新的Promise,那么链中的下一个.then()将等待这个新的Promise完成。
  • .catch():专门用于处理Promise失败时的回调函数。它会捕获前面任何.then()中抛出的错误,或者Promisereject时的错误。
  • .finally():无论Promise最终是fulfilled还是rejected,都会执行的回调函数。它主要用于清理资源,如关闭文件句柄或释放内存。

2.3 Promise的创建与使用

  • 创建Promise实例

    const myPromise = new Promise((resolve, reject) => {
      // 模拟异步操作
      setTimeout(() => {
        try {
          const result = '异步操作成功';
          resolve(result); // 成功时调用resolve
        } catch (error) {
          reject(error); // 失败时调用reject
        }
      }, 2000);
    });
    

    在这个例子中,我们创建了一个Promise实例myPromise,它模拟了一个耗时2秒钟的异步操作。
    如果异步操作成功,resolve函数会被调用并传递结果;如果操作失败或抛出错误,reject函数会被调用并传递错误。

    • Promise对象构造函数是一个立即执行函数,但是它的resolvereject方法是异步执行的
    • 当我们执行到resolve()方法的时候,我们会把这个方法放到任务队列中,等待主线程的执行栈清空后,再从任务队列中取出这个方法来执行。
    • 当我们执行到reject()方法的时候,它会直接把错误信息抛出,然后把Promise对象的状态变为rejected
    • Promise对象的状态一旦改变,就不会再变。
  • 使用.then()和.catch()

    • .then()方法用于处理Promise成功的情况,可以接受两个参数,第一个是成功时的回调,第二个是可选的失败时的回调。如果.then()中的函数返回一个Promise,则下一个.then()会等待这个Promise完成。
    • .catch()方法专门用于处理Promise失败的情况,它捕获前面任何.then()中抛出的错误,或者Promisereject时的错误。
       // 使用.then()和.catch()处理结果  
       myPromise
       .then((result) => {
         console.log('成功:', result);
        })
       .catch((error) => {
         console.error('失败:', error);
       });
    
    
  • 使用Promise.all与Promise.race方法:

    • Promise.allPromise.race是处理多个Promise实例的两种常用方法,它们分别用于不同的场景。

    • Promise.all
      Promise.all接受一个Promise数组作为参数,当所有Promise都完成(fulfilled)时,Promise.all返回的Promise才会完成,否则如果任何一个Promise失败(rejected),Promise.all返回的Promise也会立即失败。

       const promise1 = Promise.resolve('第一个Promise');
       const promise2 = Promise.resolve('第二个Promise');
       const promise3 = Promise.resolve('第三个Promise');
      
       Promise.all([promise1, promise2, promise3])
         .then((results) => {
           console.log('所有Promise完成:', results);
         })
         .catch((error) => {
           console.error('至少一个Promise失败:', error);
         });
      
      
    • Promise.race
      Promise.race同样接受一个Promise数组作为参数,但是它返回的Promise会在数组中的任何一个Promise完成或失败时立即完成或失败,这取决于最先完成或失败的那个Promise
      意思就是它会获取最先成功的Promise,不在乎结果怎么样,只要有一个成功就行,并且只会返回最先成功的那个Promise

      const promiseA = new Promise((resolve) => setTimeout(resolve, 500, '慢速Promise'));
      const promiseB = Promise.resolve('快速Promise');
      
      Promise.race([promiseA, promiseB])
          .then((value) => {
              console.log('最先完成的Promise值:', value);
          })
          .catch((reason) => {
              console.error('最先失败的Promise原因:', reason);
          });
      

      在上面的例子中,Promise.race将返回promiseBPromise,因为它会立即完成,而promiseA需要等待500毫秒。这使得Promise.race非常适合用于超时处理或在多个可能完成的异步操作中选择最快的结果。

  • Promise.all就适用于当一个操作可能需要多个接口的返回数据的时候。

  • Promise.race就适用于有好几个服务器提供的同样的服务,就可以使用Promise.race,哪个接口更快就用哪个。

3. Async/Await

3.1 Async/Await简介以及工作原理

讲到async函数

  1. 当我们用async关键字定义一个函数时,就表明这个函数为异步函数,被定义的这个函数的返回值是一个AsyncFunction对象的异步函数
  2. async函数执行时,它会返回一个Promise对象,这个Promise对象会等待async函数执行完成,就算你没有明确的返回一个Promise,函数内部也会自动创建一个Promise对象,如果async函数中没有任何return语句,它将返回一个解析为undefinedPromise
  3. 如果async函数包含一个return语句,且返回的不是Promise,则该值会被封装进一个解析的Promise中。
  4. 如果return语句返回一个Promise,则async函数的返回Promise将等待这个内部Promise的结果。

讲到await的话

首先它只能定义在async函数中。

  1. await是用来等待Promise对象返回的结果
    await会暂停async函数的执行,等待Promise对象返回结果,然后恢复async函数的执行。
  2. 提高代码可读性,让代码看起来像是同步的。
  3. 如果await不放在async函数中,我们无法控制何时暂停和恢复执行流,这样会导致程序逻辑混乱。
    • 因为await会阻塞后面的代码,只有当Promise对象的状态变为resolved时,才会继续执行下面的代码
    • 同时await会返回Promise对象的状态值,如果Promise对象的状态为rejected,await会抛出异常
    • 其次,await后面只能跟Promise对象,不能跟普通值,返回 Promise 对象的处理结果。
    • 如果等待的不是 Promise 对象,则返回该值本身。
    • 所以当await操作符后面的表达式是一个Promise的时候,它的返回值,实际上就是Promise的回调函数resolve的参数。

3.2 Async/Await的使用

关于使用的话,直接来分析几道题即可

1.例一:

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2')
}
console.log('script start')
setTimeout(function () {
  console.log('setTimeout')
}, 0)
async1();
new Promise(function (resolve) {
  console.log('promise1')
  resolve();
}).then(function () {
  console.log('promise2')
})
console.log('script end')

来吧,少侠告诉我你的答案
正确的答案:

 script start
 async1 start
 async2
 promise1
 script end
 async1 end
 promise2
 setTimeout

所以是为什么呢?
我刚开始一直对这个Promise对象函数和async1以及async2中的打印是很模糊的。

  • 现在我们来具体分析一下:

    1. 首先我们打印的肯定是script start

    2. 遇到setTimeout函数,会将setTimeout放到下一次事件循环中

    3. 执行async1函数,遇到同步代码,打印async1 start

    4. 遇到await,会暂停async1函数的执行,所以async1 end不会打印,并等待await后面的Promise对象返回结果

    5. async2函数放入到微任务队列中

    6. 执行Promise对象函数,打印promise1

    7. 遇到Promise对象,因为Promise的构造函数是立即执行的,所以我们会打印promise1

    8. 遇到Promise的回调函数,把then()放入到微任务队列之中

    9. 继续执行同步代码script end

    10. 接着js引擎会检查当前事件循环中是否有微任务,发现有微任务

    11. 执行微任务,打印async2 async1 end

    12. 继续执行微任务打印promise2

    13. 执行下一次事件循环,打印setTimeout

  • 所以输出结果为

      script start
      async1 start
      promise1
      script end
      async2
      async1 end
      promise2
      setTimeout
    

    ok,少侠,我犯下了错误,当我发现打印结果是

    script start
    async1 start
    async2
    promise1
    script end
    async1 end
    promise2
    setTimeout
    
    • 我才发现我对asyncawait理解还是不够深入。

    • 后面我发现其实在执行async2的时候其实跟我的分析是有出入的,

      其实await async2()它不会被放在任务队列之中的。(我刚开始看到async1 endpromise1后面打印我还以为await async2();console.log('async1 end');被放到任务队列之中去了)

    • await关键字的作用是暂停当前的异步函数(在本例中是 async1)的执行,直到 async2 函数返回的 Promise 变为 resolvedrejected

    • 之后继续执行async1 函数的剩余部分。
      为什么promise1会在async1 end之前打印,是因为在等待async2()返回的 Promise 变为 resolved 之前,async1 函数的执行被暂停了,

    • 所以会先执行 Promise 的构造函数中的同步代码,然后才继续执行 async1 函数的剩余部分。

    所以快让我们重新再来分析一波吧:

    1. 打印 script start:这是最开始的同步代码,它会被立即执行。

    2. setTimeout 函数:确实,setTimeout 被放置在宏任务队列中,将在当前执行栈清空后,在下一轮事件循环中执行。

    3. 执行 async1 函数async1 函数开始执行,首先打印 async1 start

    4. await async2():遇到 await 关键字时,async1 的执行将暂停,直到async2函数返回的 Promise 解析完成。但是,async2 函数的执行是立即的,它打印 async2,然后返回一个默认的 fulfilled Promise(因为 async2 没有异步操作)。

      重要说明:async2 函数并没有被放入微任务队列中。async2 的执行是同步的,直到它返回一个 Promise,这个 Promise 几乎立即完成,因为 async2 没有异步操作。await 关键字的作用是等待这个 Promise 完成,然后恢复async1函数的执行。

    5. Promise 对象函数执行new Promise(function (resolve) { ... }) 创建了一个新的 PromisePromise 构造函数内的代码是同步执行的,所以 promise1 被打印出来。

    6. Promise 的回调函数.then(function () { ... }) 注册了一个回调函数,这个回调函数会被放入微任务队列中,等待当前执行栈清空后执行。

    7. 继续执行同步代码:在 async1Promise 构造函数执行之后,script end 被打印出来,这是同步代码,所以它在 setTimeout 和微任务之前执行。

    8. 微任务执行:一旦当前执行栈清空,事件循环会检查微任务队列

      首先,async1 函数会继续执行,打印 async1 end,这是因为 await async2() 等待的 Promise 已经完成。

      然后,.then 回调函数被执行,打印 promise2

    所以的所以,因此,async1async2 不会直接进入微任务队列。它们的行为更像是包含了可能的异步操作的函数,这些异步操作通过 Promiseawait 关键字来实现。
    只有当 Promise 的回调函数(如 .then().catch())才被加入到微任务队列中等待执行。

    2.例二:又是一道经典的题目

    // 第一块代码
    Promise.resolve().then(() => {
        console.log(0);
        return Promise.resolve(4);
    }).then((res) => {
        console.log(res)
    })
    
    // 第二块代码
    Promise.resolve().then(() => {
        console.log(1);
    }).then(() => {
        console.log(2);
    }).then(() => {
        console.log(3);
    }).then(() => {
        console.log(5);
    }).then(() => {
        console.log(6);
    })
    

    好好好,这个题居然最后输出的结果是0 1 2 3 4 5 6

    好吧,那我们只能狠狠地来分析分析了

    • 这里面涉及了Promise对象的链式调用,这个过程是十分复杂的

    • 首先在这里,我们要知道每一次Promise对象的静态resolve()方法和Promise实例的then()方法都会返回一个新的Promise对象实例,也就是return new Promise()也正是因为这个原来才能实现Promise的链式调用

    • Promise.resolve(4)本身会产生一个新的promise实例,接着它作为值传给了then方法的成功回调(如果失败的话也会执行失败的回调)。

    • 由于值是Promise实例,所以会调用值的then方法, 所以又产生了一层新的Promise实例,这也就导致4在3后。

    • 意思就是当产生Promise实例的时候,会将这个事件放到微任务队列,因为它会等待返回的解决(fulfill)时的回调或者拒绝(reject)时的回调。

    所以上面第一块的代码等价于

    Promise.resolve().then(() => {
      console.log(0);
      return 4;
    })
      .then()
      .then()
      .then((res) => {
        console.log(res)
      })
    
    • 如果上面的这个讲解还是不清楚的话,请你结合下面我讲的东西,帮助理解。这里想介绍一下Promise的回调的时候,会发现传入的参数和调用的方式的不同时,它们是有区别的

    3.不同返回类型的区别

    虽然下面的代码1,2,3最后输出的结果都是4,但是它们在调用上面是有区别的

    • 代码1:

      new Promise(resolve => {
       resolve(Promise.resolve(4));//resolve了一个Promise
      })
       .then((res) => {
         console.log(res)
       })
      

      在这段代码中,我们创建了一个新的Promise,并在resolve函数中立即解析了一个包含数字4的已解析Promise
      当外部Promise解析时,它会解析为内部Promise的解析值,即4。因此,.then()回调将接收到数字4并将其打印出来
      resolve()方法中放入的时一个Promise对象。

      • 首先第一次是里面的Promise对象调用了静态方法resolve创建了一个还在pendingPromise实例但是传递的值是可以立即拿到的接着Promise状态会变成fulfilled
      • 然后外层要传递这个值的内容,会重新创建一个Promise实例并调用then()方法
      • 这样就会实现两次加入微任务队列的操作
    • 代码2:

      Promise.resolve().then(() => {
       return Promise.resolve(4);//return了一个Promise
      })
       .then((res) => {
         console.log(res)
       })
      
      • 在这段代码中,我们从Promise.resolve()开始,这创建了一个已解析的Promise

      • 然后我们在.then()回调中返回另一个已解析的Promise,其解析值为4

      • 当这个内部Promise解析时,其解析值4将传递给下一个.then()回调,最终打印出来。

      • 这里的return了一个Promise 和 上面的方法是类似的解析过程

    • 代码3:

      Promise.resolve().then(() => {
       return 4;//return了一个Number类型的4
      })
       .then((res) => {
         console.log(res)
       })
      
      • 在这段代码中,我们同样从Promise.resolve()开始,创建了一个已解析的Promise

      • 但是,这次在.then()回调中,我们直接返回了一个数值4

      • 由于返回的不是一个Promise.then()会直接将这个值传递给链中的下一个.then(),最终也将打印出4

    4.例三:让我幡然醒悟的一道题目

    • 原以为下面最后的输出结果是0 1 2 3 5 6 4

    • 结果结果为0 1 2 3 4 5 6

    • 发现其实不管嵌套多少层的Promise,只要它不涉及往下面传递内容时,是不会产生微任务的,所以这里的 return Promise.resolve(Promise.resolve(Promise.resolve(4))) 等价于 return Promise.resolve(4)。终于发现能够理解上面的内容了。

    • 因为在最外层的resolve下面Promise状态变为fulfilled的过程并不需要等待。

      Promise.resolve().then(() => {
        console.log(0);
        return Promise.resolve(Promise.resolve(Promise.resolve(4)))
      })
        .then(res => {
          console.log(res);
        })
      

3.3 错误处理

对于错误处理肯定就是使用try…catch语句了,用法也十分简单。

async function myAsyncFunction() {
  try {
    const result = await new Promise((resolve, reject) => setTimeout(() => reject(new Error('失败')), 1000));
  } catch (error) {
    console.error(error.message); // 输出:失败
  }
}

4. 最佳实践与陷阱

  1. 使用Promise链: 使用thencatch方法来构建Promise链,这有助于保持代码的顺序性和清晰度。尽量避免嵌套的回调地狱。
  2. 错误处理: 总是在Promise链中包含catch块来处理可能发生的错误。不要忽略错误,确保每个异步操作都有适当的错误处理逻辑。
  3. 使用async/await: 多使用async/await语法,因为它使异步代码看起来更像同步代码,提高可读性。但是要注意,async/await只能在async函数内使用。
  4. 未处理的Promise拒绝: 忽略Promisecatch可能导致未处理的Promise拒绝。确保每个Promise都有适当的错误处理。
  5. 资源泄露: 忘记关闭资源,如文件描述符、数据库连接,会导致资源泄露,最终可能耗尽系统资源。
  6. 死锁和竞争条件: 在复杂的异步逻辑中,不正确的同步点可能导致死锁或竞争条件。使用原子操作和互斥锁来保护共享资源。
  7. 回调地狱: 避免过多的嵌套回调,因为这样会使会使代码难以理解和维护。使用Promise链或async/await来简化代码结构。
  • 16
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值