【JavaScript】异步/Ajax/Callback Hell/Promises/Fetch API/Async/错误处理

8 篇文章 0 订阅

概念

我们平时写的代码:为同步代码,逐行进行
缺点:在遇到阻塞代码时,其他代码无法被执行,整个页面被“卡住”

异步代码:如setTimeout,在后台进行计时操作,在操作过程中同步代码不受影响,时间结束,回调函数被放到同步代码中执行(回调函数并不代表异步,只是被放到了 setTimeout中,成为异步执行。)

又例:img.src = 'xxx.jpg';
这句话让图片加载在后台进行执行,即异步执行
如果图片加载完成,可以用事件监听器监听到 ‘load’ 状态,其中的回调函数被异步执行。事件监听器并不异步,是其中的图片加载事件为异步的

Ajax

Asynchronous Javascript And XML (异步JavaScript和XML)允许我们用异步的方式与远程服务器交换数据
在这里插入图片描述

API

Application Programming Interface
XML为过去的api用来传输数据的工具,而现在更多用到JSON
在这里插入图片描述
API使用举例:

XML方式

CORS:跨域资源共享,需要被设为yes
由于AJax为异步执行,因此不能简单地对返回结果进行赋值(有可能会变为NULL),而应该使用事件监听器load属性
在这里插入图片描述

const req = new XMLHttpRequest();
//使用get方式发送请求
req.open('GET', 'https://restcountries.com/v3.1/name/china');
//发送请求
req.send();
req.addEventListener('load', function () {
	//获取到回答文本
  const [, , data] = JSON.parse(this.responseText); 
});

当多个请求被发送时,并不能保证代码中的顺序一定是送回数据的顺序,因此会引出Callback Hell

Callback Hell

这里利用开始得到的国家数据中的邻国数据,又创建了新的req2,此时两个req一定是按序到达的,因为第二个需要第一个的支持
这样的嵌套回调函数称为回调地狱,这样的代码不易读或扩充

const getCountryAndNeighbour = function (country) {
  // AJAX call country 1
  const request = new XMLHttpRequest();
  request.open('GET', `https://restcountries.eu/rest/v2/name/${country}`);
  request.send();

  request.addEventListener('load', function () {
    const [data] = JSON.parse(this.responseText);
    console.log(data);

    // Render country 1
    renderCountry(data);

    // Get neighbour country (2)
    const [neighbour] = data.borders;

    if (!neighbour) return;

    // AJAX call country 2
    const request2 = new XMLHttpRequest();
    request2.open('GET', `https://restcountries.eu/rest/v2/alpha/${neighbour}`);
    request2.send();

    request2.addEventListener('load', function () {
      const data2 = JSON.parse(this.responseText);
      console.log(data2);

      renderCountry(data2, 'neighbour');
    });
  });
};

Promises/Fetch API

Fetch API

仅需传入url

const req1 = fetch('https://restcountries.com/v3.1/name/china');

返回一个Promise,存于req1中

Promise:ES6

像一个异步传递值的容器,它提供一个占位符,用来保存未来的数据。
使用promise可以不去使用事件和回调函数,避免嵌套回调,可以将多个promise穿起来,以此达到多重按序调用的效果。

Promise过程示例:其中settled的状态只能被改变一次,即在异步完成时改变,当一个promise返回了值,我们称为consume promise
在这里插入图片描述

res.then()、res.json()

  • then可以接受两个回调函数,第一个为成功时执行,第二个为错误时执行,但是在多个then中添加多个错误发生时的回调函数并不方便,此时可以在函数链末端添加一个 catch 方法,它会捕获这条链上的任何错误并执行函数
