异步
首先我们需要了解:
JS 是单线程语言,一次只能干一件事(多线程执行效率快但是开销性能也多)
ps.进程是计算机中正在运行的程序的实例
ps.线程是进程内部的执行单元,是程序执行流的最小单元。一个进程可以包含多个线程
了解完毕,我们来看一个代码:
let a = 1
console.log(a);
setTimeout(() => { //时间器回调函数,模拟耗时的代码,1s之后再执行
console.log(a);
}, 1000)
这里会先执行第一个输出,在执行第二个输出:
这样的执行顺序的原理是什么呢?
JS 是单线程语言,遇到需要耗时执行的代码,会将其先挂起,等到后续不耗时的代码执行完毕后,再回过头来执行耗时的代码。
我们再来实验一次,这里把回调函数放在全文中间:
let a = 1
console.log(a);
setTimeout(() => { //时间器回调函数,模拟耗时的代码,1s之后再执行
a = 2
console.log(a);
}, 1000)
console.log(a);
可以看到,这个回调函数的 a = 2
和 console.log(a)
会最后执行。
在 JS 中,异步是指将耗时的代码挂起并放入任务队列,然后优先执行后面不耗时的代码,而非严格按照代码的书写顺序依次执行。
这里接收了一个 a = 2
,最后想要输出刚刚接收到的 a = 2
,那这里这样做,可以实现吗?
let a = 1
console.log(a);
setTimeout(() => {
a = 2
}, 1000)
console.log(a);
很明显,不行,这里 a = 2
是最后执行的。
我们可以把它提炼成这样一个代码:
function a() {
setTimeout(() => {
console.log('a 执行完毕');
}, 1000)
}
function b() {
console.log('b 执行完毕');
}
a()
b()
来看看它的执行结果,还是 b()
先执行。
那我们这里要先让 b()
执行,再让 a()
执行,要怎么做?也就是怎么解决异步?
解决异步
1. 回调
首先,第一个方法就是回调函数。
//接收一个实参 b
function a(cb) {
setTimeout(() => {
console.log('a 执行完毕');
cb()
}, 1000)
}
function b() {
console.log('b 执行完毕');
}
a(b)
来看看结果,可以看到确实是 a 执行之后,b 才执行。
如果我们这里,b 的执行需要 a 的值,c 的执行需要 b 的值,d 的执行需要 c 的值,那我们这里就需要,一直往函数里增加形参:
function a(cb, cc, cd) {
setTimeout(() => {
console.log('a 执行完毕');
cb()
cc()
cd()
}, 1000)
}
function b(cc, cd) {
setTimeout(() => {
console.log('b 执行完毕');
cc()
cd()
}, 1500)
}
function c(cd) {
setTimeout(() => {
console.log('c 执行完毕');
cd()
}, 500)
}
function d() {
console.log('d 执行完毕');
}
a(b, c, d)
使用这个方法,虽然解决了代码的异步问题,但是同时也带来一个问题,回调地狱。
回调地狱:代码嵌套过深,维护成本大,一旦出现问题很难排查
2. promise
第二种方法。也就是今天的主角,Promise 。
我们这里来举个例子:
function pw() {
setTimeout(() => {
console.log('排位上分了');
}, 2000)
}
function lp() {
console.log('打完啦');
}
pw()
lp()
我们想要的顺序是,“排位上分了”,接着是“打完了”。但是这里因为异步问题,输出不是这样子的。
那我们这里来使用 promise
,来实现我们想要的输出:
function pw() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('排位上分了');
resolve() //开关,over
}, 2000)
})
}
function lp() {
console.log('打完啦');
}
pw().then(() => {
lp()
})
代码里的也就是 promise 的使用方法,
函数 pw
返回一个新创建的 Promise
对象。
promise 有三种状态:
pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)
在 Promise
的构造函数中,传入一个回调函数,该回调函数接收两个参数 resolve
和 reject
,它们是用于改变 Promise
状态的函数。这里通过 setTimeout
函数来模拟一个异步操作,设置了一个 2 秒的延迟。
当延迟 2 秒结束后,会在控制台输出 排位上分了
,然后调用 resolve
函数。这会将 Promise
的状态从 pending
转变为 fulfilled
,表示异步操作成功完成,并且可以继续执行后续通过 then
方法绑定的回调函数。
=====================================================
介绍完 pw
函数,那我们来捋一捋这个代码的执行流程:
首先调用 pw
函数,这会立即返回一个 Promise
对象,并开始执行其内部的异步操作(即等待 2 秒)。然后通过调用 Promise
对象的 then
方法,传入一个回调函数。当 pw
函数内部的 Promise
状态变为 fulfilled
(也就是 resolve
被调用后),就会执行 then
方法中传入的这个回调函数。
在 pw
的 Promise
状态变为 fulfilled
后,then
方法中的回调函数会被执行,这里面调用了 lp
函数,所以会在控制台输出 打完啦
。
拓展一下,resolve()
里面可以放东西,就像下面这样子,这里后面用 res
来接收:
resolve
函数就像一个开关。调用resolve
函数,就相当于告诉这个Promise
相关的流程:“嘿,我这边的异步任务已经顺利搞定啦”。若未调用 resolve 函数,那么后续的.then 语句将不会被执行。
function pw() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('排位上分了');
resolve('太有鸡蛋了') //开关,over
}, 2000)
})
}
function lp() {
console.log('打完啦');
}
pw().then((res) => {
console.log(res);
lp()
})
这里也是成功输出了 resolve()
的内容:
那我们这里再增加一个函数,要怎么做呢?
function pw() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('排位上分了');
resolve('太有鸡蛋了') //开关,over
}, 2000)
})
}
function lp() {
setTimeout(() => {
console.log('打完啦');
}, 2000)
}
function sf() {
console.log('今天上了三颗星');
}
pw().then((res) => {
console.log(res);
lp()
sf()
})
输出显然不符合我们的预期。
这里函数 lp
也返回一个新创建的 Promise
对象:
function pw() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('排位上分了');
resolve('太有鸡蛋了') //开关,over
}, 2000)
})
}
function lp() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('打完啦');
resolve() //开关,over
}, 2000)
})
}
function sf() {
console.log('今天上了三颗星');
}
pw().then((res) => {
console.log(res);
lp().then((res) => {
sf()
})
})
可以看到输出是我们想要的顺序:
===================================================
我们为了美观,把下面这段注释的代码,修改成这个样子,这也就是 then 的链式调用。
// pw().then((res) => {
// console.log(res);
// lp().then((res) => {
// sf()
// })
// })
//then的链式调用
pw()
.then((res) => {
return lp() //把又一个promise的实例对象给了.then
})
.then(() => {
sf()
})
==================================================
简单看下面这个代码,和前面的差不多,模拟了一个异步操作,用 promise 解决异步问题:
function a() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('a');
resolve('a 执行完毕');
}, 1000);
});
}
//.then放在了Promise的原型上
a()
.then(res =>{
console.log(res);
})
那如果我们在这里使用 reject() 呢?会怎么样?
function a() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('a');
// resolve('a 执行完毕');
reject('a 执行失败');
}, 1000);
});
}
a()
.then(res =>{
console.log(res);
})
报错了,“UnhandledPromiseRejectionWarning
” 明确提示这是一个未处理的 Promise
拒绝情况的警告。
所以这里应该添加一个 catch
方法来捕获并处理这个 rejected
状态:
function a() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('a');
// resolve('a 执行完毕');
reject('a 执行失败');
}, 1000);
});
}
//.then放在了Promise的原型上
a()
.then(res =>{ //开关开了,里面的才会执行
console.log(res);
})
.catch(err =>{ //捕获错误
console.log(err);
})
这里执行成功。
OK啊,你已成功习得运用 Promise 来妥善处理异步难题的诀窍。
=====================================================
接下来我们来在实战中解决异步问题:
给一个网址,要求先访问这个网址,再输出里面的电影名字到网页上。 这个网址里的数据是这些:
代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul id="ul"></ul>
<!-- 这里定义了一个空的无序列表,用于后续展示获取到的电影列表数据 -->
<script>
// 定义函数getData,用于发送接口请求并获取数据
function getData() {
// 发接口请求
return new Promise((resolve, reject) => {
// 创建一个XMLHttpRequest对象,用于发送HTTP请求
let xhr = new XMLHttpRequest();
// 配置请求,设置请求方法为GET,指定请求的URL,true表示异步请求
xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
// 发送请求
xhr.send();
// 监听XMLHttpRequest对象的状态变化
xhr.onreadystatechange = function () {
// 当请求状态为4(表示请求已完成)且状态码为200(表示请求成功)时
if (xhr.readyState === 4 && xhr.status === 200) {
// 将响应文本解析为JSON格式,并在控制台打印出来
console.log(JSON.parse(xhr.responseText));
// 从解析后的JSON数据中提取电影列表数据(假设接口返回的数据结构中有movieList字段包含电影列表信息)
// 并通过resolve将电影列表数据传递出去,使Promise状态变为fulfilled,以便后续处理
resolve(JSON.parse(xhr.responseText).movieList)
}
}
});
}
// 定义函数showList,用于展示获取到的电影列表数据
function showList(data) {
// 遍历传入的电影列表数据数组
data.forEach(item => {
// 为每个电影数据项创建一个新的<li>元素
let li = document.createElement('li');
// 将电影名称(假设每个电影数据项有nm字段表示电影名称)设置为<li>元素的文本内容
li.innerText = item.nm;
// 将创建好的<li>元素添加到页面上id为"ul"的无序列表中
document.getElementById('ul').appendChild(li);
});
// console.log(`拿到了数据${data}`);
}
// 调用getData函数发起接口请求,返回一个Promise对象
getData()
// 当Promise状态变为fulfilled(即接口请求成功并获取到数据)时,执行这个回调函数
.then(res => {
// 将获取到的电影列表数据传递给showList函数进行展示
showList(res)
});
</script>
</body>
</html>
这里附上了一些注释,有些内容或许还是看不懂,没关系,我们只需要知道在真正的项目中,Promise 是这样子使用的就可以了。
这里也是成功的拿到了 nm
也就是电影名: