概念
我们平时写的代码:为同步代码,逐行进行
缺点:在遇到阻塞代码时,其他代码无法被执行,整个页面被“卡住”
异步代码:如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有以下几个优点:
- 可读性更高:使用Promise可以以更干净、更易读的方式处理加载图片的异步行为。在处理复杂代码(包含多个异步操作)时特别有用。
- 错误处理:使用Promise可以以集中的方式捕获和处理加载图片时发生的错误。如果发生错误,Promise将被拒绝,并传递“图片未找到”的错误消息,您可以使用.catch()方法处理该错误。
- 顺序执行:使用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));