Promise简介
Promise 也称期约, 是ES6推出的一个异步解决方案, 可以有效的解决异步函数嵌套太深(“回调地狱”)的问题
什么是回调地狱?
假设有个需求需要获取用户的指定数据用户数据3这个数据依赖用户数据2而用户数据2又依赖于用户数据1, 所以正确的数据获取数据顺序为: 用户数据1–>用户数据2–>用户数据3, 模拟一下使用回调函数的写法如下:
Node接口:
const router = require('express').Router();
const data1 = { data: "用户数据1" };
router.get('/testData1', (req, res) => {
res.json(data1);
})
const data2 = { data: "用户数据1,用户数据2" };
router.get('/testData2', (req, res) => {
if (req.query.data === data1.data) {
res.json(data2);
} else {
res.status(401).json("参数错误");
}
})
router.get('/testData3', (req, res) => {
if (req.query.data === data2.data) {
res.json({ data: "用户数据1,用户数据2,用户数据3" });
} else {
res.status(401).json("参数错误");
}
})
module.exports = router;
前端请求代码:
const baseUrl = "http://localhost:8888/test";
const request = (url, cb) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', baseUrl + url);
xhr.send();
xhr.onreadystatechange = () => {
const { readyState, status, response } = xhr;
if (readyState === 4) {
if (status >= 200 && status <= 299) {
cb(JSON.parse(response));
} else {
throw new Error(response);
}
}
}
}
request("/testData1", res1 => {
console.log(res1);
request(`/testData2?data=${res1.data}`, res2 => {
console.log(res2);
request(`/testData3?data=${res2.data}`, res3 => {
console.log("需求需要的数据", res3);
})
})
})
这个代码看着就头大, 如果需求复杂的话, 我只能用一张图表示(这张图片我也不记得从哪里保存的忘了😂)
这种一个回调嵌套一个回调的代码可读性和可维护性都很差, 被称为"回调地狱", 而Promise 的出现就可以很好的解决这个问题
Promise的特点
Promise 对象有一个状态, 这个状态不受外界影响, 状态一共分为3种:
Pending 状态 (进行中(又称待定)) 初始状态Fulfilled 状态 (成功(又称兑现))Rejected 状态(失败(又称拒绝))
关于Promise的状态叫法下文统一使用成功,失败
一旦Promise对象的状态改变(成功或失败)就不会再变, 是单向的:
- Pending(进行中) -> Fulfilled(成功)
- Pending(进行中) -> Rejected(失败)
创建Promise实例
创建Promise 实例需要new Promise 构造函数, 该构造函数接受一个函数(处理器函数)作为参数, 该函数会收到两个参数, 这两个参数分别是resolve和reject(叫什么都行一般还是要语义化名称)它们是两个函数, 不用自己实现就可以使用, 如下:
const p = new Promise((resolve, reject) => {
})
console.log(p);
可以看到这个状态默认是Pending(进行中)
resolve 和reject 这两个参数(函数)可以在处理器函数里面调用(可以传递参数), 这样将会改变 Promise 对象的状态:
const p = new Promise((resolve, reject) => {
resolve();
})
console.log(p);
当调用resolve函数 时会将Priomise对象的状态修改为Fulfilled(成功), 调用reject函数 则会将状态修改为Rejected(失败)
then方法
Promise实例的then 方法, 可以接受两个参数(都是函数)分别指定Primise实例里面状态(成功或失败)改变时调用的回调函数(并且Promise的then方法是异步的微任务):
const p = new Promise((resolve, reject) => {
resolve();
})
console.log("同步代码");
p.then(
() => {
console.log("成功的回调");
},
() => {
console.log("失败的回调");
}
)
结果如下:
反之调用reject函数 就会触发then 方法的第二个回调函数, 如果将resolve函数 和reject函数 都调用只会生效最先调用的(因为状态时单向的嘛)
resolve 和 reject 的参数传递
经过上面的测试我们知道resolve 和reject 这两个函数是可以修改状态并且触发Promise的then 方法的回调函数的, 那么是函数就可以传递参数, 这个参数会被传递给对应的then方法的回调函数接收到:
const p = new Promise((resolve, reject) => {
resolve("ok");
})
console.log("同步代码");
p.then(
res => {
console.log("成功的回调", res);
},
err => {
console.log("失败的回调");
}
)
结果如下:
then()链式调用
了解完then方法以后我们就可以稍微修改一下一开始最上面的需求:
const baseUrl = "http://localhost:8888/test";
const request = (url) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', baseUrl + url);
xhr.send();
xhr.onreadystatechange = () => {
const { readyState, status, response } = xhr;
if (readyState === 4) {
if (status >= 200 && status <= 299) {
const res = JSON.parse(response);
resolve(res);
} else {
reject(response);
}
}
}
})
}
request("/testData1").then(
res1 => {
console.log(res1);
request(`/testData2?data=${res1.data}`).then(
res2 => {
console.log(res2);
request(`/testData3?data=${res2.data}`).then(
res3 => {
console.log("需求需要的数据", res3);
}
)
}
)
}
)
写完以后发现好像还不如使用回调函数的方式写, 看到这里好像Promise还是不能很好的解决回调嵌套的问题; 换个思路如果我们在then 方法中再返回一个Promise实例, 那么不就又可以调用then 方法了吗? 代码中测试一下:
const p1 = new Promise((resolve, reject) => {
resolve("p1数据");
})
const p2 = p1.then(
res1 => {
console.log(res1);
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(res1 + ",p2数据");
}, 500);
})
return p2;
}
)
const p3 = p2.then(
res2 => {
console.log(res2);
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(res2 + ",p3数据");
}, 500);
})
return p3;
}
)
p3.then(
res3 => {
console.log(res3);
}
)
发现可行后, 把需求代码再修改一下:
const p1 = request("/testData1");
const p2 = p1.then(
res1 => {
console.log(res1);
const p2 = request(`/testData2?data=${res1.data}`);
return p2;
}
)
const p3 = p2.then(
res2 => {
console.log(res2);
const p3 = request(`/testData3?data=${res2.data}`);
return p3;
}
)
p3.then(
res3 => {
console.log("需求需要的数据", res3);
}
)
需求实现是实现了, 就是代码有点冗余, 可以精简一下, 如下:
request("/testData1").then(
res1 => {
console.log(res1);
return request(`/testData2?data=${res1.data}`);
}
).then(
res2 => {
console.log(res2);
return request(`/testData3?data=${res2.data}`);
}
).then(
res3 => {
console.log("需求需要的数据", res3);
}
)
上面的代码格式就像链条一样所以又被称为"链式调用"
then()的返回值
经过上面的代码测试, then 方法除了返回Promise对象外还可以返回其他任意的值, 返回会被转换为Promise对象, 内部的状态视返回值而定:
const p1 = new Promise(resolve => resolve());
p1.then(() => {
}).then(
res1 => {
console.log(res1);
return "hello";
}
).then(
res2 => {
console.log(res2);
return { name: "张三" };
}
).then(
res3 => {
console.log(res3);
return new Error("error object");
}
).then(
res4 => {
console.log(res4 instanceof Error);
console.log(res4.message);
throw "thorw error";
}
).then(
() => { },
err => {
console.log(err);
}
)
catch方法
上面使用then 方法的链式调用可以解决回调嵌套太深的问题, 但是还没处理请求之间的失败回调处理, then 方法的第二个回调就是指定失败的回调, 但是一般都不使用这个回调来处理错误, 而是使用catch 方法来失败, 使用格式如下:
p1.then(
).then(
).catch(err => {
})
finally方法
除了catch 方法那自然就有finally 方法, finally 方法和try...catch...finally 中的finally 是一样的作用, 使用格式如下:
p.then(
).then(
).catch(err => {
}).finally(() => {
})
Promise的方法
Promise.resolve()
立即返回一个状态是成功(Fulfilled) 的 Promise 对象, 可以传递参数, 参数会被其返回的Promise实例的then方法的回调(异步微任务)接受到:
const p = Promise.resolve("Promise.resolve");
p.then(res => console.log(res));
也可以利用Promise.resolve()来创建一个微任务:
console.log("同步代码");
setTimeout(() => console.log("setTimeout"), 0);
const p = Promise.resolve("Promise.resolve");
p.then(res => console.log(res));
结果如下:
Promise.reject()
和Promise.resolve() 一样不过返回的状态是失败(Rejected) 的Promise对象(同样是微任务)
console.log("同步代码");
setTimeout(() => console.log("setTimeout"), 0);
const p = Promise.reject("Promise.reject");
p.then().catch(err => console.log("Promise.reject"));
Promise.all()
Promise.all 接收一个Promise的iterable类型(就是可迭代对象里面存放着Promise实例, Array, Map, Set都属于ES6的iterable类型), Promise.all 会等待所有的Promise对象都完成(或第一个失败) , 根据给定的参数返回不同的参数
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("p1 data"), 500);
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => resolve("p2 data"), 1000);
})
const p3 = new Promise((resolve, reject) => {
setTimeout(() => resolve("p3 data"), 1500);
})
p1.then(res => console.log(res));
p2.then(res => console.log(res));
p3.then(res => console.log(res));
const proArray = [p1, p2, p3];
Promise.all(proArray).then(resList => {
console.log(resList);
}).catch(err => {
console.error("error: ", err);
})
利用Promise.all() 方法的特定可以用于同时发送多个请求, 如下例子:
Node接口:
const getRandom = () => Math.random() * 9 + 1;
router.get('/testData4', (req, res) => {
let n = req.query.n;
const random = getRandom();
setTimeout(() => {
res.json(`第${++n}个请求${random}`);
}, random * 50);
});
前端发送多个请求:
const proArray = [];
for (let i = 0; i < 10; i++) {
proArray.push(request(`/testData4?n=${i}`));
}
Promise.all(proArray).then(resList => {
for (const res of resList) {
console.log(res);
}
})
Promise.allSettled()
Promise.allSettled() 方法和Promise.all() 很类似只不过是接受的期约对象无论是成功还是失败都会触发then方法的成功回调, 每个期约都会返回一个对象status属性表示状态, value表示成功回调的值, 如果状态是失败的那么失败回调的值存储在reason属性中:
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("p1 data"), 500);;
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => reject("p2 err"), 1000);
})
const proArray = [p1, p2];
Promise.allSettled(proArray).then(resList => {
console.log(resList);
for (const res of resList) {
const { status, value, reason } = res;
if (reason) {
console.log(`失败: ${status}, 原因是: ${reason}`);
} else {
console.log(`成功: ${status}, 数据是: ${value}`);
}
}
}).catch(err => {
console.error("error: ", err);
})
Promise.race()
Promise.race() 和Promise.all() 类似, 都接收一个可以迭代的参数, 但是不同之处是 Promise.race()的状态变化不是受全部参数的状态影响, 一旦迭代器中的某个Promise解决或拒绝,返回的 Promise就会解决或拒绝
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("p1 data"), 500);;
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => reject("p2 err"), 1000);
})
const proArray = [p1, p2];
Promise.race(proArray).then(res => {
console.log(res);
}).catch(err => {
console.error(err);
})
async 和 await
async函数
async函数 就是使用async 关键字声明的函数(也叫异步函数), async函数和普通的函数使用没有什么区别:
async function asyncFn1() {
console.log("asyncFn1");
}
const asyncFn2 = async () => {
console.log("asyncFn2");
}
asyncFn1();
asyncFn2();
(async () => {
console.log("asyncFn3");
})();
await
await操作符用于等待一个Promise对象, 它只能在async function中使用, 使用async+await可以将异步的代码"变"的跟同步的一样:
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("data");
}, 1000);
});
async function asyncFn() {
console.log("asyncFn函数开始执行");
const res = await p;
console.log(res);
console.log("asyncFn函数执行完了");
}
asyncFn();
上面的代码会先输出"asyncFn函数开始执行"后, 等待1s左右输出"data", 然后再输出"asyncFn函数执行完了"
注意: 异步函数不会阻塞主线程的执行, 它是异步的:
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("data");
}, 1000);
});
async function asyncFn() {
console.log("asyncFn函数开始执行");
const res = await p;
console.log(res);
console.log("asyncFn函数执行完了");
}
console.log("hello");
asyncFn();
console.log("javascript");
等待大约1s后上面的代码执行结果如下:
根据上面的代码执行结果, 我们发现异步函数内await关键字会等待其右边的Promise对象的返回值, 等待结束后, 后面的代码才会被执行, 利用这个特性我们可以在JavaScript中可以实现类似Java的Thread.sleep() 方法, 如下:
const sleep = async time => new Promise(resolve => setTimeout(resolve, time));
(async () => {
console.log("1");
await sleep(1000);
console.log("2");
await sleep(500);
console.log("2.5");
})();
了解完async 和await 的使用后我们可以使用异步函数再来完成我们一开始的需求:
const baseUrl = "http://localhost:8888/test";
const request = (url) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', baseUrl + url);
xhr.send();
xhr.onreadystatechange = () => {
const { readyState, status, response } = xhr;
if (readyState === 4) {
if (status >= 200 && status <= 299) {
const res = JSON.parse(response);
resolve(res);
} else {
reject(response);
}
}
}
})
}
async function asyncFn() {
const res1 = await request("/testData1");
console.log(res1);
const res2 = await request(`/testData2?data=${res1.data}`);
console.log(res2);
const res3 = await request(`/testData3?data=${res2.data}`);
console.log(res3);
}
asyncFn();
可以看到使用async 和await 来发送网络请求写的代码很简洁, 也很直观
异步函数的错误处理
异步函数的异常处理可以使用try...catch和catch 处理:
try…catch
async function asyncFn() {
let res1, res2, res3;
try {
res1 = await request("/testData1");
try {
if (res1?.data) {
res2 = await request(`/testData2?data=${res1.data}`);
}
try {
if (res2?.data) {
res3 = await request(`/testData3?data=${res2.data}`);
}
} catch (error) {
}
} catch (error) {
}
} catch (error) {
}
}
catch
async function asyncFn() {
const res1 = await request("/testData1")
.catch(err => {
});
if (res1?.data) {
const res2 = await request(`/testData2?data=${res1.data}`)
.catch(err => {
});
if (res2?.data) {
const res3 = await request(`/testData3?data=${res2.data}`)
.catch(err => {
});
}
}
}
异步函数同样适用于Promise的一些静态方法
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("p1 data"), 500);
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => resolve("p2 data"), 1000);
})
const p3 = new Promise((resolve, reject) => {
setTimeout(() => resolve("p3 data"), 1500);
})
const proArray = [p1, p2, p3];
async function asyncFn() {
const list = await Promise.all(proArray);
console.log(list);
}
asyncFn();
for await…of
一个数组中存储多个Promise对象我们可以通过Promise.all获取或者通过循环来等待其返回值, 我们使用循环:
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("p1 data"), 500);
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => resolve("p2 data"), 1000);
})
const p3 = new Promise((resolve, reject) => {
setTimeout(() => resolve("p3 data"), 1500);
})
const proArray = [p1, p2, p3];
async function asyncFn() {
for (const pro of proArray) {
const res = await pro;
console.log(res);
}
}
asyncFn();
ES9开始有一个新语法就是for await..of 可以自动等待每次循环的项
async function asyncFn() {
for await (const item of proArray) {
console.log(item);
}
}
asyncFn();
|