JavaScript异步编程

292 阅读9分钟

JavaScript中的异步机制可以分为以下几种:

  1. 回调函数 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
  2. Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
  3. generator 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
  4. async 函数 的方式,async函数是 generatorpromise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。

回调函数

什么是回调函数?

JavaScript中的回调函数是指在某个函数执行完毕后,调用另一个函数作为参数传递进去,并在后续的程序中被调用执行的函数。回调函数在JavaScript中广泛应用于事件处理、异步编程和Ajax等场景。

  • 就是把函数 A 当作参数传递到 函数 B 中
  • 在函数 B 中以行参的方式进行调用
  function a(callback) {
    callback()
  }
	  function b() {
	    console.log('我是函数 b')
	  }
  a(b)

为什么需要回调函数?

  • 当我们执行一个异步的行为的时候,我们需要在一个异步行为执行完毕之后做一些事情
  • 那么,我们是没有办法提前预知这个异步行为是什么时候完成的
  • 我们就只能以回调函数的形式来进行
  • 就比如我们刚刚封装过的那个 ajax 函数里面的 success
  • 我们并不知道 ajax 请求什么时候完成,所以就要以回调函数的形式来进行

回调地狱

  • 当一个回调函数嵌套一个回调函数的时候 就会出现一个嵌套结构

  • 当嵌套的多了就会出现回调地狱的情况

  • 比如我们发送三个 ajax 请求

    1. 第一个正常发送
    2. 第二个请求需要第一个请求的结果中的某一个值作为参数
    3. 第三个请求需要第二个请求的结果中的某一个值作为参数
  ajax({
    url: '我是第一个请求',
    success (res) {
      // 现在发送第二个请求
      ajax({
        url: '我是第二个请求'data: { a: res.a, b: res.b },
        success (res2) {
          // 进行第三个请求
          ajax({
            url: '我是第三个请求',
            data: { a: res2.a, b: res2.b },
    				success (res3) { 
              console.log(res3) 
            }
          })
        }
      })
    }
  })

回调地狱,其实就是回调函数嵌套过多导致的

Promise

Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise的实例有三个状态:

    1. Pending(进行中)

    2. Resolved(已完成)

    3. Rejected(已拒绝)

当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。

Promise的实例有两个过程:

    1. pending -> fulfilled : Resolved(已完成)
    2. pending -> rejected:Rejected(已拒绝)

注意:一旦从进行状态变成为其他状态就永远不能更改状态了。

Promise对象的基本用法

创建Promise实例对象

使用new Promise()
const promise = new Promise((resolve,reject)=>{
	resolve();  //成功
	reject(); // 失败/异常
}).then(req=>{
	// fullfiled状态执行此部分
}).catch(err=>{
	//rejected状态执行此部分
}).finally(()=>{
	// 只要从pending状态改变就会执行此部分
})

一般情况下都会使用new Promise()来创建promise对象,但是也可以使用Promise.resolve()Promise.reject()这两个方法:

Promise.resolve()

Promise.resolve(value)的返回值也是一个Promise实例对象,可以对返回值进行.then调用,代码如下:

Promise.resolve('success').then(req=>{
	// 成功后执行的代码
})

resolve('success')代码中,会让Promise实例对象进入resolve状态,并将参数"success"传递给后面的then所指定的fullfiled函数.

**then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。
**then方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

Promise.reject()

Promise.resolve(value)的返回值也是一个Promise实例对象,可以对返回值进行.catch调用,代码如下:

Promise.reject(new Error('我报错了!'))

Promise.all([...个 promise 实例对象])

  1. ...个 promise 实例对象 都走resolve,则 最终走 thenthen 的回调函数的 res 是数组,数组的每个元素是 ...个 promise 实例对象resolve 的值
  1. ...个 promise 实例对象,只要 有一个 走reject,则 立刻走 catch,(不走then 了),

catch的回调函数的 err 的值是reject的值。

Promise.race([...个 promise 实例对象])

race数组中 第一个 Promise 实例对象,改变了 pending 的状态, 则立刻停止数组中 剩下的Promise 实例对象 的实行。

情况1:若 Promise 实例对象 执行了 resolve,则 执行 then 的实例方法

情况2:若 Promise 实例对象 执行了 reject,则 执行 catch 的实例方法

Promise.allSellted([...个 promise 实例对象])

需求:同时请求 N 个 ajax ,当 所有的 ajax 都获取到了 数据,再进行下一步...

若用 Promise.all 会有 一个"痛点":一旦有 一个 ajax 报错,则所有的 ajax 数据都拿不到...

那么,上述问题的 解决办法:

Promise.allSettled([oPSuccess1, oPSuccess2, oPSuccess3, oPError1])

作用:不管 ...promise 实例对象 走不走 reject,最终当 所有的 Promise 实例对象 都 由 pending 改变了状态

则走 .then(回调函数),then 中 回调函数的参数 是 数组,数组的数据形式 为下述类型:

  [
    {status: 'fulfilled', value: '第1S 的 resolve 数据'},
    {status: 'fulfilled', value: '第2S 的 resolve 数据'},
    {status: 'fulfilled', value: '第3S 的 resolve 数据'},
    {status: 'rejected', reason: '第0.4S 的 reject 数据'}
  ]

Promise.any([...个 promise 实例对象])

Promise的特点:

  1. 对象的状态不受外界影响。promise对象代表一个异步操作,有三种状态,pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是promise这个名字的由来——“承诺”;
  2. 一旦状态改变就不会再变,任何时候都可以得到这个结果。promise对象的状态改变,只有两种可能:从pending变为fulfilled,从pending变为rejected。这时就称为resolved(已定型)。如果改变已经发生了,你再对promise对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。

Promise的缺点:

  1. 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  2. 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  3. 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

总结:
**Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。

****状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。

**注意:在构造 Promise 的时候,构造函数内部的代码是立即执行的

import()异步导入

其作用:将 import关键字 的导入(本质是 同步的导入),变成 异步导入

好处:避免了若同步导入大量的数据,而造成 阻塞主线程的代码执行。

Promise解决了回调地狱的问题

async/await

async 函数中,可以 将 Promise 实例对象 的链式写法,变成 同步写法

async/await 是 ECMAScript 2017(ES8)引入的语言特性,用于异步编程。async/await 的目的是使异步代码的编写和阅读更加简单和直观,类似于同步代码的写法。

async/await 实际上是基于 Promise 的,它通过 asyncawait 关键字来定义异步函数和处理异步操作的结果。

async 关键字用于定义一个异步函数,异步函数会返回一个 Promise 对象.异步函数内部可以使用await 关键字来等待一个 Promise 对象的完成,并暂停异步函数的执行,直到 Promise 完成或拒绝,并返回它的结果。当 Promise完成或拒绝时,异步函数会继续执行。

async/await其实是Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定await只能出现在asnyc函数中.