本期内容主要是讲一下什么是回调函数,地狱回调的形成,如何使用Promise解决地狱回调,promise到底是什么,promise常用方法的总结,什么是async/await及其如何工作的。
一,什么是回调函数?
一个函数当做参数传递给另一个函数时,这个被转递的函数就是回调函数,其作用说的直白一点就是在被需要的时候方便使用这一块代码,我们常见的定时器和ajax请求中都存在这样的回调函数:
- 定时器的回调函数
该定时器回调函数的执行结果分别在2秒后和3秒后显示:
2.ajax请求的回调函数
在
xhr.send()
发送请求并得到响应结果后执行第四步注册的回调函数并根据判断将得到的响应结果赋值给result
变量。
二,什么是事件循环,同步任务,异步任务?
众所周知js是单线程
运行的,在代码执行时,在主线程上排队执行的任务就是同步任务
,当前一个任务执行完,才会执行下一个任务,当代码执行遇到异步任务时,会将这个任务暂时挂起,这种不阻塞后面任务执行的任务就是异步任务
,当同步任务执行完后才会从任务队列
中取出可执行的异步任务
,这个任务队列可分为微任务
队列和宏任务
队列,js首先会判断微任务
队列中是否有可执行的任务,如果有就先执行微任务
队列中的异步任务
,当微任务
队列中的任务都执行完后再去判断宏任务
队列中的任务是否存在,如果有就取出来执行。
- 微任务:promise.then catch finallys
- 宏任务:I/O setTimeout setInterval setImmediate
根据上述推论代码执行至上而下,
同步任务=》异步任务(微任务)=》异步任务(宏任务)
,执行结果:
三,什么是地狱回调?
在执行异步任务时,我们要保证异步任务按照我们要求的顺序执行就会形成地狱回调,下面我们通过node.js读取文件来解释一下。
使用require('fs')载入fs模块,模块中所有方法都有同步和异步两种形式。异步方法中回调函数的第一个参数总是留给异常参数(exception),如果方法成功完成,该参数为null或undefined。
上述例子按照同步任务的思想执行顺序应该是a,b,c,但
fs.readFile
代表的是异步任务,我们无法保证它执行结果的顺序。
readFile同步的写法就是没有回调函数:fs.readFileSync(filename,[options])。
根据结果可以看出如果a中的文本很长,fs模块读取a文件的速度会相对于读取b文件的速度慢,b文件读取的异步任务先执行完,就会先被打印出来。如果我们想要按照a,b,c的顺序获得执行结果应该怎么写呢,如下:
这种回调函数嵌套回调函数的多层写法就会形成
地狱回调
,如果就这样嵌套下去,就会造成我们的代码可读性差,后期不好维护,看起来也不美观。当然上方代码的执行结果是按顺序执行的。
那么如何解决地狱回调呢?就要用到Promise或者async/await。
三,什么是Promise?
Promise
的出现是为了解决地狱回调
的问题,它是异步编程的一种解决方案。 那在代码中Promise
是个什么东西呢,我们在控制台打印Promise
来看一下:
从上图我们可以看出Promise
是一个构造函数
,它里面有许多自带的方法比如all,race,reject,resolve
,这几个方法我们之后会详细说。在它的原型上我们也可以看到catch,finally,then
方法,那么我们通过 new Promise()
得到的Promise
对象也可以使用原型上的这些方法,这就为之后我们讲解链式调用提前了解了一下。那么知道了Promise
是一个构造函数,我们就通过new关键字来实例化一个Promise
对象来看一下:
从上图我们可以看出Promise构造函数接受一个函数作为参数,这个作为参数的函数叫做“处理器函数”(executor function)
,它接受两个参数resolve
,reject
,它的内部就是我们做异步操作的地方,当异步任务成功完成且返回结果值时,会调用resolve
函数返回结果,而当异步任务失败且返回失败原因(通常是一个错误对象)时,会调用reject
函数。
上方我们人为的控制了异步任务的成功和失败,分别调用了resolve
和reject
函数,两者都能得到一个Promise
对象,这个Promise
对象就是用于表示一个异步操作的最终完成 (或失败)及其结果值。
一个Promise必然处于以下几种状态:
- pending(待定):初始化状态,既不是成功,也不是失败。
- fulfilled(已兑现):意味着操作成功。
- rejected(已拒绝):意味着操作失败。
一个
Promise
对象在刚被创建时它代表的值可能是未知的,也就是说异步任务可能在进行当中,它此时处于的状态是pending(待定)
状态,当异步任务完成时(或失败),完成时待定状态的Promise
对象要么会通过一个值被兑现(fulfilled)
,或者通过一个原因(错误)被拒绝(rejected)
,下面我们通过代码来感受一下:
上图我们可以看出我们把
setTimeout
调整成10秒,刚实例化promise
对象时,我们就打印这个promise
对象,异步任务
还在进行当中,此时异步任务
的返回值还是未知的,promise
正处于pending(待定)
状态,十秒后setTimeout
的回调函数执行完成,我们再打印promise
对象,此时promise
处于fulfilled(已兑现)
的状态,并且我们可以看到成功的返回值。
那么怎么通过promise
解决上方我们演示过的地狱回调的案例呢,promise
对象能过让我们将异步操作成功或失败的返回值和响应的处理程序关联在一起,当promise
状态变更时(pending=>fulfilled
or pending=>rejectd
),我们用 promise
的 then,catch,finally
方法排列起来的相关处理程序就会被调用,这样使得异步方法可以像同步方法那样返回值,那我们就用promise重写上方地狱回调的案例:
从执行结果可以看出程序按照我们预先想要顺序a,b,c得到我们想要的执行结果。
Promise的链式调用 因为
Promise.prototype.then
和Promise.prototype.catch
方法返回的是promise
, 所以它们可以被链式调用。
then(),catch(),finally()
,这三个方法的参数都是一个函数。then()可以将参数中的函数添加到当前promise
正常执行序列。then()
传入的函数会按顺序依次执行,有任何异常都会跳到catch
序列,catch()
则用来处理异常响应的情况,finally()
是不管promise
的状态是什么情况最后都会被执行。链式调用时,每一个then
做完处理后,一定要return
一个Promise
对象,不然下一个接受到的数据就是一个undefined
接下来我们拿一个项目中常用的异步请求接口的代码块来看一下链式调用:
这是一个保存按钮,我们通过
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
实例,这个Promise
的resolve
回调执行是在传入的所有promise
的resolve
回调执行完成或者内部没有可promise
的时候,如果传入的这个数组中其中一项调用了reject
回调,那么Promise.all()
的promise
实例的reject
函数回调将被调用,并且reject
的是第一个抛出的错误信息。
从结果可以看出Promise.all()
是按传入的promise
集合的顺序执行,并且等到了所有内部promise
的resolve
执行完成才调用自己的resolve
回调输出结果。
接下来我们再看一下输入的promise
调用reject
回调函数的情况
当输入的promise
中有调用reject
回调函数的情况就会立刻触发Promise.all()
返回的Promise
实例的reject
回调函数将第一个错误的信息抛出来。
2.Promise.race()
Promise.race()
方法接受一个由Promise
所组成的iterable
类型(Array,Map,Set都属于ES6的iterable类型)的内容作为参数,并且只返回一个Promise
实例,一旦传入的promise
数组中的的某个promise
完成或拒绝,Promise.race()
的Promise
实例就立即调用resolve
或reject
回调函数返回完成或拒绝的值或原因。
显然p2要先执行完所以返回的是p2的失败原因。
显然p4要先执行完所以返回的是p4成功的信息。
3.Promise.resolve()
promise.resolve()
接受一个将被promise
解析的参数,这个参数也可以是个promise
对象,promise.resolve()
返回的也是一个经过解析带有解析值得promise
对象。
4.Promise.reject()
Promise.reject()
接受一个失败原因作为参数,返回一个带有失败原因的promise
对象。
四,什么是async/await?
async/await
是建立在promise
对象之上的一种编写异步的新方法,用async
关键字声明的函数叫做async
函数也可成为异步函数,表示该函数现在是一个异步任务,不会阻塞后面函数的执行,拿个例子来看一下:
打印出来我们可以看出异步函数通过
return
函数返回的数据自动生成一个promise
对象,那么如何获取promise
对象的值我们都已经清楚了,当然是用then,catch,finally
这些方法。
async
关键字了解过后,我们来看一下await
关键字,await
关键字意味着等待,它与async
关键字搭配使用,并且必须写在async
函数的内部。
当调用
异步函数testResult
时执行函数内部的程序,遇到第一个await
表达式,await
后面放置的就是返回promise
对象的一个表达式,遇到由await
关键字修饰的程序,那么由async
关键字修饰的异步任务成立,代码从调用testResult()
处会继续执行同步任务,打印出'你好'
,当同步任务执行完后会执行异步任务,等待await关键字后面修饰的内容返回结果,结果返回后会继续向下执行异步函数里面的程序打印出result
结果。
async
函数的函数体可以被看作是由0个或者多个await
表达式分割开来的。从第一行代码直到(并包括)第一个await
表达式(如果有的话)都是同步运行的。这样的话,一个不含await
表达式的async
函数是会同步运行的。然而,如果函数体内有一个await
表达式,async
函数就一定会异步执行。
从执行结果可以看出异步函数内部的第一行代码还是同步的打印出了
'我在第一位'
,当遇到await
表达式就变成了异步任务,暂时挂起后,去执行了同步任务打印了'你好'
,同步任务执行完后再执行了异步任务,等待第一个await
表达式执行完后,再去执行了第二个await
表达式,最后await
表达式都执行完后拿到结果继续执行了异步函数里面await
表达式下面的内容。
接下来我们就使用async/await
改造下我们上方的地狱回调案例:
我们使用try/catch
来捕获异常,当await
表达式捕获的不是promise
实例对象resolve
回调的返回值,会通过catch
抛出异常,并且抛出异常的await
表达式下方的程序不会继续执行。
以上就是我对回调函数,地狱回调,Promise,async/await理解的全部内容。