1.回调函数,地狱回调,Promise,async/await。

147 阅读11分钟

本期内容主要是讲一下什么是回调函数,地狱回调的形成,如何使用Promise解决地狱回调,promise到底是什么,promise常用方法的总结,什么是async/await及其如何工作的。

一,什么是回调函数?

一个函数当做参数传递给另一个函数时,这个被转递的函数就是回调函数,其作用说的直白一点就是在被需要的时候方便使用这一块代码,我们常见的定时器和ajax请求中都存在这样的回调函数:

  1. 定时器的回调函数 image.png 该定时器回调函数的执行结果分别在2秒后和3秒后显示: image.png 2.ajax请求的回调函数 image.pngxhr.send()发送请求并得到响应结果后执行第四步注册的回调函数并根据判断将得到的响应结果赋值给result变量。

二,什么是事件循环,同步任务,异步任务?

众所周知js是单线程运行的,在代码执行时,在主线程上排队执行的任务就是同步任务,当前一个任务执行完,才会执行下一个任务,当代码执行遇到异步任务时,会将这个任务暂时挂起,这种不阻塞后面任务执行的任务就是异步任务,当同步任务执行完后才会从任务队列中取出可执行的异步任务,这个任务队列可分为微任务队列和宏任务队列,js首先会判断微任务队列中是否有可执行的任务,如果有就先执行微任务队列中的异步任务,当微任务队列中的任务都执行完后再去判断宏任务队列中的任务是否存在,如果有就取出来执行。

  1. 微任务:promise.then catch finallys
  2. 宏任务:I/O setTimeout setInterval setImmediate image.png 根据上述推论代码执行至上而下,同步任务=》异步任务(微任务)=》异步任务(宏任务),执行结果:

image.png

三,什么是地狱回调?

在执行异步任务时,我们要保证异步任务按照我们要求的顺序执行就会形成地狱回调,下面我们通过node.js读取文件来解释一下。

使用require('fs')载入fs模块,模块中所有方法都有同步和异步两种形式。异步方法中回调函数的第一个参数总是留给异常参数(exception),如果方法成功完成,该参数为null或undefined。

image.png image.png 上述例子按照同步任务的思想执行顺序应该是a,b,c,但fs.readFile代表的是异步任务,我们无法保证它执行结果的顺序。

readFile同步的写法就是没有回调函数:fs.readFileSync(filename,[options])。 image.png 根据结果可以看出如果a中的文本很长,fs模块读取a文件的速度会相对于读取b文件的速度慢,b文件读取的异步任务先执行完,就会先被打印出来。如果我们想要按照a,b,c的顺序获得执行结果应该怎么写呢,如下:

image.png 这种回调函数嵌套回调函数的多层写法就会形成地狱回调,如果就这样嵌套下去,就会造成我们的代码可读性差,后期不好维护,看起来也不美观。当然上方代码的执行结果是按顺序执行的。

image.png

那么如何解决地狱回调呢?就要用到Promise或者async/await。

三,什么是Promise?

Promise的出现是为了解决地狱回调的问题,它是异步编程的一种解决方案。 那在代码中Promise是个什么东西呢,我们在控制台打印Promise来看一下:

image.png

从上图我们可以看出Promise是一个构造函数,它里面有许多自带的方法比如all,race,reject,resolve,这几个方法我们之后会详细说。在它的原型上我们也可以看到catch,finally,then方法,那么我们通过 new Promise()得到的Promise对象也可以使用原型上的这些方法,这就为之后我们讲解链式调用提前了解了一下。那么知道了Promise是一个构造函数,我们就通过new关键字来实例化一个Promise对象来看一下:

image.png

从上图我们可以看出Promise构造函数接受一个函数作为参数,这个作为参数的函数叫做“处理器函数”(executor function),它接受两个参数resolve,reject,它的内部就是我们做异步操作的地方,当异步任务成功完成且返回结果值时,会调用resolve函数返回结果,而当异步任务失败且返回失败原因(通常是一个错误对象)时,会调用reject函数。

image.png

上方我们人为的控制了异步任务的成功和失败,分别调用了resolvereject函数,两者都能得到一个Promise对象,这个Promise 对象就是用于表示一个异步操作的最终完成 (或失败)及其结果值。

一个Promise必然处于以下几种状态:

  • pending(待定):初始化状态,既不是成功,也不是失败。
  • fulfilled(已兑现):意味着操作成功。
  • rejected(已拒绝):意味着操作失败。 一个Promise对象在刚被创建时它代表的值可能是未知的,也就是说异步任务可能在进行当中,它此时处于的状态是pending(待定)状态,当异步任务完成时(或失败),完成时待定状态的 Promise 对象要么会通过一个值被兑现(fulfilled),或者通过一个原因(错误)被拒绝(rejected),下面我们通过代码来感受一下:

image.png 上图我们可以看出我们把setTimeout调整成10秒,刚实例化promise对象时,我们就打印这个promise对象,异步任务还在进行当中,此时异步任务的返回值还是未知的,promise正处于pending(待定)状态,十秒后setTimeout的回调函数执行完成,我们再打印promise对象,此时promise处于fulfilled(已兑现)的状态,并且我们可以看到成功的返回值。

那么怎么通过promise解决上方我们演示过的地狱回调的案例呢,promise对象能过让我们将异步操作成功或失败的返回值和响应的处理程序关联在一起,当promise状态变更时(pending=>fulfilled or pending=>rejectd),我们用 promisethen,catch,finally 方法排列起来的相关处理程序就会被调用,这样使得异步方法可以像同步方法那样返回值,那我们就用promise重写上方地狱回调的案例:

