Skip to content

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 使用例子:

js
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,允许开发者处理异步操作,提供对请求和响应数据的简单管理方式。

一个小例子:

js
const request = fetch('https://restcountries.com/v3.1/name/china');
console.log(request); // Promise {<pending>}

什么是 Promise?

TIP

Promise 是异步操作未来的结果的占位符

可以把它比喻做一张彩票来理解🎉。拿着这张彩票,我们可能中奖,也可能不中,但不管如何,我们都可以在结果出来的时候用这张彩票去检查我们有没有中奖。

Promise 的优点:

  1. 不需要依赖事件以及回调函数。
  2. 可以使用 Promise 链,避免了回调地狱。

看一个经典的例子:

js
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

js
// 传入执行器函数,该函数的参数为 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 方法处理被拒绝的情况。

在上面这种情况中,如何实现一秒后在展示输赢?

js
// 传入执行器函数,该函数的参数为 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() 函数:

js
const sleep = function(sec){
  return new Promise(resolve=>{
    setTimeout(() => {
      console.log(`after ${sec} seconds!`);
      resolve();
    }, sec * 1000);
  })
}

sleep(1);

获取地理位置

使用 web API 可以异步获取当前用户的地理位置,如:

js
navigator.geolocation.getCurrentPosition(
  position => console.log(position),
  err => console.log(err)
);

console.log("getting position");

打印出的内容为:

bash
getting position
GeolocationPosition {coords: GeolocationCoordinates, timestamp: 1726385109729}

如何将上方这种基于回调函数的 API 封装为基于 promise 的 API?

js
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 ,使得我们可以很方便地去调用 thencatch 方法。

async await

一个简单的例子:

js
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');

打印出的结果为:

bash
first
second
Response {type: 'cors', url: 'https://restcountries.com/v3.1/name/china', redirected: false, status: 200, ok: true, …}

这种方法实质上只是 promise 的语法糖,本质上是相同的,上面的代码用 promise 实现如下:

js
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');

这种方式可以让多个异步操作看起来非常清晰,如:

js
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 的,但现在就不需要了。

再看一种情况,如果我想在返回一个异步结果,并在外面获取到该结果,应该怎么办?比如:

js
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 以我返回的数据被兑现。因此,想要实现上述的功能,真正的写法为:

js
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');
  });

如果我不想使用链式调用,只想用同步代码的形式,应该怎么做呢?可以使用立即执行函数表达式:

js
(async function() {
  const res = await whereAmI('china');
  console.log(res);
  console.log('last');
})();

异步并行 Promise.all()

先看一个例子:

js
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');

在这种情况下,三个请求是先后发生的,串行的,前一个结束后后一个才开始,如图:

但其实完全可以并行请求:

js
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...

js
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,我们可以给它设置一个超时时间。

如:

js
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()

js
Promise.allSettled([
  Promise.resolve('success'),
  Promise.reject('false'),
  Promise.resolve('success2'),
]).then(res=>{
  console.log(res);
});

即使其中的一个被 reject ,所有的 promise 依然会全部运行完毕,这就是和 Promise.all() 不同的地方。

参考