JavaScript期约(Promise)的链式调用
JavaScript 期约(Promise)的链式调用
什么是 Promise 链式调用
在 JavaScript 中,Promise 是一种处理异步操作的方式,它代表了一个尚未完成但预计将来会完成的操作结果。Promise 有三种状态:pending
(进行中)、fulfilled
(已成功)和 rejected
(已失败)。
Promise 的链式调用是指通过 .then()
方法不断地串联多个异步操作。每个 .then()
方法都会返回一个新的 Promise,这样就可以将多个异步操作连接成一个链条,前一个操作的结果可以作为后一个操作的输入。
Promise 链式调用的基本语法
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Result of promise1');
}, 1000);
});
promise1
.then(result1 => {
console.log(result1);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`Processed: ${result1}`);
}, 1000);
});
})
.then(result2 => {
console.log(result2);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`Final result: ${result2}`);
}, 1000);
});
})
.then(finalResult => {
console.log(finalResult);
})
.catch(error => {
console.error('An error occurred:', error);
});
在上述代码中,promise1
是一个初始的 Promise,它在 1 秒后 resolve 并返回一个字符串。第一个 .then()
方法接收 promise1
的结果,打印出来,并返回一个新的 Promise,这个新 Promise 在 1 秒后 resolve 并处理前一个结果。以此类推,通过链式调用实现了多个异步操作的顺序执行。
链式调用中的返回值
- 返回非 Promise 值
当在
.then()
方法中返回一个非 Promise 值时,这个值会被自动包装成一个已 resolved 的 Promise。例如:
const promise = new Promise((resolve, reject) => {
resolve('Initial value');
});
promise
.then(result => {
return 'Returned non - Promise value';
})
.then(nextResult => {
console.log(nextResult);
});
这里第一个 .then()
返回了一个字符串,第二个 .then()
会接收到这个字符串,因为它被自动包装成了一个已 resolved 的 Promise。
- 返回 Promise
如果在
.then()
中返回一个 Promise,后续的.then()
会等待这个返回的 Promise 完成。例如:
const promise = new Promise((resolve, reject) => {
resolve('Initial value');
});
promise
.then(result => {
return new Promise((innerResolve, innerReject) => {
setTimeout(() => {
innerResolve(`Processed: ${result}`);
}, 1000);
});
})
.then(nextResult => {
console.log(nextResult);
});
第一个 .then()
返回了一个新的 Promise,第二个 .then()
会等待这个新 Promise 被 resolve 后才执行。
错误处理在链式调用中的表现
- 单个
.catch()
处理整个链条的错误 在 Promise 链式调用中,可以在链条的末尾添加一个.catch()
方法来捕获整个链条中任何一个 Promise 被 rejected 时的错误。
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('Error in promise1');
}, 1000);
});
promise1
.then(result1 => {
console.log(result1);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`Processed: ${result1}`);
}, 1000);
});
})
.then(result2 => {
console.log(result2);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`Final result: ${result2}`);
}, 1000);
});
})
.catch(error => {
console.error('An error occurred:', error);
});
在这个例子中,promise1
被 rejected,但是由于在链条末尾有 .catch()
,错误被捕获并打印。
- 在链条中间处理错误
也可以在链条中间的某个
.then()
后添加.catch()
来处理特定部分的错误。例如:
const promise1 = new Promise((resolve, reject) => {
resolve('Initial value');
});
promise1
.then(result1 => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('Error in middle promise');
}, 1000);
});
})
.catch(error => {
console.error('Error in middle:', error);
return 'Recovered value';
})
.then(result2 => {
console.log(result2);
});
这里中间的 Promise 被 rejected,.catch()
捕获了错误并返回了一个新值,后续的 .then()
可以继续执行。
链式调用与并发和并行操作
- 并发操作
虽然 Promise 链式调用主要用于顺序执行异步操作,但可以通过
Promise.all()
等方法实现并发操作,然后再将并发操作的结果融入链式调用中。
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Promise1 result');
}, 1000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Promise2 result');
}, 1500);
});
Promise.all([promise1, promise2])
.then(results => {
console.log('All promises resolved:', results);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`Combined: ${results.join(', ')}`);
}, 1000);
});
})
.then(finalResult => {
console.log(finalResult);
});
Promise.all()
会等待所有传入的 Promise 都 resolved,然后将结果以数组形式传递给 .then()
,之后可以继续链式调用。
- 并行操作
JavaScript 本身是单线程的,但通过 Web Workers 等技术,可以实现类似并行的效果。在 Promise 链式调用的场景下,可以在每个
.then()
中启动 Web Workers 进行一些 CPU 密集型的并行任务。不过这涉及到更多的跨线程通信和数据传递的细节。例如:
// main.js
const worker = new Worker('worker.js');
const promise = new Promise((resolve, reject) => {
worker.onmessage = function (event) {
resolve(event.data);
};
worker.onerror = function (error) {
reject(error);
};
worker.postMessage('Start work');
});
promise
.then(result => {
console.log('Worker result:', result);
return new Promise((resolve, reject) => {
const newWorker = new Worker('anotherWorker.js');
newWorker.onmessage = function (event) {
resolve(event.data);
};
newWorker.onerror = function (error) {
reject(error);
};
newWorker.postMessage(result);
});
})
.then(finalResult => {
console.log('Final result from workers:', finalResult);
});
// worker.js
self.onmessage = function (event) {
// 模拟一些计算
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += i;
}
self.postMessage(result);
};
// anotherWorker.js
self.onmessage = function (event) {
// 更多计算
let newResult = event * 2;
self.postMessage(newResult);
};
在这个例子中,通过 Web Workers 在 Promise 链式调用中实现了类似并行的操作。
链式调用中的微任务与宏任务
- 微任务队列
Promise 的
.then()
回调函数会被放入微任务队列。微任务队列会在当前宏任务执行完后,下一个宏任务执行前被执行。例如:
console.log('Start');
const promise = new Promise((resolve, reject) => {
resolve('Resolved');
});
promise.then(result => {
console.log('Promise then:', result);
});
console.log('End');
输出结果为:
Start
End
Promise then: Resolved
这里 console.log('End')
先执行,因为它在宏任务中,而 .then()
回调在微任务队列中,在当前宏任务结束后执行。
- 宏任务队列
像
setTimeout
这样的操作会将回调放入宏任务队列。当微任务队列为空时,事件循环会从宏任务队列中取出任务执行。例如:
console.log('Start');
const promise = new Promise((resolve, reject) => {
resolve('Resolved');
});
promise.then(result => {
console.log('Promise then:', result);
});
setTimeout(() => {
console.log('Timeout');
}, 0);
console.log('End');
输出结果为:
Start
End
Promise then: Resolved
Timeout
这里 setTimeout
的回调在宏任务队列中,会在微任务队列(.then()
回调所在队列)执行完后才执行。
实际应用场景
- 数据获取与处理 在前端开发中,经常需要从服务器获取数据,然后对数据进行处理。例如,先获取用户信息,再根据用户信息获取用户的订单列表,最后处理订单数据。
function getUserInfo() {
return new Promise((resolve, reject) => {
// 模拟 API 调用
setTimeout(() => {
resolve({ name: 'John', age: 30 });
}, 1000);
});
}
function getOrderList(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (user) {
resolve([{ orderId: 1, amount: 100 }, { orderId: 2, amount: 200 }]);
} else {
reject('User not found');
}
}, 1000);
});
}
function processOrders(orders) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const totalAmount = orders.reduce((acc, order) => acc + order.amount, 0);
resolve(`Total amount: ${totalAmount}`);
}, 1000);
});
}
getUserInfo()
.then(user => {
return getOrderList(user);
})
.then(orders => {
return processOrders(orders);
})
.then(finalResult => {
console.log(finalResult);
})
.catch(error => {
console.error('Error:', error);
});
- 文件读取与处理 在 Node.js 中,可以使用 Promise 链式调用来读取文件内容,然后对文件内容进行解析和处理。
const fs = require('fs');
const path = require('path');
const util = require('util');
const readFile = util.promisify(fs.readFile);
const filePath = path.join(__dirname, 'example.txt');
readFile(filePath, 'utf8')
.then(content => {
console.log('File content:', content);
const lines = content.split('\n');
return lines.filter(line => line.trim()!== '');
})
.then(filteredLines => {
const wordCount = filteredLines.reduce((acc, line) => acc + line.split(' ').length, 0);
return `Total word count: ${wordCount}`;
})
.then(finalResult => {
console.log(finalResult);
})
.catch(error => {
console.error('Error reading file:', error);
});
在这个例子中,首先使用 readFile
读取文件内容,然后对内容进行处理,统计单词数量。
链式调用的最佳实践
- 保持链条简洁
避免在链式调用中添加过多复杂的逻辑,尽量将每个
.then()
方法的功能单一化。这样可以提高代码的可读性和可维护性。例如:
// 不好的实践
const promise = new Promise((resolve, reject) => {
resolve('Initial');
});
promise
.then(result => {
// 多个复杂操作
let processed1 = result.toUpperCase();
let processed2 = processed1.split('').reverse().join('');
return new Promise((innerResolve, innerReject) => {
setTimeout(() => {
innerResolve(processed2);
}, 1000);
});
})
.then(nextResult => {
console.log(nextResult);
});
// 好的实践
const promise = new Promise((resolve, reject) => {
resolve('Initial');
});
function upperCase(result) {
return result.toUpperCase();
}
function reverseString(result) {
return result.split('').reverse().join('');
}
promise
.then(upperCase)
.then(reverseString)
.then(result => {
return new Promise((innerResolve, innerReject) => {
setTimeout(() => {
innerResolve(result);
}, 1000);
});
})
.then(nextResult => {
console.log(nextResult);
});
-
合理处理错误 在链式调用中,确保在合适的位置处理错误。如果错误处理放在链条末尾,可能会掩盖中间步骤的错误。如果中间某个操作的错误有特定的处理方式,应该在该操作对应的
.then()
后添加.catch()
。 -
避免回调地狱替代物 虽然 Promise 链式调用解决了回调地狱的问题,但如果链式调用过长且不注意代码结构,也会导致代码难以阅读。可以考虑使用
async/await
语法来进一步优化代码结构,它基于 Promise 并提供了更简洁的异步代码书写方式。
async/await
与 Promise 链式调用的关系
async/await
是 JavaScript 中处理异步操作的另一种方式,它建立在 Promise 的基础之上。async
函数总是返回一个 Promise。await
只能在 async
函数内部使用,它会暂停 async
函数的执行,等待 Promise 被 resolved 或 rejected。
例如,将之前的数据获取与处理的例子用 async/await
改写:
function getUserInfo() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ name: 'John', age: 30 });
}, 1000);
});
}
function getOrderList(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (user) {
resolve([{ orderId: 1, amount: 100 }, { orderId: 2, amount: 200 }]);
} else {
reject('User not found');
}
}, 1000);
});
}
function processOrders(orders) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const totalAmount = orders.reduce((acc, order) => acc + order.amount, 0);
resolve(`Total amount: ${totalAmount}`);
}, 1000);
});
}
async function main() {
try {
const user = await getUserInfo();
const orders = await getOrderList(user);
const finalResult = await processOrders(orders);
console.log(finalResult);
} catch (error) {
console.error('Error:', error);
}
}
main();
这里 async/await
语法使得异步代码看起来更像同步代码,避免了层层嵌套的 .then()
链式调用。但本质上,它还是基于 Promise 进行异步操作的处理,await
等待的就是一个 Promise 的完成。
总结 Promise 链式调用的要点
- Promise 链式调用通过
.then()
方法串联多个异步操作,前一个操作的结果可以作为后一个操作的输入。 - 正确处理返回值,非 Promise 值会被自动包装,返回 Promise 时后续
.then()
会等待其完成。 - 合理进行错误处理,可以在链条末尾或中间合适位置使用
.catch()
捕获错误。 - 了解微任务和宏任务队列对 Promise 链式调用执行顺序的影响。
- 在实际应用中,如数据获取与处理、文件读取等场景,合理运用 Promise 链式调用。
- 遵循最佳实践,保持链条简洁,合理处理错误,避免新的代码混乱。
- 认识到
async/await
是基于 Promise 链式调用的更简洁的异步处理方式,可以根据具体情况选择使用。
通过深入理解和掌握 Promise 链式调用,开发者能够更有效地处理 JavaScript 中的异步操作,提高代码的质量和可维护性。无论是在前端开发还是后端开发中,Promise 链式调用都是处理异步流程的重要工具。