image.png

image.png

从执行结果可以看出程序按照我们预先想要顺序a,b,c得到我们想要的执行结果。

Promise的链式调用 因为 Promise.prototype.then 和  Promise.prototype.catch 方法返回的是 promise, 所以它们可以被链式调用。

image.png then(),catch(),finally(),这三个方法的参数都是一个函数。then()可以将参数中的函数添加到当前promise正常执行序列。then()传入的函数会按顺序依次执行,有任何异常都会跳到catch序列,catch()则用来处理异常响应的情况,finally()是不管promise的状态是什么情况最后都会被执行。链式调用时,每一个then做完处理后,一定要return一个Promise对象,不然下一个接受到的数据就是一个undefined

image.png

image.png

接下来我们拿一个项目中常用的异步请求接口的代码块来看一下链式调用:

image.png 这是一个保存按钮,我们通过then()方法拿到我们保存成功后返回的数据,展示一个保存成功的提示信息,将保存按钮的加载效果恢复默认,然后重新请求页面数据,当保存失败接口异常时会走到catch()方法,打印错误信息,将保存按钮的加载效果恢复默认,最后加一个兜底函数finally(),不管接口调用成功或失败都触发的函数将保存按钮的加载效果恢复默认。

接下来讲一讲Promise自带的四种方法,Promise.all(),Promise.race(),Promise.reject(),Promise.resolve()

1.Promise.all()

Promise.all()方法接受一个由Promise所组成的iterable类型(Array,Map,Set都属于ES6的iterable类型)的内容作为参数,并且只返回一个Promise实例,这个Promiseresolve回调执行是在传入的所有promiseresolve回调执行完成或者内部没有可promise的时候,如果传入的这个数组中其中一项调用了reject回调,那么Promise.all()promise实例的reject函数回调将被调用,并且reject的是第一个抛出的错误信息。

image.png image.png

从结果可以看出Promise.all()是按传入的promise集合的顺序执行,并且等到了所有内部promiseresolve执行完成才调用自己的resolve回调输出结果。

接下来我们再看一下输入的promise调用reject回调函数的情况 image.png image.png image.png

当输入的promise中有调用reject回调函数的情况就会立刻触发Promise.all()返回的Promise实例的reject回调函数将第一个错误的信息抛出来。

2.Promise.race()

Promise.race()方法接受一个由Promise所组成的iterable类型(Array,Map,Set都属于ES6的iterable类型)的内容作为参数,并且只返回一个Promise实例,一旦传入的promise数组中的的某个promise完成或拒绝,Promise.race()Promise实例就立即调用resolvereject回调函数返回完成或拒绝的值或原因。

image.png 显然p2要先执行完所以返回的是p2的失败原因。

image.png image.png

显然p4要先执行完所以返回的是p4成功的信息。

3.Promise.resolve()

promise.resolve()接受一个将被promise解析的参数,这个参数也可以是个promise对象,promise.resolve()返回的也是一个经过解析带有解析值得promise对象。

image.png

image.png

4.Promise.reject()

Promise.reject()接受一个失败原因作为参数,返回一个带有失败原因的promise对象。

image.png

四,什么是async/await?

async/await是建立在promise对象之上的一种编写异步的新方法,用async关键字声明的函数叫做async函数也可成为异步函数,表示该函数现在是一个异步任务,不会阻塞后面函数的执行,拿个例子来看一下:

image.png image.png 打印出来我们可以看出异步函数通过return函数返回的数据自动生成一个promise对象,那么如何获取promise对象的值我们都已经清楚了,当然是用then,catch,finally这些方法。 image.png image.png

async关键字了解过后,我们来看一下await关键字,await关键字意味着等待,它与async关键字搭配使用,并且必须写在async函数的内部。

image.png image.png 当调用异步函数testResult时执行函数内部的程序,遇到第一个await表达式,await 后面放置的就是返回promise对象的一个表达式,遇到由await关键字修饰的程序,那么由async关键字修饰的异步任务成立,代码从调用testResult()处会继续执行同步任务,打印出'你好',当同步任务执行完后会执行异步任务,等待await关键字后面修饰的内容返回结果,结果返回后会继续向下执行异步函数里面的程序打印出result结果。

async函数的函数体可以被看作是由0个或者多个await表达式分割开来的。从第一行代码直到(并包括)第一个await表达式(如果有的话)都是同步运行的。这样的话,一个不含await表达式的async函数是会同步运行的。然而,如果函数体内有一个await表达式,async函数就一定会异步执行。 image.png image.png 从执行结果可以看出异步函数内部的第一行代码还是同步的打印出了'我在第一位',当遇到await表达式就变成了异步任务,暂时挂起后,去执行了同步任务打印了'你好',同步任务执行完后再执行了异步任务,等待第一个await表达式执行完后,再去执行了第二个await表达式,最后await表达式都执行完后拿到结果继续执行了异步函数里面await表达式下面的内容。

接下来我们就使用async/await改造下我们上方的地狱回调案例: image.png image.png

我们使用try/catch来捕获异常,当await表达式捕获的不是promise实例对象resolve回调的返回值,会通过catch抛出异常,并且抛出异常的await表达式下方的程序不会继续执行。

以上就是我对回调函数,地狱回调,Promise,async/await理解的全部内容。