按照国际惯例,说异步之前,需要说下同步
同步
比如如下代码,正常来说,会依次打印 123
1 | console.log(1); |
JS 的代码执行是单线程的,这个不是说 JS 引擎只有一个线程,而是执行代码的是一个线程,另外还有其他的线程做其他的事情
在上面输出 123 的情况下,如果第二步非常耗时,而这些步骤之间没有强的依赖关系的话,就会影响效率
这个时候就可以通过回调函数来处理第二步请求,也叫 callback
回调函数
1 | console.log(1); |
在第二步模拟一个耗时操作,比如要用 1 秒,如果按照上面的同步方式,则会输出 1,卡 1 秒后再输出 23,但是如果用回调,则不会阻塞,输出 13,1 秒后再通过回调,输出 2
耗时任务越多,这个效果越明显
下面用 XHR 模拟一个比较真实的场景
XHR 有 5 种状态:XMLHttpRequest.readyState
Value | State | Description |
---|---|---|
0 | UNSENT | Client has been created. open() not called yet. |
1 | OPENED | open() has been called. |
2 | HEADERS_RECEIVED | send() has been called, and headers and status are available. |
3 | LOADING | Downloading; responseText holds partial data. |
4 | DONE | The operation is complete. |
在一个普通的请求中,如果正常完成,会返回 DONE
,然后就可以根据 xhr 的状态码和 HTTP 的状态码来进行处理
1 | const xhr = new XMLHttpRequest(); |
考虑到可能会在多个地方需要发出这个请求,稍微封装下
1 | const getData = () => { |
这样就可以很方便的进行调用
1 | getData(); |
如果要通过回调函数的方式,则可以改为
1 | const getData = (callback) => { |
传入了一个回调函数,带两个参数,并进行成功与否的处理
这部分处理很像 Go 的双返回值,太像了
1 | func getData() (string, error) { |
如果在 getData()
前后分别打印 12,则会输出 12,再执行里面的耗时任务,达到一个异步处理的效果
这就是回调函数,但是回调函数带来了一个问题
如果任务之间有强依赖,比如我要先登录,再获取用户信息,再进行另外的操作,如果用回调函数的写法,就会出现一个问题
1 | login((data, err) => { |
如果有 N 个请求,就会出现一个 >
,业界叫回调地狱
虽然代码是给机器执行的,但那是编译过的,源代码是给人看的,这样会不好维护
于是出现了一个新对象 Promise
Promise
Promise
用人话说就是一些需要一定的时间去完成的事情,不知道英文为什么叫这个名字,如果你用中文的承诺去套,也可以说得通
比如你结婚的时候,许下承诺,一辈子,那就是需要很长时间来完成的事情
Promise
会出现两种可能的结果:
- 成功的时候,走
resolve
- 失败则走
reject
同时也接收这两个函数作为参数
这样上面的 getData()
就可以改写成
1 | const getData = () => { |
而 Promise
的后续一般用 then()
来处理 resolve
的结果,catch()
来处理 reject
的结果
1 | getData() |
那么 Promise
是如何解决回调地狱的?
1 | login() |
既然每个请求都返回 Promise
,而 Promise
又要 then()
来处理,那么通过将 Promise
进行 return
, 再通过 then()
来处理,则可完成相同的效果
这样在一定程度上避免的回调的嵌套,但是还是不太直观
那么还有没有另外的方式?
有
async / await
通过将函数设置为 async
,在这个异步函数内部则可通过 await
来进行顺序的处理
改写上面方法,要定义一个异步方法,async
关键字要写在 function
前面,如果是箭头函数,则在参数前面
1 | const getData = async () => { |
由于 fetch
本身是异步的,返回一个 Promise
,因此里面通过 await
关键字可以得到最终的结果
又由于这个方法也设置成了异步方法,所以也需要通过 then()
再来进行最后的处理
1 | getData().then((data) => console.log(data)); |
值得注意的是,一般情况下,请求不是 2xx 就会当作失败,出异常,理应走 reject
,但是这个 fetch
即使出了 404 或者 500,还是走 resolve
,这个时候,需要进一步判断它的 ok
属性或 HTTP 状态码,才能判断请求是否正常