我们知道,任何一门编程语言,代码都是从上往下执行的。有些代码不耗时执行,而有些代码需要耗时执行。当这些代码从上往下执行时,就会带来一些问题。究竟是不耗时的代码先执行,还是耗时的代码先执行呢?
今天,我们就来探讨一下这个问题。在下文中,我们统一使用定时器模拟需要耗时的代码。
1.异步
我们先来看一段代码:
let a = 1
console.log(a);
setTimeout(() => {
console.log(a);
}, 1000)
我们定义一个a为1,然后输出a,又写了一个定时器模拟耗时执行的代码,在1秒过后再次输出a。
这段代码输出结果应该是先输出一个1,等1秒过后,再次输出1。这很容易接受,因为代码是从上往下执行的。
let a = 1
console.log(a, 2);
setTimeout(() => {
a = 2
console.log(a, 6);
}, 1000)
console.log(a, 9);
我们在定时器里将a的值改为了2,在定时器的下面也输出一下a。这段代码就变成了一段不耗时代码、一段耗时代码、一段不耗时代码。它的执行顺序会是什么呢?是依然从上往下执行,碰到耗时1秒的代码先将耗时代码执行完毕再执行下面的代码,还是碰到耗时的代码先搁置,先运行完不耗时的代码再运行耗时的代码呢?
我们输出a的时候顺便输出它的行数,这样看的更清楚。如果依然从上往下执行,那就是2、6、9的顺序输出。如果是耗时代码先搁置,那就是2、9、6的顺序输出。我们来看一下输出结果。
我们发现是2、9、6的顺序。这说明在JS中,当碰到需要耗时执行的代码时,它会先搁置,先执行完不耗时的代码,再来执行耗时的代码。
为什么要这样设计呢?为什么不能碰到耗时的代码,我就按顺序执行,执行完耗时的代码再执行下面的代码。
因为这样设计的话代码执行效率会大大降低。假如在这段耗时代码的后面还有几千行代码,那后面几千行代码就会被你活活拖累一段时间,而后面要执行的代码可能和你这段耗时的代码没太大关系,那执行效率不就变慢了。
而又因为JS是一门单线程语言,它不能像Java一样碰到耗时的代码,可以一边执行耗时代码一边执行不耗时的代码。它一次性只能干一件事情,所以当碰到耗时的代码时,它会先将耗时的代码挂起,它会维护一个队列来存放耗时代码,先去执行不耗时的代码,再来执行耗时的代码。
那这就会带来一个问题。如果我就是想让耗时的代码先执行呢?在实际开发过程中,有些代码的耗时是无法避免的,比如前端向后端发送一个http请求,它就得先执行,但因为JS会将耗时的代码先挂起之后再去执行,那就造成了代码的不同步执行,这就是代码的异步执行了。
所以我们得去解决这个异步问题。
2.回调函数解决异步
在es6之前,代码异步执行的问题就存在,当时的人们就是用回调函数解决异步问题的。我们来看一下回调函数是怎么解决异步的。
function a() {
setTimeout(() => {
console.log('a 执行完毕');
}, 1000)
}
function b() {
console.log('b 执行完毕');
}
a()
b()
我们定义一个函数a,里面用一个定时器模拟需要耗时执行的代码,如果函数a被调用了,它会在1秒钟后输出'a 执行完毕'。我们还定义了一个函数b,它输出'b 执行完毕'。然后我们先调用a再调用b,按照JS的特性,它会异步执行。那就会先输出'b 执行完毕'再输出'a 执行完毕'。
那如果我就是要让函数a先执行呢?这在实际开发中太常见了。因为有些代码就是不可避免的需要耗时执行,但它又得先执行。那我们可以用回调函数来解决。
我们直接将函数b的调用放到函数a中console.log('a 执行完毕')的后面,就行了。
function a() {
setTimeout(() => {
console.log('a 执行完毕');
b()
}, 1000)
}
function b() {
console.log('b 执行完毕');
}
a()
我们人为的让a执行完毕后再去调用b,这样只有等我a执行完了你b才能调用。但我们一般这样写,给函数a定义一个形参cb,然后调用的时候将b作为实参传进来调用。
function a(cb) {
setTimeout(() => {
console.log('a 执行完毕');
cb()
}, 1000)
}
function b() {
console.log('b 执行完毕');
}
a(b)
这样我们就让a的执行在b的前面。这就叫回调,回过头来调用。
这种方法能很好的解决异步的问题,但是它会有个问题,当碰到很多个函数需要互相依赖调用时,嵌套关系就会变得很复杂。
比如,我们有四个耗时的函数a、b、c、d。我希望函数b的执行必须依赖函数a的执行结果,函数c的执行必须依赖函数b的执行结果,又希望函数d的执行必须依赖函数c的执行结果。那我们就只能这样写:
function a(cb, cb2, cb3) {
setTimeout(() => {
console.log('a 执行完毕');
cb(cb2, cb3)
}, 1000)
}
function b(cb, cb3) {
setTimeout(() => {
console.log('b 执行完毕');
cb(cb3)
}, 1500)
}
function c(cb) {
setTimeout(() => {
console.log('c 执行完毕');
cb()
}, 500)
}
function d() {
console.log('d 执行完毕');
}
a(b,c,d)
函数之间的嵌套关系就会变得非常复杂。这只是四个函数之间互相等待,要是有100个函数之间互相等待,那嵌套关系简直不敢想象。业内把这一现象称作回调地狱。
后来为了应对在这一现象,es6新增了一个解决异步的方法——promise。
3.Promise
我们来看看promise解决异步问题的语法。
我们依旧用定时器模拟需要耗时执行的代码。
我们先来定义一个相亲函数xq,需要耗时2秒执行:
function xq() {
setTimeout(() => {
console.log('章总相亲了');
}, 2000)
}
再来定义一个结结婚函数marry,需要耗时1秒执行:
function marry() {
setTimeout(() => {
console.log('章总结婚了');
}, 1000)
}
正常去调用这两个函数的话marry函数就会先执行,因为它耗时更短。
那这样就不是我们想要的结果,应该是先相亲再结婚才对。我们应该要让函数xq先执行。
除了上一种提到的回调函数解决外,还可以用promise方法解决,我们来看一下它是怎么使用的。
在xq函数里我们return 一个new Promise,它接收一个函数体,函数里面有两个形参:resolve和reject。然后我们把这个xq函数的过程,放到这个函数体里去。
function xq() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('章总相亲了');
}, 2000)
})
}
Promise显然是一个构造函数,我们用new去调用它,能得到一个实例对象,然后我们返回了它。那我们调用了函数xq,xq()就是一个实例对象,在这个对象上有一个方法then,这个then可以接收一个回调函数,我们可以将marry放到then里面来使用。
xq().then(() => {
marry()
})
但是这样还不够,我们还要在xq函数里resolve调用一下。resolve和reject就像两个开关,一个是执行成功,一个是执行失败。我们人为的去调用resolve,那整个promise就是成功的状态;人为的去调用reject,那整个promise就是失败的状态。只有在成功的状态下,then方法才会走。
整段代码:
function xq() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('章总相亲了');
resolve()
}, 2000)
})
}
function marry() {
setTimeout(() => {
console.log('章总结婚了');
}, 1000)
}
xq().then(() => {
marry()
})
这样就成功先相亲再结婚了。我们解决了代码异步问题。
这个resolve还有一个作用,它里面可以放一个值‘相亲顺利’,resolve出来的值可以被then拿到,我们可以输出这个值。
function xq() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('章总相亲了');
resolve('相亲顺利')
}, 2000)
})
}
function marry() {
setTimeout(() => {
console.log('章总结婚了');
}, 1000)
}
xq().then((res) => {
console.log(res);
marry()
})
了解完语法后,我们再来加一个函数baby,我们想让它在marry函数的后面执行。
function baby() {
console.log('小章出生了');
}
因为marry函数需要耗时执行,而baby函数不用,这两个函数就会异步执行。所以我们得在marry函数里也加一个promise。
function marry() {
return new Promise(() => {
setTimeout(() => {
console.log('章总结婚了');
}, 1000)
resolve()
})
}
此时我们就可以在marry()上调用then方法了。
xq().then((res) => {
console.log(res);
marry().then(() => {
baby()
})
})
还有另外一种写法,看起来更简洁。我们可以让then并列。
function xq() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('章总相亲了');
resolve('相亲顺利')
}, 2000)
})
}
function marry() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('章总结婚了');
resolve()
}, 1000)
})
}
function baby() {
console.log('小章出生了');
}
xq()
.then((res) => {
console.log(res);
return marry()
})
.then(() => {
baby()
})
我们在第一个then里return marry(),于是返回一个对象,我们就可以then后面再接then。
现在我们来看一下输出结果:
成功达到了我们想要的顺序。
我们再来看一下promise中关于reject的语法。刚刚我们看了resolve,它可以让整个promise执行成功,被then方法拿到。
function a() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('a');
resolve('a 执行完毕')
}, 1000)
})
}
a()
.then(res => {
console.log(res);
})
我们还可以用reject让整个promise执行失败,它会被catch方法拿到。
function a() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('a');
// resolve('a 执行完毕')
reject('a 执行失败')
}, 1000)
})
}
a()
.then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
})
这就是promise的一些语法,接下来我们来看一个promise的真实场景应用。
4.Promise的应用场景
我们来模拟一个前端向后端发送http请求要求数据的场景。
我们提前准备一个接口地址:'mock.mengxuegu.com/mock/65a915…' ,这是后端给前端提供的一个地址,当我们用前端代码往这个地址发送请求,我们就能请求到地址中的数据。
我希望能将地址中的数据展示到页面上。我们准备一个ul,到时候我们就将数据放到ul中的li中一行一行展示。给ul取一个id=“ul”。
<body>
<ul id="ul"></ul>
</body>
然后我们来写一段JS。在JS中,有专门一个方法能向后端发送http请求。
<body>
<ul id="ul"></ul>
<script>
let xhr = new XMLHttpRequest();
</script>
</body>
XMLHttpRequest 是JS内定的一个构造函数,我们new一下它得到了一个发送http请求的实例对象xhr。
然后xhr调用open方法,然后发送一个‘GET’请求,第二个参数就放我们的接口地址,第三个参数写一个‘true’,将这段代码变成异步执行。
<body>
<ul id="ul"></ul>
<script>
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
</script>
</body>
然后我们调用send方法,发射这个请求。
<body>
<ul id="ul"></ul>
<script>
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
xhr.send();
</script>
</body>
我们发送了这个请求,后端就会给我们返回数据,那我们就要接收它。接收用onreadystatechange 方法,监听状态的变更,它为一个函数体。
<body>
<ul id="ul"></ul>
<script>
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
xhr.send();
xhr.onreadystatechange = function () {
}
</script>
</body>
在这个函数里我们去判断一下。
<body>
<ul id="ul"></ul>
<script>
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
}
}
</script>
</body>
readyState 代表的是状态,status 是http的状态码。这个在这里先不细聊。
我们在里面输出xhr.responseText。responseText是后端传来的数据的字符串格式,它是JSON字符串。我们将它转换成JSON对象。
<body>
<ul id="ul"></ul>
<script>
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(JSON.parse(xhr.responseText));
}
}
</script>
</body>
我们现在写的就是发送请求获取数据的代码,所以我们写一个函数getData将它们包裹起来,分门别类。
<body>
<ul id="ul"></ul>
<script>
function getData() {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(JSON.parse(xhr.responseText));
}
}
}
</script>
</body>
然后我们再写一个展示数据的函数showList。里面放一个参数data。
<body>
<ul id="ul"></ul>
<script>
function getData() {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(JSON.parse(xhr.responseText));
}
}
}
function showList(data) {
}
</script>
</body>
此时在写showList函数之前,有一个问题。如果我们要去使用这两个函数,应该先调用getData再调用showList。但getData里面的代码一定是耗时的,因为它向后端发送了http请求,所以会造成代码的异步执行。它会先执行showList再执行getData,所以在这里,我们可以用promise去解决异步。
我们这样写,在函数getData里面return new Promise:
<body>
<ul id="ul"></ul>
<script>
function getData() {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(JSON.parse(xhr.responseText));
resolve(JSON.parse(xhr.responseText).movieList)
}
}
})
}
function showList(data) {
}
</script>
</body>
然后我们将得到的这个JSON对象里面的属性movieList通过resolve返回出去,它到时候就会被return new Promise生成的实例对象中的then方法拿到。然后我们再将这个数据传给showList方法使用。
<body>
<ul id="ul"></ul>
<script>
function getData() {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(JSON.parse(xhr.responseText));
resolve(JSON.parse(xhr.responseText).movieList)
}
}
})
}
function showList(data) {
}
getData()
.then((res) => {
showList(res)
})
</script>
</body>
此时res存放的就是JSON对象里的属性movieList,也就是我们想要的数据,我们在then方法里再去调用showList方法,并将res作为参数传给它。此时showList里的形参data就接收到了这个数据。我们再去showList方法中对这串数据进行操作。
我们对这个data数据进行遍历,这个数据是一个数组类型,这个数组里面存了12个对象,我们用foreach去遍历这个数组,里面接收一个回调函数,参数item代表数组里的每一个对象。
<body>
<ul id="ul"></ul>
<script>
function getData() {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(JSON.parse(xhr.responseText));
resolve(JSON.parse(xhr.responseText).movieList)
}
}
})
}
function showList(data) {
data.forEach(item => {
}
}
getData()
.then((res) => {
showList(res)
})
</script>
</body>
然后我们需要去创建li标签,并将对象里的nm属性赋值给li,并将这个li添加到ul里去。
<body>
<ul id="ul"></ul>
<script>
function getData() {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList', true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(JSON.parse(xhr.responseText));
resolve(JSON.parse(xhr.responseText).movieList)
}
}
})
}
function showList(data) {
data.forEach(item => {
let li = document.createElement('li')
li.innerText = item.nm
document.getElementById('ul').appendChild(li)
});
}
getData()
.then((res) => {
showList(res)
})
</script>
</body>
这样我们就完成了封装接口的代码。我们到浏览器看看是否展示了数据:
成功展示了数据。
5.总结
在这篇文章中,我们了解了异步是一个什么样的概念,并且学到了两种解决异步的方法:回调函数和Promise。在实际开发中我们更倾向于使用Promise,因为它更清晰简洁。