异步JS

按照国际惯例,说异步之前,需要说下同步

同步

比如如下代码,正常来说,会依次打印 123

1
2
3
console.log(1);
console.log(2);
console.log(3);

JS 的代码执行是单线程的,这个不是说 JS 引擎只有一个线程,而是执行代码的是一个线程,另外还有其他的线程做其他的事情

在上面输出 123 的情况下,如果第二步非常耗时,而这些步骤之间没有强的依赖关系的话,就会影响效率

这个时候就可以通过回调函数来处理第二步请求,也叫 callback

回调函数

1
2
3
4
5
console.log(1);
setTimeout(() => {
console.log(2);
}, 1000);
console.log(3);

在第二步模拟一个耗时操作,比如要用 1 秒,如果按照上面的同步方式,则会输出 1,卡 1 秒后再输出 23,但是如果用回调,则不会阻塞,输出 13,1 秒后再通过回调,输出 2

耗时任务越多,这个效果越明显

下面用 XHR 模拟一个比较真实的场景

XHR 有 5 种状态:XMLHttpRequest.readyState

ValueStateDescription
0UNSENTClient has been created. open() not called yet.
1OPENEDopen() has been called.
2HEADERS_RECEIVEDsend() has been called, and headers and status are available.
3LOADINGDownloading; responseText holds partial data.
4DONEThe operation is complete.

在一个普通的请求中,如果正常完成,会返回 DONE,然后就可以根据 xhr 的状态码和 HTTP 的状态码来进行处理

1
2
3
4
5
6
7
8
9
10
11
12
const xhr = new XMLHttpRequest();
xhr.addEventListener("readystatechange", () => {
if (xhr.readyState === xhr.DONE) {
if (xhr.status === 200) {
console.log(xhr.responseText);
} else {
console.log("error");
}
}
xhr.open("GET", "xxx");
xhr.send();
});

考虑到可能会在多个地方需要发出这个请求,稍微封装下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const getData = () => {
const xhr = new XMLHttpRequest();
xhr.addEventListener("readystatechange", () => {
if (xhr.readyState === xhr.DONE) {
if (xhr.status === 200) {
console.log(xhr.responseText);
} else {
console.log("error");
}
}
});
xhr.open("GET", "xxx");
xhr.send();
};

这样就可以很方便的进行调用

1
getData();

如果要通过回调函数的方式,则可以改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const getData = (callback) => {
const xhr = new XMLHttpRequest();
xhr.addEventListener("readystatechange", () => {
if (xhr.readyState === xhr.DONE) {
if (xhr.status === 200) {
callback(xhr.responseText, undefined);
} else {
callback(undefined, "error");
}
}
});
xhr.open("GET", "xxx");
xhr.send();
};

getData((data, err) => {
if (err) {
console.log(err);
} else {
console.log(data);
}
});

传入了一个回调函数,带两个参数,并进行成功与否的处理

这部分处理很像 Go 的双返回值,太像了

1
2
3
4
5
6
7
8
func getData() (string, error) {
return data, nil;
}

data, err := getData()
if err != nil {
// print data
}

如果在 getData() 前后分别打印 12,则会输出 12,再执行里面的耗时任务,达到一个异步处理的效果

这就是回调函数,但是回调函数带来了一个问题

如果任务之间有强依赖,比如我要先登录,再获取用户信息,再进行另外的操作,如果用回调函数的写法,就会出现一个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
login((data, err) => {
if (!err) {
getUserInfo((data, err) => {
if (!err) {
getData((data, err) => {
if (!err) {
// ok
}
});
}
});
}
});

如果有 N 个请求,就会出现一个 >,业界叫回调地狱

虽然代码是给机器执行的,但那是编译过的,源代码是给人看的,这样会不好维护

于是出现了一个新对象 Promise

Promise

Promise 用人话说就是一些需要一定的时间去完成的事情,不知道英文为什么叫这个名字,如果你用中文的承诺去套,也可以说得通

比如你结婚的时候,许下承诺,一辈子,那就是需要很长时间来完成的事情

Promise 会出现两种可能的结果:

  • 成功的时候,走 resolve
  • 失败则走 reject

同时也接收这两个函数作为参数

这样上面的 getData() 就可以改写成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const getData = () => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.addEventListener("readystatechange", () => {
if (xhr.readyState === xhr.DONE) {
if (xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject("error");
}
}
});
xhr.open("GET", "xxx");
xhr.send();
});
};

Promise 的后续一般用 then() 来处理 resolve 的结果,catch() 来处理 reject 的结果

1
2
3
getData()
.then((data) => console.log(data))
.catch((err) => console.log(err));

那么 Promise 是如何解决回调地狱的?

1
2
3
4
5
6
7
8
9
10
11
12
login()
.then((data) => {
console.log(data);
return getUserInfo();
})
.then((data) => {
console.log(data);
return getData();
})
.then((data) => {
console.log(data);
});

既然每个请求都返回 Promise,而 Promise 又要 then() 来处理,那么通过将 Promise 进行 return, 再通过 then() 来处理,则可完成相同的效果

这样在一定程度上避免的回调的嵌套,但是还是不太直观

那么还有没有另外的方式?

async / await

通过将函数设置为 async,在这个异步函数内部则可通过 await 来进行顺序的处理

改写上面方法,要定义一个异步方法,async 关键字要写在 function 前面,如果是箭头函数,则在参数前面

1
2
3
4
5
6
7
8
9
10
11
12
const getData = async () => {
const loginResponse = await fetch("login");
const loginData = await loginResponse.json();

const userInfoResponse = await fetch("userInfo");
const userInfoData = await userInfoResponse.json();

const response = await fetch("data");
const data = response.json();

return data;
};

由于 fetch 本身是异步的,返回一个 Promise,因此里面通过 await 关键字可以得到最终的结果

又由于这个方法也设置成了异步方法,所以也需要通过 then() 再来进行最后的处理

1
getData().then((data) => console.log(data));

值得注意的是,一般情况下,请求不是 2xx 就会当作失败,出异常,理应走 reject,但是这个 fetch 即使出了 404 或者 500,还是走 resolve,这个时候,需要进一步判断它的 ok 属性或 HTTP 状态码,才能判断请求是否正常