//单独添加err处理
const getCountryData = function (country) {
  fetch(`https://restcountries.eu/rest/v2/name/${country}`)
  .then(function (response) {}, err=>{});
  
//使用catch
const getCountryData = function (country) {
  fetch(`https://restcountries.eu/rest/v2/name/${country}`)
  .then(...).then(...).then(...)
  .catch(err => alert(err));
  • fulfilled状态会返回一个 response,此时使用 then 获取 res,其中会返回一个具有数据流的对象,要使用 json() 方法进行解析

  • json 是 res对象自带方法,也是异步的,因此它还会返回一个promise,此时要继续使用一个then,最终获取到数据。

const getCountryData = function (country) {
  fetch(`https://restcountries.eu/rest/v2/name/${country}`)
    .then(function (response) {
      console.log(response);
      return response.json();
    })
    .then(function (data) {
      console.log(data);
      renderCountry(data[0]);
    });
};

//先fetch,后json解析,后获取到数据进行渲染
const getCountryData = function (country) {
  fetch(`https://restcountries.eu/rest/v2/name/${country}`)
    .then(response => response.json())
    .then(data => renderCountry(data[0]));
};

链式调用、finally方法、错误处理

链式调用时,只需要在第一个then中返回想要处理的第二个promise即可。这样就可以将所有promise按顺序执行。

const getCountryData = function (country) {
  // Country 1
  fetch(`https://restcountries.eu/rest/v2/name/${country}`)
    .then(response => {
      console.log(response);
      
      if (!response.ok)
        throw new Error(`Country not found (${response.status})`);
        
      return response.json();
    })
    .then(data => {
      renderCountry(data[0]);
      // const neighbour = data[0].borders[0];
      const neighbour = 'dfsdfdef';

      if (!neighbour) return;
	 
      // Country 2
      //此时return新的promise,那么then中将执行这个promise为 fulfilled 的代码,接下来再接then方法,无论return什么值,其中的值都会被当成一个promise的response data
      return fetch(`https://restcountries.eu/rest/v2/alpha/${neighbour}`);
      //不要在此处直接接then:又会成为Callback Hell,括号外再接
    })
    .then(response => {
      //如果新请求还出错
      if (!response.ok)
        throw new Error(`Country not found (${response.status})`);
        
 	  //要记得返回json
      return response.json();
    })
    .then(data => renderCountry(data, 'neighbour'))
    //最后错误处理函数
    .catch(err => {
      console.error(`${err} 💥💥💥`);
      //自定义
      renderError(`Something went wrong 💥💥 ${err.message}. Try again!`);
    })
    //无论前面发生什么都会被执行
    .finally(() => {
      countriesContainer.style.opacity = 1;
    });
};
  • finally加在链条末尾,总是会被执行
  • 使用new Error传入一个错误信息,生成一个错误对象,这里的错误对象可以在catch处被捕获,可以读取其信息:err.message
  • 使用throw时,会立即终止当前功能,实现类似return的功能,这时,promise状态变为rejected

将重复的地方封装为函数,简化代码:

//错误提示
const renderError = function (msg) {
  countriesContainer.insertAdjacentText('beforeend', msg);
  countriesContainer.style.opacity = 1;
};

//将请求收到一个函数内部
const getJSON = function (url, errorMsg = 'Something went wrong') {
  return fetch(url).then(response => {
    if (!response.ok) throw new Error(`${errorMsg} (${response.status})`);

    return response.json();
  });
};

const getCountryData = function (country) {
  // Country 1
  getJSON(
    `https://restcountries.eu/rest/v2/name/${country}`,
    'Country not found'
  )
    .then(data => {
      renderCountry(data[0]);
      const neighbour = data[0].borders[0];

      if (!neighbour) throw new Error('No neighbour found!');

      // Country 2
      return getJSON(
        `https://restcountries.eu/rest/v2/alpha/${neighbour}`,
        'Country not found'
      );
    })
    .then(data => renderCountry(data, 'neighbour'))
    .catch(err => {
      console.error(`${err} 💥💥💥`);
      renderError(`Something went wrong 💥💥 ${err.message}. Try again!`);
    })
    .finally(() => {
      countriesContainer.style.opacity = 1;
    });
};

Fatch的幕后

在这里插入图片描述
在这里插入图片描述
所有异步任务都是通过WEB API完成的,可以理解为另一个互不影响的线程,完成后的结果与回调函数被放入队列中

微任务队列和回调队列都在合适的时候回到主栈中

微任务队列优先级高于普通回调函数队列,因此当多个promise完成后,微任务队列将使回调队列阻塞,造成饿死现象。

因此在同时使用异步和计时器时,,要注意执行顺序

Promise对象的应用

  • 在新建的promise对象中,需要传入一个执行函数,这个函数包含两个参数:resolve, reject
const lotteryPromise = new Promise(function (resolve, reject) {
  console.log('Lotter draw is happening 🔮');
  setTimeout(function () {
    if (Math.random() >= 0.5) {
      //使用resolve设置resolve状态返回的信息,最终可被then方法捕获
      resolve('You WIN 💰');
    } else {
      //reject的信息
      reject(new Error('You lost your money 💩'));
    }
  }, 2000);
});

//then中:You WIN 💰
//catch中:You lost your money 💩
lotteryPromise.then(res => console.log(res)).catch(err => console.error(err));

Pormise.resolve() & Promise.reject()

直接将Promise的状态设置为res或rej

Promise.resolve('abc').then(x => console.log(x));
Promise.reject(new Error('Problem!')).catch(x => console.error(x));

Promisify 改写异步请求

举例1:地理信息API

使用Promise对象操纵地理信息API:

const getPosition = function () {
  return new Promise(function (resolve, reject) {
  	//旧写法,使用getCurrentPosition传入两个函数
    // navigator.geolocation.getCurrentPosition(
    //   position => resolve(position),
    //   err => reject(err)
    // );
    //自动传入两个回调函数
    navigator.geolocation.getCurrentPosition(resolve, reject);
  });
};
//操纵回调函数
getPosition().then(pos => console.log(pos));
举例2:图片加载操作

使用promise将加载图片这个异步操作使用promise 的方式(封装?)起来:

const imgContainer = document.querySelector('.images');

const createImage = function (imgPath) {
  return new Promise(function (resolve, reject) {
    const img = document.createElement('img');
    img.src = imgPath;

    img.addEventListener('load', function () {
      imgContainer.append(img);
      resolve(img);
    });

    img.addEventListener('error', function () {
      reject(new Error('Image not found'));
    });
  });
}; 

这样做的意义:

在这段代码中使用Promise有以下几个优点:

  1. 可读性更高:使用Promise可以以更干净、更易读的方式处理加载图片的异步行为。在处理复杂代码(包含多个异步操作)时特别有用。
  2. 错误处理:使用Promise可以以集中的方式捕获和处理加载图片时发生的错误。如果发生错误,Promise将被拒绝,并传递“图片未找到”的错误消息,您可以使用.catch()方法处理该错误。
  3. 顺序执行:使用Promise,您可以轻松链接多个异步操作,并以特定顺序执行它们。例如,您可以按顺序加载多张图像,并在它们全部加载完成后执行一些操作。

相比之下,如果使用传统的事件监听器处理图像加载事件,您将不得不为每张图像创建单独的事件监听器,并编写额外的代码来处理错误和确保操作以所需的顺序执行。这可能使代码变得更难读和维护,特别是在复杂的应用程序中。

Async Function/Await

const whereAmI = async function (){ await fetch();} ;

async/await提供了一种更方便的方法来处理HTTP请求的异步性,并可以使代码更易读和理解。async关键字用于声明异步函数,而await关键字用于等待Promise的完成。

其中可添加多个await语句,按序进行,await 将会一直运行直到后面的语句返回一个promise对象

asnyc总是返回一个promise对象!

如const resGeo = await fetch(`https://geocode.xyz/ l a t , {lat}, lat,{lng}?geoit=json`);
相当于把.then()方法使用await 改写为保存到一个变量中

实际上,asnyc是生成promise对象的简化,是一种语法糖

与fetch对比

chatgpt的答案:
在这里插入图片描述

与promise对比

在这里插入图片描述
在这里插入图片描述

错误处理

使用 try/catch来进行错误处理并不局限于 async,它是 js的错误处理的语法结构。此处因为 async无法使用链式结构,因此需要通过 try/catch来捕获错误。
try和catch成对出现,首先尝试执行try中的代码
catch捕获try中的任何错误并执行自己的代码
结构:

try {
  let y = 1;
  const x = 2;
  y = 3;
} catch (err) {
  alert(err.message);
}

使用try/catch进行实际编码:

const whereAmI = async function () {
  try {
    // Geolocation
    const pos = await getPosition();
    const { latitude: lat, longitude: lng } = pos.coords;

    // Reverse geocoding
    const resGeo = await fetch(`https://geocode.xyz/${lat},${lng}?geoit=json`);
    if (!resGeo.ok) throw new Error('Problem getting location data');

    const dataGeo = await resGeo.json();
    console.log(dataGeo);

    // Country data
    const res = await fetch(
      `https://restcountries.eu/rest/v2/name/${dataGeo.country}`
    );
     
    // FIX:
    if (!res.ok) throw new Error('Problem getting country');

    const data = await res.json();
    console.log(data);
    renderCountry(data[0]);
  } catch (err) {
    console.error(`${err} 💥`);
    renderError(`💥 ${err.message}`);
  }
};

return from async

由于asnyc总是返回一个promise对象,因此其中的返回语句将会被放到promise的fulfilled值中:
在上面的程序中加入一句:
return 'You are in ${dataGeo.city}, ${dataGeo.country}';

  • 此时要获得 return 内容需要使用then,否则将是一整个对象
  • 此时如果上面的函数出现error,依然抛出promise对象,then依然会被执行,但对象将变为undefined
  • 修复方法:在上方catch中重新抛出问题⬇️
whereAmI()
  .then(city => console.log(`2: ${city}`))
  .catch(err => console.error(`2: ${err.message} 💥`))
  .finally(() => console.log('3: Finished getting location'));

原函数的catch:

catch (err) {
    console.error(`${err} 💥`);
    renderError(`💥 ${err.message}`);

    // Reject promise returned from async function
    throw err;
  }

IIFE方式

(async function () {
  try {
    const city = await whereAmI();
    console.log(`2: ${city}`);
  } catch (err) {
    console.error(`2: ${err.message} 💥`);
  }
  console.log('3: Finished getting location');
})();

Top level await(ES2022)

在之前版本的js中,await无法在async外被调用,但在新版支持了这种调用
在一个模块文件中:

Blocking code
console.log('Start fetching users');
await fetch('https://jsonplaceholder.typicode.com/users');
console.log('Finish fetching users');

在这种情况下,await会阻塞module的运行,并且阻塞引用模块的所有文件

更实际的应用:


const getLastPost = async function () {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts');
  const data = await res.json();

  return { title: data.at(-1).title, text: data.at(-1).body };
};
//这里会返回一个promise对象
const lastPost = getLastPost();
console.log(lastPost);

// Not very clean
//await无法在async外使用时的旧用法
// lastPost.then(last => console.log(last));

//新版特性
const lastPost2 = await getLastPost();
console.log(lastPost2);

Promise常用方法集合

Promise.all();

在async中同时进行多个异步请求

当异步请求顺序不重要是,可以使用这个函数

Promise.all() 将多个Promise实例,包装成一个新的Promise实例。

接受一个由promise任务组成的数组,可以同时处理多个promise任务。

当所有的任务都执行完成时,Promise.all()返回一个resolve结果数组,如果有一个失败(reject),则返回失败的信息,即使其他promise执行成功,也会返回失败。

// Running Promises in Parallel
const get3Countries = async function (c1, c2, c3) {
  try {
  	//老方法
    // const [data1] = await getJSON(
    //   `https://restcountries.eu/rest/v2/name/${c1}`
    // );
    // const [data2] = await getJSON(
    //   `https://restcountries.eu/rest/v2/name/${c2}`
    // );
    // const [data3] = await getJSON(
    //   `https://restcountries.eu/rest/v2/name/${c3}`
    // );
    // console.log([data1.capital, data2.capital, data3.capital]);
	//使用all
    const data = await Promise.all([
      getJSON(`https://restcountries.eu/rest/v2/name/${c1}`),
      getJSON(`https://restcountries.eu/rest/v2/name/${c2}`),
      getJSON(`https://restcountries.eu/rest/v2/name/${c3}`),
    ]);
    console.log(data.map(d => d[0].capital));
  } catch (err) {
    console.error(err);
  }
};
get3Countries('portugal', 'canada', 'tanzania');

Promise.race()

“竞赛”
接收一组promise,返回一个promise,谁来的快返回谁,不管返回的是成功还是失败

// Promise.race
(async function () {
  const res = await Promise.race([
    getJSON(`https://restcountries.eu/rest/v2/name/italy`),
    getJSON(`https://restcountries.eu/rest/v2/name/egypt`),
    getJSON(`https://restcountries.eu/rest/v2/name/mexico`),
  ]);
  console.log(res[0]);
})();

//定义一个错误信息函数
const timeout = function (sec) {
  return new Promise(function (_, reject) {
    setTimeout(function () {
      reject(new Error('Request took too long!'));
    }, sec * 1000);
  });
};

//错误信息也会被返回
Promise.race([
  getJSON(`https://restcountries.eu/rest/v2/name/tanzania`),
  timeout(5),
])
  .then(res => console.log(res[0]))
  .catch(err => console.error(err));

Promise.allSettled()

与all方法不同的是这个方法不论成功失败都会返回所有结果,无短路特性

Promise.allSettled([
  Promise.resolve('Success'),
  Promise.reject('ERROR'),
  Promise.resolve('Another success'),
]).then(res => console.log(res));

Promise.all([
  Promise.resolve('Success'),
  Promise.reject('ERROR'),
  Promise.resolve('Another success'),
])
  .then(res => console.log(res))
  .catch(err => console.error(err));

Promise.any()

接收多个Promise,会返回第一个resolve的Promise,与race类似,但是他会忽略被reject的请求,除非所有都是reject

// Promise.any [ES2021]
Promise.any([
  Promise.resolve('Success'),
  Promise.reject('ERROR'),
  Promise.resolve('Another success'),
])
  .then(res => console.log(res))
  .catch(err => console.error(err));
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值