如何优雅的解决异步问题和回调地狱?promise会给你答案
前言
JavaScript作为单线程语言,在处理复杂或耗时的异步操作时显得尤为关键。为了在不阻塞主线程的情况下执行这些操作,如网络请求、文件读写等,JavaScript引入了Promise。Promise提供了一种优雅的方式来处理异步操作的结果,无论是成功还是失败,都能通过.then()
和.catch()
链式调用处理,从而有效解决了单线程环境下异步编程的难题。
解决异步问题--回调函数
如何解决异步带来的访问不到的问题?
请大家看下面这一段报错代码,假设b函数的调用依赖a函数的结果,在promise出现之前,程序员是怎么解决这个问题的?
let data = null
function a() {
setTimeout(() => {
data = { name: '绵绵冰' }
}, 1000)
}
function b() {
console.log(data.name + '好帅')
}
a()
b()
解决方法:回调。一般情况下,十年前的程序员往往只能将b函数放入他需要依赖执行结果的函数a的回调函数中调用,也就是将他们变成同步代码。
function a() {
setTimeout(() => {
data = { name: '绵绵冰' }
b()
}, 1000)
}
但是这往往带来更多麻烦,试想一下,如果回调函数过多的话会造成什么情况?就比如以下的代码,如果e的执行依赖d的执行结果,d需要执行必须拿到c的执行结果......以此类推,我们该如何解决这个问题?
function a(){}
function b() { }
function c() { }
function d() { }
function e() { }
如下,我们需要将函数一层层嵌套,但是在一些大项目中,往往不止这一点函数,试想2如果有一百个这样层层依赖的函数呢?将这样数量的函数层层放入,势必会造成回调地狱!而且如果其中一个函数发生错误则会导致后续代码全都无法执行,出现的问题很难排查,效率极低!
function a() {
b();
}
function b() {
c()
}
function c() {
d()
}
//...回调地狱
ES6提供的解决方法:promise
好在ES6中提供了这种灾难的解决方法,接下来我将遵循 相亲->结婚->生小孩 这三个步骤为你展示promise的用法。
function xq() {
setTimeout(() => {
console.log('相亲');
resolve();
}, 2000)
}
function marry() {
setTimeout(() => {
console.log('结婚');
}, 1000)
}
对于上面这两个函数来说,如果使用常规调用的话,会打印 结婚->相亲 ,这显然不符合我们预想的结果。
我们可以利用promise来解决这个问题,我们将 return
一个 new Promise((resolve, reject) => {}
将你想要先触发的函数放入其中,在他的回调中调用resolve()
(其中可以传入参数,其中传入的参数可以在.then中传入res得到),这样就形成了一个许诺,在调用时,使用 .then()
将希望后触发的函数调用放在其中。
function xq() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('相亲');
resolve();
}, 2000)
})
}
function marry() {
setTimeout(() => {
console.log('结婚');
}, 1000)
}
xq().then(() => {
marry();
})
这样就能规划好函数的调用顺序
.then语法
当需要调整触发顺序的函数有很多时,除了将他们依次放进
return newPromise((resolve, reject) => {})
许诺中外,还需要考虑到调用他们的语法。
使用.then
连续调用时,需要将上一个函数的执行结果 return
出来才能在后面继续
接着调用
所以顺序执行 相亲->结婚->生小孩 这三个步骤的函数代码如下
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('baby出生');
}
// 调用规则
xq()
.then((res) => { // 得到第一个reslove()中传入的参数
console.log(res);
return marry()
})
.then((res2) => { // 得到第二个reslove()中传入的参数
console.log(res2);
return baby()
})
Promise实操
在上节课的学习中我们学习了如何使用AJAX请求数据,在今天这节课中我们将他们拆成三部分————获取数据的函数,渲染li的函数,监听点击事件的执行操作。
首先准备好按钮和ul列表
<button id="btn">前后端请求数据</button>
<ul id="ul"></ul>
获取数据的函数
地址依旧是我上节课提供给大家的 mock.mengxuegu.com/mock/65a915…
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) {
let movieList = JSON.parse(xhr.responseText).movieList;
console.log(movieList)
resolve(movieList)
}
}
}
渲染li的函数
function renderLi(arr) {
arr.forEach(item => {
let li = document.createElement('li')
li.innerHTML = item.nm
document.getElementById('ul').appendChild(li);
});
}
如果大家对上述步骤有不清楚的地方,建议移步重温上篇文章
监听点击事件并执行操作
那么来到我们今天的重点,当点击事件触发时,首先得请求数据,当数据请求回来后再循环渲染列表。因为请求数据是耗时函数,所以我们需要使用 Promise 的语法来实现————先请求数据,再渲染列表这一操作
function getData() {
return new Promise(function (resolve, reject) {
// 请求数据操作
})
}
function renderLi(arr) {
// 循环遍历渲染li操作
}
// 点击事件触发,控制函数执行前后顺序
document.getElementById('btn').addEventListener('click', () => {
getData()
.then(res => {
renderLi(res)
})
})
在这里大家看着 getData()
用起来是不是是否眼熟,这不就fetch吗?我们可以将函数执行操作如下,直接在fetch中传入http地址,可以将获取数据函数中的http地址和参数简化为 url
document.getElementById('btn').addEventListener('click', () => {
fetch('https://mock.mengxuegu.com/mock/65a91543c4cd67421b34c898/movie/movieList')
.then(res => {
return res.json()
})
.then(data => {
console.log(data);
renderLi(data.movieList)
})
})
于是我们初步洞悉了 fetch()
的实现原理
Promise语法
.catch()
在往后的项目开发过程中我们还需要注意请求数据失败的情况,如果数据请求失败我们需要在.then后接 .catch()
来捕获错误
function a() {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log('a is ok');
reject('发生错误');
}, 1000)
})
}
function b() {
console.log('b is ok');
}
a()
.then((res) => {
b()
})
.catch((err) => {
console.log(err)
})
这样的错误捕获机制使得我们能够在不报错的情况下寻找到错误出现的地方,提高项目中的开发效率。
Promise.any()
只要函数a或者函数b其中一个执行完成,c都可以执行。
Promise.any([a(), b()]).then(() => {
c()
})
Promise.all()
只有当函数a函数b都执行完成后,函数c才能执行
Promise.all([a(), b()]).then(() => {
c()
})
Promise.race
函数a,b哪个先执行完,那么 Promise.race
的结果将是那个首先 执行完成 的 Promise 的结果,之后函数c再执行
Promise.race([a(), b()]).then(() => {
c()
})
总结
在本节课的学习中,我们通过设置setTimeout()模拟异步函数,从执行机制出发学习了 Promise 的用法。JS的单线程特性虽有其局限性,但通过Promise等现代异步编程机制的引入,极大地增强了其处理复杂异步任务的能力。
转载自:https://juejin.cn/post/7391746098681479206