我没有很系统地学过前端,在写前端的时候如果只是单个的网络请求还好,用fetch、axios或者直接用XMLHttpRequest写起来也不复杂。

但是如果是多个请求按顺序拉取数据那可要了我老命了😂,我只会最简单的回调函数的写法,就像下面这些代码:

const requestOptions = {
    method: 'GET',
    redirect: 'follow'
};

fetch('https://xxx.yyy.com/api/zzz/', requestOptions)
    .then(response => response.text())
    .then(result => {
        const data = JSON.parse(result)
          
        fetch('https://xxx.yyy.com/api/aaa/'+data.id, requestOptions)
            .then(response => response.text())
            .then(result => {
                const data = JSON.parse(result)
                
            })
            .catch(error => console.log('error', error));
    })
    .catch(error => console.log('error', error));

假设我需要两步获取一个数据,如从https://xxx.yyy.com/api/zzz/获取一个数据对象data,之后通过data.id得到我要获取数据的序号,最后再发一次请求得到想要的数据。

用回调的方式就类似于上面这样,太繁琐了,而且容易出错,并且一旦逻辑复杂就不好改啦😭。

今天就花些时间好好学一下这块的知识,摆脱回调地狱👻

一、XMLHttpRequest

通过XMLHttpRequest对象创建网络请求的套路如下:

// 假设访问http://localhost:3000/user返回json对象{"name":"Jiangda"}
const xhr = new XMLHttpRequest();
const url = 'http://localhost:3000/user'

xhr.onreadystatechange = function(){
	if (this.readyState == 4 && this.status == 200){
		let json=JSON.parse(xhr.responseText)
		let name=json.name
		console.log(name)
	}
}
xhr.open('GET',url)
xhr.send()

首先创建一个XMLHttpRequest对象xhr,然后xhr.onreadystagechange监听“状态改变”事件,之后xhr.open初始化请求,最后xhr.send发送请求。

请求发送后,当浏览器收到响应时就会进入xhr.onreadystagechange的回调函数中去,首先if (this.readyStage == 4 && this.status == 200)判断readyStage是不是4,在整个请求过程中,xhr.onreadystagechange会触发四次,每次readyStage都会自增,从1一直到4,只有到了最后阶段也就是readyStage4时我们才能得到最终的响应数据,同时还要根据status判断响应的状态码是否正常,通常响应码为2开头说明请求没有遇到问题,最终在控制台上会打出Jiangda

可以看出,通过XMLHttpRequest来处理请求的话,回调函数的书写很麻烦。

二、Promise

如果一个事件依赖于另一个事件的返回结果,那么使用回调会使代码变得很复杂。Promise对象提供了检查操作失败或成功的一种模式。如果成功,则会返回另一个Promise。这使得回调的书写更加规范。

Promise处理的套路如下:

const promise = new Promise((resolve,reject)=>{
	let condition = true;
	if (condition) {
		resolve("ok")
	} else {
		reject("failed")
	}
}).then( msg => console.log(msg))
  .catch( err => console.error(err))
  .finally( _ =>console.log("finally"))

上面这段代码把整个处理过程串起来了,首先创建一个Promise对象,它的构造器接收一个函数,函数的第一个参数是没出错时要执行的函数resolve,第二个参数是出错后要执行的函数reject

我的理解是resolvereject都只是个代称,resolve代指成功后then里面的回调函数,reject代指失败后catch里执行的回调函数。最后的finally是不论成功失败都会执行的,可以用来做一些收尾工作。

基于Promise的网络请求可以用axios库以及浏览器自带的fetch实现。

axios库创建请求的套路如下:

import axios from 'axios'
const url = 'http://xxx.yyy.com/'
axios.get(url)
	.then(data => console.log(data))
	.catch(err => console.error(err))

我比较喜欢用fetch,它不需要导库,fetch创建请求的方式和axios类似,在开头已经展示过了就不重复写了。

虽然Promise把回调函数的编写方式简化了一些,但还是没有摆脱回调地狱,多个请求串起来的话就会像我开头写的那样,在then里面创建新的Promise,最终变成Promise地狱😂

三、async/await

async/await可以简化Promise的写法,使得代码中的函数虽然是异步的但执行顺序是同步的,并且在代码的书写上也和同步的差不多,易于理解。

直接拿MDN(https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Asynchronous/Async_await)上的例子说明吧:

直接用fetch获取数据:

fetch('coffee.jpg')
.then(response => response.blob())
.then(myBlob => {
  let objectURL = URL.createObjectURL(myBlob);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
})
.catch(e => {
  console.log('There has been a problem with your fetch operation: ' + e.message);
});

async/await改写后:

async function myFetch() {
  let response = await fetch('coffee.jpg');
  let myBlob = await response.blob();

  let objectURL = URL.createObjectURL(myBlob);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}

myFetch()
.catch(e => {
  console.log('There has been a problem with your fetch operation: ' + e.message);
});

改写后的代码是不是就很清楚了,没有那么多的then跟在后面了,这样如果有一连串的网络请求也不用怕了😁

async放在一个函数的声明前时,这个函数就是一个异步函数,调用该函数会返回一个Promise

await用于等待一个Promise对象,它只能在异步函数中使用,await表达式会暂停当前异步函数的执行,等待 Promise 处理完成。