「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。
异步编程
javascript将任务的执行分成两种,同步模式和异步模式。
javascript通过事件循环和消息队列来处理js单线程的异步模式。
同步处理意味着任务必须一个一个依次执行,碰到耗时任务必须等到这个耗时任务执行完才能执行下一个任务,这样会造成卡死。我们可以异步处理这些耗时任务来避免卡死,异步模式不会去等待这个任务的结束才开始下一个任务。对于耗时操作,它开启过后就立即往后执行下一个任务。耗时任务的后续逻辑一般会通过回调函数的方式定义,在内部耗时任务完成过后会自动执行传入的回调函数。
异步模式对于javascript是十分重要的。因为如果没有异步模式,单线程的javascript就无法同时处理大量耗时任务。
我们来看下异步模式的工作流程。
js线程在某一时刻发起了一个异步调用,然后它紧接着执行往后的其他任务。此时,异步线程会单独执行这个异步任务,然后在这个异步任务执行完成之后会将这个异步任务的回调放入消息队列。js主线程完成所有任务之后依次执行消息队列中的任务
回调函数
回调函数是所有异步编程方案的根基。
回调函数可以理解为一件你想要做的事情,你明确知道这件事应该怎么做,但是你不知道这件事情所依赖的任务什么时候才能完成。最好的办法是你把任务的步骤写到一个函数当中,交给函数的执行者,他知道这个任务是什么时候结束的,他可以在结束过后帮你去执行你想要做的事情。那么这件你想要做的事情可以想象成回调函数。
由调用者定义,交给执行者执行的函数称为回调函数。
// 回调函数
function foo(callback) {
setTimeout(function() {
callback()
}, 3000)
}
foo(function() {
console.log('这就是一个回调函数');
console.log('调用者定义这个函数,执行者执行这个函数');
console.log('其实就是调用者告诉执行者异步任务结束后应该做什么');
})
处理异步任务
在讲解Promise之前,我们先通过一个实际的例子来处理异步任务。
首先我们封装一个函数,使用setTimeout来模拟网络请求。当发送网络请求成功时,告知调用者该请求成功了,并返回相应的数据。如果发送网络请求失败了,告知调用者该请求失败了,并返回相应的错误。
function requestData(url, successCallback, failtureCallback) {
// 模拟网络请求
setTimeout(() => {
if (url === 'haha') {
// 成功
// 返回的数据
let names = ['abc', 'cba', 'nba']
// 使用return 外面是拿不到返回的结果的
// return names
successCallback(names)
} else {
// 请求失败
let errMessage = '请求失败,url错误'
failtureCallback(errMessage)
}
}, 3000)
}
// 通过回调函数拿到返回的结果
requestData('haha', (res) => {
console.log(res);
}, err => {
console.log(err);
})
在上述处理中,我们确实拿到了网络请求返回的结果,但是存在两个主要问题。
(1) 我们需要自己来设计回调函数、回调函数的名称、回调函数的使用等。
(2) 不同的人、不同的框架设计出来的方案是不同的,我们需要看别人的源码或者文档,以便理解它这个函数是怎么使用的。
Promise介绍
在ES6中,新增了Promise,意思是承诺。Promise是一种更优的异步编程统一方案。它会给调用者一个承诺:如果我这儿成功了,我会给你相应的数据;如果失败了,我会给你相应的错误信息。它给上面的处理方法做了规范。Promise本质上也是使用回调函数的方式去定义异步任务结束后所需要执行的任务。它是一个构造函数,可以通过new创建一个Promise对象。
在通过new创建Promise对象时,我们需要传入一个回调函数,我们称之为executor。
(1) 这个回调函数会立即执行,并且需要传入另外两个回调函数resolve、reject;
(2) 当我们调用resolve函数时,会执行Promise对象的then方法传入的回调函数;
(3) 当我们调用reject函数时,会执行Promise对象的catch方法传入的回调函数;
基本使用
在promise A+规范里面,必须传入一个函数(executor立即执行函数),这个函数是立即执行的。
如果不传立即执行函数,会报错。
<script>
const promise = new Promise()
console.log(promise);
</script>
const promise = new Promise(() => {
console.log(777); // 会立即执行
})
console.log(promise);
new的时候,函数里的代码会立即执行。
在创建Promise时,需要传入一个回调函数,称为executor,它会 会立即执行,并且传入两个回调函数resolve和reject。
resolve: 回调函数,在成功时,进行回调;
reject: 回调函数,在失败时,进行回调;
const promise = new Promise((resolve, reject) => {
console.log('promise传入的函数被立即执行了');
resolve('成功')
// reject('失败')
})
promise.then((res) => {
console.log(res);
}).catch((err) => {
console.log(err);
})
即使没有异步操作,then方法中传入的回调依然会被放入队列,等待下一轮执行。
// Promise基本示例
const promise = new Promise((resolve, reject) => {
resolve(100)
//reject(new Error('promise rejected'))
})
promise.then(res => {
// 即使没有异步操作,then方法中传入的回调依然会被放入队列,等待下一轮执行
console.log('resolved', res);
}, err => {
console.log('rejected', err);
})
console.log('end');
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve()
console.log(2);
})
promise.then(() => {
console.log(3);
})
console.log(4);
这是因为执行器executor是自动执行的。
重写异步任务
学习了Promise的基本使用,我们来重写上述的异步任务。我们会将异步请求的代码写在executor中。我们可以在promise对象的then方法中拿到网络请求返回的结果。
function requestData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url === 'haha') {
// 成功
let names = ['abc', 'cba', 'nba']
resolve(names)
} else {
// 失败
let errMessage = '请求失败'
reject(errMessage)
}
}, 3000)
})
}
const promise = requestData('haha')
promise.then(res => {
console.log('请求成功:', res);
}, err => {
console.log('请求失败', err);
})
Promise的三种状态
我们将Promise分为三个状态:pending(待定)、fulfilled(已兑现)、rejected(已拒绝)。
pending(待定):初始状态,既没有被兑现,也没有被拒绝。当执行executor中的代码时,处于该状态。
fulfilled(已兑现): 执行了resolve时,处于该状态,表示操作成功。
rejected(已拒绝): 执行了reject时,处于该状态,表示操作失败。
new Promise((resolve, reject) => {
// pending状态
console.log('-----------');
resolve() // 处于fulfilled状态(已敲定/兑现)
reject() // 处于rejected状态(已拒绝状态)
console.log('+++++++');
}).then(res => {
console.log(res);
}, err => {
console.log(err);
})
注意: 一旦状态被确定下来,该Promise的状态是不可更改的,但是并不意味着resolve()或者reject()下面的代码不执行。我们可以看到运行结果,输出了+。
(1) 在我们调用resolve的时候,如果resolve传入的值本身不是一个Promise,那么会将该Promise的状态变为fulfilled。
(2)在之后我们去调用reject时,已经不会有任何响应了(并不是这行代码不会执行,而是无法改变Promise状态)。
如果我们需要连续串联执行多个异步任务,那么仍然会出现回调函数嵌套的问题,形成回调地狱。
$.get('/url1', function (data1) {
$.get('/url2', data1, function (data2) {
$.get('/url3', data2, function (data3) {
$.get('/url4', data3, function (data4) {
$.get('/url5', data4, function (data5) {
$.get('/url6', data5, function (data6) {
$.get('/url7', data6, function (data7) {
// 略微夸张了一点点
})
})
})
})
})
})
})
这种嵌套使用的方式是使用Promise最常见的错误,那么正确的做法是借助于Promise then方法链式调用的特点,尽可能保证异步任务的扁平化。
相比与传统回调函数的方式,Promise最大的优势在于可以链式调用,这样就可以最大程度的避免回调嵌套。
往期文章 👇👇👇