14-异步
同步指的是代码是一行一行地顺序执行的。
通过回调函数实现异步
TIP
Asynchronous JavaScript And XML: Allows us to communicate with remote web servers in an asynchronous way. With AJAX calls, we can request data from web servers dynamically.
AJAX: 使我们能够通过异步的方式与远程服务器通信,通过 AJAX 调用,我们可以动态地从网络服务器请求数据。
一个简单的 AJAX 使用例子:
const request = new XMLHttpRequest();
request.open('GET', 'https://restcountries.com/v3.1/name/china');
request.send();
request.addEventListener('load', function(){ // 通过回调函数来处理异步
const [,,data] = JSON.parse(this.responseText);
console.log(data);
});
虽然现在不常用了,但是可以帮助我们理解异步。
通过上面这个例子,我能够更加明白为什么异步总是和回调函数一同出现。
在这种情况下,如果我们想要准确地控制两个异步函数的前后顺序要怎么做?比如在一个异步函数收到返回值的时候,利用这个返回值进行下一个请求。
答案就是在第一个异步函数的回调函数内再进行一次内部请求,如果我们还想继续异步请求呢?那就需要再在回调函数内进行异步请求,回调地狱就诞生了。😢
Promise 是什么
异步JavaScript 中有了一些讲解可以参考。
首先介绍 fetch
:
Fetch API
是一个用于在 JavaScript 中进行网络请求的接口,支持从服务器获取资源(如 JSON 文件、API 数据等)。它基于 Promise,允许开发者处理异步操作,提供对请求和响应数据的简单管理方式。
一个小例子:
const request = fetch('https://restcountries.com/v3.1/name/china');
console.log(request); // Promise {<pending>}
什么是 Promise?
TIP
Promise
是异步操作未来的结果的占位符。
可以把它比喻做一张彩票来理解🎉。拿着这张彩票,我们可能中奖,也可能不中,但不管如何,我们都可以在结果出来的时候用这张彩票去检查我们有没有中奖。
Promise
的优点:
- 不需要依赖事件以及回调函数。
- 可以使用
Promise
链,避免了回调地狱。
看一个经典的例子:
fetch('https://restcountries.com/v3.1/name/china')
.then(function(response){
return response.json(); // 返回一个 Promise,链式调用
}).then((data)=>{
console.log(data);
});
在 .then()
方法中,如果没有显式地返回,则自动返回一个 promise
;如果显式地返回了,则默认返回的 promise
将以这个返回值被解决。
在异常捕获方面,可以向 then()
中传递第二个参数(onreject 回调函数),也可以在最后用 catch()
方法捕获。
finally()
方法放在链式调用的最后,无论发生什么都会被调用。
事件循环
首先回忆一下之前学过的运行时概念:
#todo
创建 Promise
// 传入执行器函数,该函数的参数为 resolve 和 reject
const lotteryPromise = new Promise((resolve, reject) => {
if (Math.random() >= 0.5) {
resolve('WIN! 🏆');
} else {
reject('LOSE...💩');
}
});
lotteryPromise
.then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
});
传入一个执行器函数作为参数,该执行器函数又有两个参数,一个是 resolve
函数,一个是 reject
函数:
- 当在执行器函数中调用
resolve
函数时,表示该 promise 被兑现,然后使用then
方法处理被解决的情况。 - 当在执行器函数中调用
reject
函数时,表示该 promise 被拒绝,然后使用catch
方法处理被拒绝的情况。
在上面这种情况中,如何实现一秒后在展示输赢?
// 传入执行器函数,该函数的参数为 resolve 和 reject
const lotteryPromise = new Promise((resolve, reject) => {
console.log("🔮ing...")
setTimeout(() => {
if (Math.random() >= 0.5) {
resolve('WIN! 🏆');
} else {
reject('LOSE...💩');
}
}, 1000);
});
lotteryPromise
.then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
});
实现一个 sleep()
函数:
const sleep = function(sec){
return new Promise(resolve=>{
setTimeout(() => {
console.log(`after ${sec} seconds!`);
resolve();
}, sec * 1000);
})
}
sleep(1);
获取地理位置
使用 web API 可以异步获取当前用户的地理位置,如:
navigator.geolocation.getCurrentPosition(
position => console.log(position),
err => console.log(err)
);
console.log("getting position");
打印出的内容为:
getting position
GeolocationPosition {coords: GeolocationCoordinates, timestamp: 1726385109729}
如何将上方这种基于回调函数的 API 封装为基于 promise 的 API?
const getPosition = function () {
return new Promise((resolve, reject) => {
// navigator.geolocation.getCurrentPosition(
// position => resolve(position),
// err => reject(err)
// );
navigator.geolocation.getCurrentPosition(resolve, reject);
});
};
getPosition().then((result) => {
console.log(result);
}).catch((err) => {
console.log(err);
});
注意上方高亮的那一行的写法和注释掉的作用是相同的。
通过这种方式,我们就将一个简单的异步函数封装为了 promise ,使得我们可以很方便地去调用 then
和 catch
方法。
async await
一个简单的例子:
const whereAmI = async function(country){
console.log('first');
const res = await fetch(`https://restcountries.com/v3.1/name/${country}`); // 在这里暂停直到 promise 被兑现
console.log(res);
}
whereAmI('china');
console.log('second');
打印出的结果为:
first
second
Response {type: 'cors', url: 'https://restcountries.com/v3.1/name/china', redirected: false, status: 200, ok: true, …}
这种方法实质上只是 promise 的语法糖,本质上是相同的,上面的代码用 promise 实现如下:
const whereAmI = async function (country) {
console.log('first');
// const res = await fetch(`https://restcountries.com/v3.1/name/${country}`);
// console.log(res);
fetch(`https://restcountries.com/v3.1/name/${country}`).then(res => {
console.log(res);
});
};
whereAmI('china');
console.log('second');
这种方式可以让多个异步操作看起来非常清晰,如:
const whereAmI = async function(country){
console.log('first');
const res = await fetch(`https://restcountries.com/v3.1/name/${country}`); // 在这里暂停直到 promise 被兑现
const data = await res.json();
console.log(data);
}
whereAmI('china');
console.log('second');
按照原来的方法,这里是需要先返回一个 promise 再 then 的,但现在就不需要了。
再看一种情况,如果我想在返回一个异步结果,并在外面获取到该结果,应该怎么办?比如:
const whereAmI = async function(country){
const res = await fetch(`https://restcountries.com/v3.1/name/${country}`); // 在这里暂停直到 promise 被兑现
const data = await res.json();
return data;
}
const chinaData = whereAmI('china');
console.log(chinaData);
当前这种方法打印出来的结果是:Promise {<pending>}
这是因为 async
函数总是返回一个 promise ,该 promise 以我返回的数据被兑现。因此,想要实现上述的功能,真正的写法为:
const whereAmI = async function(country){
const res = await fetch(`https://restcountries.com/v3.1/name/${country}`); // 在这里暂停直到 promise 被兑现
const data = await res.json();
return data;
}
whereAmI('china')
.then(data => {
console.log(data);
})
.finally(() => {
console.log('last');
});
如果我不想使用链式调用,只想用同步代码的形式,应该怎么做呢?可以使用立即执行函数表达式:
(async function() {
const res = await whereAmI('china');
console.log(res);
console.log('last');
})();
异步并行 Promise.all()
先看一个例子:
const get3Countries = async function(c1, c2, c3) {
try{
const res1 = await fetch(`https://restcountries.com/v3.1/name/${c1}`);
const res2 = await fetch(`https://restcountries.com/v3.1/name/${c2}`);
const res3 = await fetch(`https://restcountries.com/v3.1/name/${c3}`);
console.log([res1, res2, res3]);
}catch(err){
console.log(err);
}
}
get3Countries('china','usa','japan');
在这种情况下,三个请求是先后发生的,串行的,前一个结束后后一个才开始,如图:
但其实完全可以并行请求:
const get3Countries = async function (c1, c2, c3) {
try {
const res = await Promise.all([
fetch(`https://restcountries.com/v3.1/name/${c1}`),
fetch(`https://restcountries.com/v3.1/name/${c2}`),
fetch(`https://restcountries.com/v3.1/name/${c3}`),
]);
console.log(res);
} catch (err) {
console.log(err);
}
};
get3Countries('china', 'usa', 'japan');
Promise.all()
接收一个 promise 数组,并返回一个 promise ,以所有传入的 promise 的兑现构成的数组来兑现。
此时,传入的所有 promise 就是并行的:
Promise.all
中只要有一个失败,就会立即被拒绝,并不会执行后面所有的 promise。
异常处理 try...catch...
try {
const x = 2;
x = 5;
} catch {
console.error('error when assign to a const variable!');
}
经常用 try...catch...
来捕获 async await
中的异常。
Promise.race()
Promise.race()
: 传入一个 promise 数组,返回的 promise 数组总是以第一个 settle
的 promise 被解决。就像是所有 promise 在比赛一样。
race 方法有一个非常有用的功能就在于防止时间超级长的异步请求,比如由于用户网络不好,该异步请求很长时间都不会 settle
,我们可以给它设置一个超时时间。
如:
const timeout = function (ms) {
return new Promise((_, reject) => {
setTimeout(() => {
reject('long long time...');
}, ms);
});
};
(async function () {
const res = await Promise.race([
fetch(`https://restcountries.com/v3.1/name/china`),
fetch(`https://restcountries.com/v3.1/name/usa`),
fetch(`https://restcountries.com/v3.1/name/japan`),
timeout(100)
]);
console.log(res);
})();
并且这里需要注意的一点是,立即执行函数表达式前面需要有正确的分号,不然可能会报错。
上方的代码限制了异步操作必须在 0.1 秒之内有结果,否则直接以超时被解决。
Promise.allSettled()
Promise.allSettled([
Promise.resolve('success'),
Promise.reject('false'),
Promise.resolve('success2'),
]).then(res=>{
console.log(res);
});
即使其中的一个被 reject
,所有的 promise 依然会全部运行完毕,这就是和 Promise.all()
不同的地方。