AJAX进阶
一、同步代码和异步代码
同步代码
逐行执行,需要原地等待结果后,才继续向下执行。
异步代码
调用后耗时,不阻塞代码继续执行,在将来完成后触发一个回调函数。
const result = 0 + 1;
console.log(result);
setTimeout(() => {
console.log(2);
}, 2000);
document.querySelector(".btn").addEventListener("click", () => {
console.log(3);
});
console.log(4);
先输出1和4,再在设置的定时器倒计时完之前,点击多少次按钮,就输出多少次3,当倒计时结束,执行定时器内的代码(输出2)。
JavaScript中常见的异步代码有:
setTimeout
/setInterval
事件
AJAX
【异步代码依靠回调函数接收结果】
二、回调地狱
先回顾一下回调函数:
当一个函数作为参数传入另一个参数中,并且它不会立即执行,只有当满足一定条件后该函数才可以执行,这种函数就称为回调函数。
例如:
定时器
setTimeout(function () {
console.log("这是回调函数");
}, 3000);
这里的function () {console.log("这是回调函数");}
就是回调函数。
AJAX
let xhr=new XMLHttpRequest();
xhr.onreadystatechange=function(){
// 此函数整个过程会被调用4次
if(xhr.readyState==4 && xhr.status==200){
//把响应数据存储到变量result中
let result=xhr.responseText;
console.log(result);
}
}
xhr.open("get","",true);
xhr.send();
这里xhr.onreadystatechange
绑定的函数就是回调函数。
回调函数地狱
实现代码顺序执行而出现的一种操作,它会造成我们的代码可读性非常差、异常捕获困难、耦合性严重,后期不好维护。
例如:
setTimeout(function () {
//第一层
console.log("我");
setTimeout(function () {
//第二程
console.log("们");
setTimeout(function () {
//第三层
console.log("是");
setTimeout(function () {
// 第四层
console.log("好");
setTimeout(function () {
// 第五层
console.log("友");
}, 1000);
}, 1000);
}, 1000);
}, 1000);
}, 3000);
那要怎么解决这种问题呢?
1、Promise链式调用
- 依靠 then() 方法会返回一个新生成的 Promise 对象特性,继续串联下一环任务,直到结束。
- then() 回调函数中的返回值,会影响新生成的 Promise 对象最终状态和结果。
例子:
function fn(str) {
let p = new Promise(function (resolve, reject) {
//处理异步任务
setTimeout(function () {
resolve(str);
});
});
return p;
}
fn("我们")
.then((data) => {
console.log(data);
return fn("是");
})
.then((data) => {
console.log(data);
return fn("好友");
})
.then((data) => {
console.log(data);
});
Promise链式调用也是容易造成代码冗余,不管什么操作都用then,一眼看过去全是then,这样也不利于代码维护的。
所以看下面的async
和await
。
2、async和await使用
运用这两个关键字以一种更简洁的方式写出基于Promise
的异步代码,无需刻意链式调用Promise
。
async
async
关键字放在函数面前,可以表明这个函数是执行异步任务的,不阻塞代码往下面执行。
async function fn() {
return "你好";
}
console.log(fn());
可以看到,async函数返回值是一个Promise对象。
既然如此,那应该也可以像Promise对象一样,按照成功和失败来返回不同的数据,处理成功时用then方法来接收,失败时用catch方法来接收数据:
async function fn() {
const flag = true;
if (flag) {
return "处理成功";
} else {
throw "处理失败";
}
}
fn()
.then((result) => {
console.log(result);
})
.catch((result) => {
console.log(result);
});
console.log("先执行,表明async声明的函数是异步的");
把flag改为false:
await
- await关键字只能在使用async定义的函数中使用
- await后面可以直接跟一个 Promise实例对象(可以跟任何表达式,更多的是跟一个返回Promise对象的表达式)
- await函数不能单独使用
- await可以直接拿到Promise中resolve中的数据。
function fn(str) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
if (str) {
resolve(str);
} else {
reject("处理失败");
}
});
});
}
//封装一个执行上述异步任务的async函数
async function demo() {
const res1 = await fn("我们"); //await直接拿到fn()返回的promise的数据,并且赋值给res
const res2 = await fn("是");
const res3 = await fn("好友");
console.log(res1, res2, res3);
}
//执行函数
demo();
在 async 函数内,使用 await 关键字取代 then 函数,等待获取 Promise 对象成功状态的结果值。
当代码执行到async
函数中的await
时,代码就在此处等待不继续往下执行,直到await
拿到Promise对象中resolve的数据,才继续往下执行,这样就保证了代码的执行顺序,使异步代码看起来更像同步代码。
错误捕获
如果我们在上面async demo
函数中再加上一行:
const res4 = await fn("");
运行后会报错:
并且不会输出res1、res2、res3。
所以我们需要进行错误捕获,使用try-catch
来进行。
将async demo
改成:
async function demo() {
const res1 = await fn("我们");
const res2 = await fn("是");
const res3 = await fn("好友");
let res4;
try {
res4 = await fn("");
} catch (e) {
res4 = e;
}
console.log(res1, res2, res3, res4);
}
思考:但是难道每个await都要使用
try-catch
包围吗?会不会显得代码更加冗余。
三、事件循环-EventLoop
事件循环
JavaScript中有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理时间以及执行队列中的子任务。
因为JS是单线程的,为了不让耗时代码阻塞其它代码执行,设计了事件循环模型。
这一点在BOM操作-CSDN博客有提到过,同样在该文章中,有提到过JS执行机制:
- 逐行执行同步代码,遇到异步代码就交给宿主浏览器环境执行。
- 异步有了结果后,把回调函数放入任务队列排队。
- 当调用栈 空闲后,反复调用任务队列里的回调函数。
而事件循环就是将任务队列的任务调用到空闲的调用栈中执行【执行代码和收集异步任务】,注意这个过程是反复执行的。
宏任务与微任务
异步任务就是分为宏任务与微任务。
宏任务
由浏览器环境执行的异步代码。
常见的有:
JS脚本执行事件(Script)、setTimeout/setInterval、AJAX请求完成事件、用户交互事件等
微任务
由JS引擎环境执行的异步代码。
常见的有:
Promise.then()/catch()
Promise本身是同步的,但是then和catch回调函数是异步的。
微任务空闲后才会执行宏任务。
所以可以更新一下执行代码的顺序:
- 执行同步代码。
- 遇到宏任务就交给浏览器环境执行,遇到微任务就交给JS引擎环境执行。
- 异步有了结果后,把回调函数放入任务队列(宏、微)排队。
- 当调用栈空闲后,先清空微任务队列,然后再执行下一个宏任务,再重复以上步骤。
练习
// 目标:回答代码执行顺序
console.log(1)
setTimeout(() => {
console.log(2)
const p = new Promise(resolve => resolve(3))
p.then(result => console.log(result))
}, 0)
const p = new Promise(resolve => {
setTimeout(() => {
console.log(4)
}, 0)
resolve(5)
})
p.then(result => console.log(result))
const p2 = new Promise(resolve => resolve(6))
p2.then(result => console.log(result))
console.log(7)
代码应该输出:1 7 5 6 2 3 4
四、Promise.all 静态方法
概念
合并多个Promise对象,等待所有同时成功完成(或某一个失败),做后续逻辑。
语法
const p = Promise.all([Promise对象,Promise对象...]);
p.then(result =>{
// result结果是:[Promise对象成功结果,Promise对象成功结果...]
}).catch(e => {
// 第一个返回失败的promise对象,抛出的异常
})
let p1 = new Promise((resolve, reject) => {
resolve('成功了')
})
let p2 = new Promise((resolve, reject) => {
resolve('success')
})
let p3 = Promise.reject('失败')
Promise.all([p1, p2]).then((result) => {
console.log(result) // ['成功了', 'success']
}).catch((error) => {
console.log(error)
})
Promise.all([p1,p3,p2]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 失败了,打出 '失败'
})
该方法用于将多个Promise实例,包装成一个新的Promise实例。