9.3 Promise
回调地狱
Promise是为了解决回调地狱问题而产生的一种编程模式。它可以将异步操作转换为类似同步操作的形式,以便更好地处理异步代码。所以先看一下什么是回调地狱。 回调地狱指的是使用回调函数进行异步编程时,由于多个异步任务之间存在依赖关系,所以在代码中出现大量的回调函数嵌套,造成代码可读性差、难以维护的问题。 以下是一个使用 XMLHttpRequest 进行异步请求的回调地狱示例:
function loadData(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(xhr.responseText);
} else {
console.error('Error loading data:', xhr.status);
}
}
};
xhr.send();
}
function loadUser(userId, callback) {
loadData('/user/' + userId, function(userData) {
var user = JSON.parse(userData);
callback(user);
});
}
function loadOrders(userId, callback) {
loadData('/orders/' + userId, function(orderData) {
var orders = JSON.parse(orderData);
callback(orders);
});
}
function loadDetails(orderId, callback) {
loadData('/details/' + orderId, function(detailsData) {
var details = JSON.parse(detailsData);
callback(details);
});
}
loadUser('123', function(user) {
console.log('Loaded user:', user);
loadOrders(user.id, function(orders) {
console.log('Loaded orders:', orders);
loadDetails(orders[0].id, function(details) {
console.log('Loaded details:', details);
});
});
});
在这个例子中,我们有多个异步任务需要执行,且这些任务之间存在依赖关系。如果我们使用回调函数进行异步编程,就会出现大量的回调函数嵌套,造成代码可读性差、难以维护的问题。这就是回调地狱的体现。 通过使用Promise,可以避免嵌套的回调函数,代码结构更清晰,可读性更好。此外,Promise还提供了链式调用的方式,使得代码更加优雅。 相较于传统的回调函数方式,使用Promise可以提高代码的可维护性和可重用性。同时,它也为异步编程带来了一种更加简单和可靠的方式。
创建Promise对象
通过Promise构造函数来创建Promise对象。其语法为:
new Promise(function(resolve, reject) {
// 执行异步操作
});
Promise 构造函数接收一个函数作为参数,这个函数的两个参数分别是 resolve 和 reject。当异步操作成功完成时,调用 resolve 函数;当异步操作失败时,调用 reject 函数。resolve 函数返回一个成功的结果,reject 函数返回一个失败的原因(通常是一个 Error 对象)。
Promise的状态
Promise 实例有三种状态: pending (待定):Promise 对象的初始化状态,表示还没有被 resolved 或 rejected。 fulfilled (已完成):表示 Promise 成功完成异步操作,并返回了一个值,这个值可以是任何 JavaScript 的有效值。只有调用了Promise的resolve方法才会让Promise的状态从pending 状态转化为 fulfilled 状态。 rejected (已拒绝):表示 Promise 异步操作失败,调用了Promise的reject方法或者在Promise构造函数的的回调函数中抛出同步的Error,Promise 会从 pending 状态转化为 rejected 状态。 在 Promise 对象的生命周期中,它只能从 pending 转变为 fulfilled 或 rejected 状态中的一种,且一旦发生转变,就不能再次改变状态。
Promise.prototype.then()
Promise.prototype.then() 方法可以用来注册 Promise 实例状态为 fulfilled(已解决)的回调函数,也可以链式调用多个 then() 方法来串联多个异步操作。 该方法接收两个参数,分别为 onFulfilled 和 onRejected,它们都是可选参数,如果不传递则会被忽略。 当 Promise 实例状态为 fulfilled(已解决)时,会调用 onFulfilled 回调函数;当 Promise 实例状态为 rejected(已拒绝)时,会调用 onRejected 回调函数。
then() 方法会返回一个新的 Promise 实例,该 Promise 实例的状态根据 onFulfilled 或 onRejected 回调函数的返回值来决定: 如果 onFulfilled 或 onRejected 返回一个值,那么新 Promise 实例的状态为 fulfilled(已解决),并且返回值会作为新 Promise 实例的值。 如果 onFulfilled 或 onRejected 抛出异常,那么新 Promise 实例的状态为 rejected(已拒绝),并且异常对象会作为新 Promise 实例的值。 如果 onFulfilled 或 onRejected 返回一个 Promise 实例,那么新 Promise 实例的状态和值都会和返回的 Promise 实例保持一致。 以下是一个简单的示例:
const promise = new Promise((resolve, reject) => {
resolve(1);
});
promise.then((value) => {
console.log(value); // 1
return 2;
}).then((value) => {
console.log(value); // 2
throw new Error('Error');
}).then(() => {
console.log('This line will not be executed.');
},(error) => {
console.log(error.message); // Error
})
在上面的代码中,Promise 实例的初始状态为 fulfilled(已解决),因此第一个 then() 回调函数会被立即执行,输出 1,并返回值 2。然后第二个 then() 回调函数会被立即执行,输出 2,然后抛出一个异常,返回了一个新的rejected状态的Promise,所以在第3个then中进入的是onRejected的回调函数。
Promise.prototype.catch()
Promise.prototype.catch()方法是Promise.prototype.then(null, rejection)的别名,它只接收一个参数,即rejected状态的回调函数。如果Promise实例被rejected状态,它会执行这个回调函数,并把错误对象作为参数传递给它。如果Promise实例没有被rejected状态,Promise.prototype.catch()方法不会做任何事情,它只是返回一个新的Promise实例,该实例与原始实例相同。以下是一个简单的示例:
new Promise(function(resolve, reject) {
// 执行异步操作
throw Error("this is a error")
}).catch(e=>{
console.log(e.message) // this is a error
return 'catch response'
}).then((res)=>{
console.log('catch 继续返回一个新的promise: ' + res) // catch 继续返回一个新的promise: catch response
})
Promise构造函数的回调函数中抛出同步错误,Promise对象状态变成了rejected,Error对象作为参数传给catch方法,catch方法会再返回一个全新的Promise,状态是fulfilled, 并且返回参数。
Promise.prototype.finally()
Promise.prototype.finally() 是 ES2018 中新增的方法,它的作用是在 Promise 状态转换为 resolved 或 rejected 后调用,无论如何都会执行指定的回调函数。该方法不会改变原始 Promise 的状态,而是返回一个新的 Promise 实例,该实例的状态与原始 Promise 实例的状态相同, 包括Promise实例的值也是相同的,所以可以继续进行链式调用。对于 finally 方法,它的作用主要是在 Promise 状态不论是成功或失败的情况下,都会执行一些最终的操作。这些操作包括清理操作,取消请求等等。看以下的示例:
var p = new Promise(function(resolve, reject) {
resolve(1)
})
var p2 = p.then((res)=>{
console.log(res)
console.log(2)
return 3
},()=>{}).finally((...args)=>{
console.log(args)
return Promise.resolve(4)
}).then(res=>{
console.log(res)
})
console.log(p === p2)
输出的结果如下:
false
1
2
[]
3
打印的false证明了finally返回的Promise实例和之前的实例是不同的。 打印的[]证明了finally是不接受任何参数的。 最后打印的3证明了finally返回的Promise实例的状态和值和原Promise实例相同,并不是finallly回调函数中的返回。
Promise.resolve()
Promise.resolve 方法返回一个已经被解决(resolved)的 Promise 对象,可以带有指定的值。 如果传入的值是一个 Promise 对象,那么将直接返回该对象,不做任何处理。 如果传入的值是一个 thenable 对象(即带有 then 方法),则会将其包装成一个 Promise 对象并返回。 如果传入的是一个普通的非 Promise、非 thenable 值,则会返回一个状态为 fulfilled的 Promise 对象,并将该值作为 Promise 的结果值。 下面是一些使用 Promise.resolve 方法的示例:
// 返回一个带有指定值的 Promise 对象
const p1 = Promise.resolve(42);
console.log(p1); // Promise { 42 }
// 将现有 Promise 对象转换为一个新的 Promise 对象
const p2 = Promise.resolve(p1);
console.log(p1 === p2); // true
// 将 thenable 对象转换为 Promise 对象
const thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
const p3 = Promise.resolve(thenable);
p3.then(res=>{
console.log(p3); // Promise { 42 }
})
// 将普通值转换为 Promise 对象
const p4 = Promise.resolve('hello');
console.log(p4); // Promise { "hello" }
可以看到,不管传入的参数是什么类型,Promise.resolve 方法最终都返回一个 Promise 对象。如果参数本身就是一个 Promise 对象,那么 Promise.resolve 直接返回该对象。如果参数是一个 thenable 对象,那么会包装成一个 Promise 对象并返回。如果参数是一个普通的非 Promise、非 thenable 值,则会返回一个状态为 resolved 的 Promise 对象,并将该值作为 Promise 的结果值。
Promise.reject()
Promise.reject(reason) 方法返回一个新的 Promise 实例,该实例的状态为 rejected,并且拒绝原因为 reason。该方法与 Promise.resolve() 方法类似,但是其返回的 Promise 实例的状态总是为 rejected,而不是根据传入参数的值决定状态。 示例:
Promise.reject('something went wrong').catch(reason => {
console.log(reason); // 输出:something went wrong
});
在上述示例中,Promise.reject() 方法返回的 Promise 实例被 catch 方法捕获,因为该实例的状态为 rejected,并且拒绝原因为 'something went wrong'。
Promise.all()
Promise.all() 方法接收一个由 Promise 实例组成的数组作为参数,并返回一个新的 Promise 实例。 当传入的所有 Promise 实例的状态都变为 fulfilled 时,返回的 Promise 实例状态也变为 fulfilled,并将每个 Promise 实例的结果按顺序放入数组中作为回调函数的参数。 如果传入的 Promise 实例中任意一个状态变为 rejected,则返回的 Promise 实例状态变为 rejected,并将第一个被 rejected 的 Promise 实例的返回值作为回调函数的参数。 例如,假设有三个 Promise 实例 a、b、c,我们可以使用 Promise.all() 方法来等待它们的结果,并在所有 Promise 实例都变为 fulfilled 时将它们的结果作为数组返回:
Promise.all([a, b, c])
.then(function(results) {
console.log('Results:', results);
})
.catch(function(error) {
console.error('Error:', error);
});
上面的代码中,当 a、b、c 三个 Promise 实例都变为 fulfilled 时,then() 方法的回调函数将以一个数组作为参数被调用,数组包含了 a、b、c 的结果。如果其中任意一个 Promise 实例变为 rejected,则 catch() 方法的回调函数将被调用,参数为第一个被 rejected 的 Promise 实例的返回值。
Promise.race()
Promise.race() 方法接收一个可迭代对象(如数组)作为参数,返回一个新的 Promise 对象。新的 Promise 对象将在可迭代对象中的任意一个 Promise 对象 settled(即状态变为fulfilled或rejected)后立即 resolve,并以该 Promise 对象的 settled 结果作为自己的解决值(resolved value)。 如果可迭代对象中的所有 Promise 都没有 settled,那么返回的 Promise 对象将一直处于 pending 状态,直到有一个 Promise settled。如果可迭代对象为空,则返回的 Promise 对象将永远处于 pending 状态。 下面是一个示例:
const p1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'one');
});
const p2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'two');
});
Promise.race([p1, p2]).then((value) => {
console.log(value); // 'two'
// p2 先 settled,因此 Promise.race() 返回的 Promise 立即 resolve,并以 p2 的结果作为自己的解决值。
});
在这个示例中,Promise.race([p1, p2]) 返回一个新的 Promise 对象。由于 p2 的定时器只需要 100ms,所以它比 p1 先 settled,因此返回的 Promise 对象将在 100ms 后 resolve,解决值为 'two'。 需要注意的是,Promise.race() 返回的是一个新的 Promise 对象,而不是原来的 Promise 对象,因此 p1 和 p2 的状态和结果不会受到影响。另外,Promise.race() 方法也是可传入一个空数组的,这时会返回一个永远处于 pending 状态的 Promise 对象。 实际上,传入的参数可以是任何可迭代对象,包括数组、Set、Map 等。Promise.race() 方法会自动将可迭代对象转换为数组形式处理。如果传入的不是可迭代对象,Promise.race() 方法会抛出 TypeError 异常。以下是一个示例:
const set = new Set([Promise.resolve('a'), Promise.resolve('b'), Promise.reject('c')]);
Promise.race(set).then(
value => console.log(value), // 输出 'a'
error => console.log(error) // 不会执行
);
Promise改成回调地狱 最后再用Promise改写一下回调地狱章节的例子,对比看一下:
function loadData(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject('Error loading data: ' + xhr.status);
}
}
};
xhr.send();
});
}
function loadUser(userId) {
return loadData('/user/' + userId)
.then(function(userData) {
return JSON.parse(userData);
});
}
function loadOrders(userId) {
return loadData('/orders/' + userId)
.then(function(orderData) {
return JSON.parse(orderData);
});
}
function loadDetails(orderId) {
return loadData('/details/' + orderId)
.then(function(detailsData) {
return JSON.parse(detailsData);
});
}
loadUser('123')
.then(function(user) {
console.log('Loaded user:', user);
return loadOrders(user.id);
})
.then(function(orders) {
console.log('Loaded orders:', orders);
return loadDetails(orders[0].id);
})
.then(function(details) {
console.log('Loaded details:', details);
})
.catch(function(error) {
console.error(error);
});
使用 Promise 改写后,可以看到代码的层次结构更清晰,而且不再存在回调地狱问题。每个异步操作都返回一个 Promise 对象,可以使用 .then() 方法进行链式调用,最后使用 .catch() 方法捕捉错误。