Promise 是 JavaScript 中的一个重要概念,与前端的工作更是息息相关。因此本文将整理一下 Promise 在日常工作中的应用。
从 MDN | 使用 Promise 中我们能学习到 Promise 的基础使用与错误处理、组合等概念,可以将 Promise 的特点概括为:
Promise 对象有三种状态,且状态一旦改变就不会再变。其值记录在内部属性 [[PromiseState]] 中:
在现实工作中,当我们使用 Promise 时更多是对请求的管理,由于不同请求或任务的异步性。因此我们会根据不同的使用场景处理 Promise 的调度。
async/await 是基于 Promise 的一种语法糖,使得异步代码的编写和理解更加像同步代码。
当一个函数前通过 async 关键字声明,那这个函数的返回值一定会返回一个 Promise,即便函数返回的值与 Promise 无关,也会隐式包装为 Promise 对象:
async function getAge() {
return 18;
}
getAge().then(age => console.log(`age: ${age}`))
// age: 18
await 操作符通常和 async 是配套使用的,它会等待 Promise 并拆开 Promise 包装直接取到里面的值。当它处于 await 状态时,Promise 还处于 ``,后续的代码将不会被执行,因此看起来像是 “同步” 的。
function delayResolve(x, timeout = 2000) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(x);
}, timeout);
});
}
async function main() {
const x = await delayResolve(2, 2000);
console.log(x);
const y = await delayResolve(1, 1000);
console.log(y);
}
main();
// 2
// 1
async/await 的错误处理通过是通过 try..catch 来捕获错误。当然,我们也会根据实际业务的需求来 catch 我们真正需要处理的问题。
try {
const response = await axios.get('https://example.com/user');
// 处理响应数据
console.log('User data fetched:', response.data);
} catch (error) {
console.error('Error response:', error.response);
// 做其他错误处理
}
学习了前文的基础概念后,我们可以更近一步的探讨 Promise 的使用。
Promise 串联一般是指多个 Promise 操作按顺序执行,其中每个操作的开始通常依赖于前一个操作的完成。这种串行执行的一个典型场景是在第一个异步操作完成后,其结果被用作第二个异步操作的输入,依此类推。
考虑以下场景:加载系统时,需要优先读取用户数据,同时需要用户的数据去读取用户的订单的信息,再需要两者信息生成用户报告。因此这是一个存在前后依赖的场景。
function fetchUserInfo(userId) {
return axios.get(`/api/users/${userId}`);
}
function fetchUserOrders(userId) {
return axios.get(`/api/orders/${userId}`);
}
function generateReport(userInfo, orders) {
// 根据用户信息和订单生成报告
return {
userName: userInfo.name,
totalOrders: orders.length,
// ...其他报告数据
};
}
处理串联请求无非有两种方法:
方法 1: 链式调用 .then()
在这种方法中,我们利用 .then() 的链式调用来处理每个异步任务。这种方式的优点是每个步骤都明确且连贯,但可能导致所谓的“回调地狱”,尤其是在处理多个串联的异步操作时。
const userId = '12345'; // 假设已知的用户ID
fetchUserInfo(userId)
.then(response => {
const userInfo = response.data;
return fetchUserOrders(userInfo.id); // 使用用户ID获取订单
})
.then(response => {
const orders = response.data;
return generateReport(userInfo, orders); // 生成报告
})
.then(report => {
console.log('用户报告:', report);
})
.catch(error => {
console.error('在处理请求时发生错误:', error);
});
方法 2: 使用 async/await
async/await 提供了一种更加直观、类似同步的方式来处理异步操作。它使代码更易于阅读和维护,特别是在处理复杂的异步逻辑时。
async function getUserReport(userId) {
try {
const userInfoResponse = await fetchUserInfo(userId);
const userInfo = userInfoResponse.data;
const userOrdersResponse = await fetchUserOrders(userInfo.id);
const orders = userOrdersResponse.data;
const report = generateReport(userInfo, orders);
console.log('用户报告:', report);
} catch (error) {
console.error('在处理请求时发生错误:', error);
}
}
const userId = '12345'; // 假设已知的用户ID
getUserReport(userId);
在这个示例中,使用 async/await 使得代码的逻辑更加清晰和直观,减少了代码的嵌套深度,使错误处理变得简单。
以上是日常工作中最常见的需求.但这里我们还可以发散一下思维,考虑更复杂的情况:
现在有一个数组,数组内有 10 个或更多的异步函数,每个函数都依赖前一个异步函数的返回值需要做处理。在这种请求多了的特殊情况下我们手动维护会显得很冗余,因此可以通过循环来简化逻辑:
方法 1: 通过数组方法 reduce 组合
const processFunctions = [processStep1, processStep2, processStep3, ...];
processFunctions.reduce((previousPromise, currentFunction) => {
return previousPromise.then(result => currentFunction(result));
}, Promise.resolve(initialValue))
.then(finalResult => {
console.log('最终结果:', finalResult);
})
.catch(error => {
console.error('处理过程中发生错误:', error);
});
方法 2: 循环体和 async/await 的结合
async function handleSequentialTasks(tasks, initialResult) {
let result = initialResult;
try {
for (const task of tasks) {
result = await task(result);
}
console.log('最终结果:', result);
} catch (error) {
console.error('处理过程中发生错误:', error);
}
}
const tasks = [task1, task2, task3, ...];
handleSequentialTasks(tasks, initialValue);
并发(Concurrency)在编程中是指多个任务在同一时间段内启动、执行,但不一定同时完成。在 JavaScript 的 Promise 中,并发通常涉及同时开始多个异步操作,并根据它们何时解决(fulfilled)或被拒绝(rejected)来进行相应的处理。
Promise 的并发会比串联的场景更复杂。Promise 对象提供了几个静态方法来处理并发情况,让开发者可以根据不同的使用场景选择合适的方法:
Promise.all(iterable)
Promise.all 静态方法接受一个 Promise 可迭代对象作为输入,当传入的数组中每个都被 resolve 后返回一个 Promise。若任意一个 Promise 被 reject 后就 reject。
const promise1 = fetch('https://example.com/api/data1');
const promise2 = fetch('https://example.com/api/data2');
Promise.all([promise1, promise2])
.then(([data1, data2]) => {
console.log('所有数据已加载:', data1, data2);
})
.catch(error => {
console.error('加载数据时发生错误:', error);
});
Promise.allSettled(iterable)
Promise.allSettled 方法同样接受一个 Promise 的可迭代对象。不同于 Promise.all,这个方法等待所有传入的 Promise 都被解决(无论是 fulfilled 或 rejected),然后返回一个 Promise,它解决为一个数组,每个数组元素代表对应的 Promise 的结果。这使得无论成功还是失败,你都可以得到每个 Promise 的结果。
Promise.allSettled([
Promise.resolve(33),
new Promise((resolve) => setTimeout(() => resolve(66), 0)),
99,
Promise.reject(new Error("an error")),
]).then((values) => console.log(values));
// [
// { status: 'fulfilled', value: 33 },
// { status: 'fulfilled', value: 66 },
// { status: 'fulfilled', value: 99 },
// { status: 'rejected', reason: Error: an error }
// ]
Promise.race(iterable)
Promise.race 方法接受一个 Promise 的可迭代对象,但与 Promise.all 和 Promise.allSettled 不同,它不等待所有的 Promise 都被解决。相反,Promise.race 返回一个 Promise,它解决或被拒绝取决于传入的迭代对象中哪个 Promise 最先解决或被拒绝。
function sleep(time, value, state) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (state === "fulfill") {
return resolve(value);
} else {
return reject(new Error(value));
}
}, time);
});
}
const p1 = sleep(500, "one", "fulfill");
const p2 = sleep(100, "two", "fulfill");
Promise.race([p1, p2]).then((value) => {
console.log(value); // "two"
// Both fulfill, but p2 is faster
});
const p3 = sleep(100, "three", "fulfill");
const p4 = sleep(500, "four", "reject");
Promise.race([p3, p4]).then(
(value) => {
console.log(value); // "three"
// p3 is faster, so it fulfills
},
(error) => {
// Not called
},
);
const p5 = sleep(500, "five", "fulfill");
const p6 = sleep(100, "six", "reject");
Promise.race([p5, p6]).then(
(value) => {
// Not called
},
(error) => {
console.error(error.message); // "six"
// p6 is faster, so it rejects
},
);
Promise.any(iterable)
Promise.any 接受一个 Promise 的可迭代对象,并返回一个 Promise。它解决为迭代对象中第一个被解决的 Promise 的结果。如果所有的 Promise 都被拒绝,Promise.any 会返回一个 AggregateError 实例。
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, "one");
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(reject, 100, "two");
});
Promise.race([promise1, promise2])
.then((value) => {
console.log("succeeded with value:", value);
})
.catch((reason) => {
// Only promise1 is fulfilled, but promise2 is faster
console.error("failed with reason:", reason);
});
// failed with reason: two
JavaScript 默认提供的并发处理函数很方便我们根据业务场景的不同来处理请求,但显然我们工作中所遇到的需求得考虑更复杂的情况,还需要进一步的封装和扩展我们的 API。
在服务器端编程,我们经常遇到需要批量处理数据的场景。例如,批量修改数据库中的用户数据。在这种情况下,由于数据库操作的性能限制或者 API 调用限制,我们不能直接一口气修改全部,因为短时间内发出太多的请求数据库也会处理不来导致应用性能下降。因此,我们需要一种方法来限制同时进行的操作任务数,以保证程序的效率和稳定性。
我们代入实际业务场景:假设有一个社区组织了一次大型户外活动,活动吸引了大量参与者进行在线报名和付费。由于突发情况(比如恶劣天气或其他不可抗力因素),活动不得不取消。这时,组织者需要对所有已付费的参与者进行退款。
活动组织者发起「解散活动」后,服务端接收到请求后当然也不能一次性全部执行退款的操作啦,毕竟一场活动说不定有上千人。因此我们需要分批次去处理。
在上述社区活动退款的例子中,服务器端处理退款请求的一个有效方法是实施分批次并发控制。这种方法不仅保护了后端服务免受过载,还确保了整个退款过程的可管理性和可靠性。
分批次处理时有以下关键问题需要考虑:
将所有待处理的异步任务(如退款请求)存放在一个 tasks 数组中,在调用并发请求前将 tasks 数组分割成多个小批次,每个批次包含固定数量的请求。每当前一个批次处理完后,才处理下一个批次的请求,直到所有批次的请求都被处理完毕:
// 假设这些是返回 Promise 的函数
const tasks = [task1, task2, task3, ...];
// 分割任务数组为批次
function splitIntoBatches(tasks, batchSize) {
let batches = [];
for (let i = 0; i < tasks.length; i += batchSize) {
batches.push(tasks.slice(i, i + batchSize));
}
return batches;
}
// 处理单个批次的函数
function processBatch(batch) {
return Promise.all(batch.map(task => task()));
}
// 控制并发的主函数
async function processTasksInBatches(tasks, batchSize) {
const batches = splitIntoBatches(tasks, batchSize);
for (const batch of batches) {
await processBatch(batch);
// 可以在这里加入日志记录或其他处理
console.log('一个批次处理完毕');
}
console.log('所有批次处理完毕');
}
// 调用主函数,假设每批次处理 10 个任务
processTasksInBatches(tasks, 10);
这种写法实现的并发简单易懂,也易于维护,在一些并发压力不大,比较简单的业务场景来看是足够了。
但如果我们将这种处理方式放在时序图上进行分析,就能发现服务器可能有能力处理更多的并发任务,而这种方法可能没有充分利用可用资源。每个批次开始前会依赖于上一个批次中请求响应时间最慢的那一个,因此我们还可以进一步考虑优化并发实现方案。
在之前的 “控制批次” 方法中,我们发现固定处理批次的局限性,尤其是在并发任务数量较大时可能导致的资源利用不足。为了解决这个问题,我们可以考虑采用一种更灵活的方法:维护一个动态的任务队列来处理异步请求:
我们封装一个函数,提供 concurrency 参数作为并发限制:
function parallelLimit(tasks, {concurrency = 10}) {
const results = [];
const executing = new Set();
let currentlyRunning = 0;
let currentIndex = 0;
return new Promise((resolve) => {
const next = () => {
if (currentIndex < tasks.length) {
// 取出记录数,准备执行
const index = currentIndex;
const task = tasks[index];
currentIndex += 1
currentlyRunning += 1;
const resultPromise = task().then((result) => {
// 任务执行完毕,更新运行数、保存结果
currentlyRunning -= 1;
results[index] = result;
executing.delete(resultPromise);
// 开启下一个任务
next();
});
executing.add(resultPromise);
// 当前运行的任务数小于限制并且还有任务未开始时,继续添加任务
if (currentlyRunning < concurrency && currentIndex < tasks.length) {
next();
}
} else if (currentlyRunning === 0) {
// 所有任务都已完成
resolve(results);
}
};
// 初始化
for (let i = 0; i < Math.min(concurrency, tasks.length); i += 1) {
next();
}
});
}
该函数会在初始阶段会按照并发数先同步执行指定任务数,若某个任务执行完毕后,在执行完毕的回调中会唤醒下一个任务,直至任务队列执行完毕。
以下添加一些测试数据用于测试:
function asyncTask(id) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`任务 ${id} 完成`);
resolve(`结果 ${id}`);
}, Math.random() * 2000);
});
}
const taskArray = Array.from({ length: 10 }, (_, i) => () => asyncTask(i + 1));
parallelLimit(taskArray, {concurrency: 3}).then((results) => {
console.log('所有任务完成:', results);
});
在实际的项目开发中,特别是面临复杂的并发处理需求时,我们更多会考虑使用成熟的第三库来处理业务问题,它们具备更完善的测试用例来检验边界情况。
处理并发的热门库有 RxJS、p-map 和 async.js。
笔者在 Node.js 环境下只需要处理并发问题,故用的 p-map 会更多一些。下面简要介绍 p-map 的使用:
import pMap from 'p-map';
const list = Array.from({ length: 10 }, (_, i) => i)
pMap(list, asyncTask, { concurrency: 3 })
.then((results) => {
console.log('所有任务完成:', results);
});
p-map 的源码实现很精简,建议想深入复习并发的同学去阅读其底层代码的实现作为参考思路。
在本文中,我们首先回顾了 Promise 的基本概念及其在 JavaScript 异步编程中的常用方法。通过这个基础,我们能够更好地理解如何有效地处理和组织异步代码。
随后,我们深入到并发处理的实际应用场景,探讨了如何根据具体需求选择合适的并发实现策略。我们讨论了从简单的批次控制到更复杂的动态任务队列的不同方法,展示了在不同场景下优化异步任务处理的多种可能性。
但值得注意的是,我们自行实现的并发控制工具在没有做足测试用例测试时,可能不适合直接应用于生产环境。在实际的项目开发中,选择成熟且持续维护的第三方库往往是更安全和高效的选择。比如笔者选择的 p-map 稳定性和可靠性相比上文简单实现的版本将会更好。
参考资料:MDN | async 函数 GitHub | p-map
原文来自:https://segmentfault.com/a/1190000044526289
javascript中alert是Bom中的成员函数,alert对话框是模态的,具有阻塞性质的,不点击是不会执行后续代码的。js的阻塞是指在调用结果返回之前,当前线程会被挂起, 只有在得到结果之后才会继续执行。
如何优化async代码?更好的编写async函数:使用return Promise.reject()在async函数中抛出异常,让相互之间没有依赖关系的异步函数同时执行,不要在循环的回调中/for、while循环中使用await,用map来代替它
Javascript语言的执行环境是单线程,异步模式非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。
js异步加载又被称为非阻塞加载,浏览器在下载JS的同时,还会进行后续页面处理。那么如何实现js异步加载呢?下面整理了多种实现方案供大家参考。异步加载js方案:Script Dom Element、onload时的异步加载、$(document).ready()、async属性、defer属性、es6模块type=module属性
回调函数方式:将异步方法如readFile封装到一个自定义函数中,通过将异步方法得到的结果传给自定义方法的回调函数参数。事件驱动方式:使用node events模块,利用其EventEmitter对象
JavaScript引擎是基于单线程 (Single-threaded) 事件循环的概念构建的,同一时刻只允许一个代码块在执行,所以需要跟踪即将运行的代码,那些代码被放在一个任务队列 (job queue) 中
传统的异步解决方案采用回调函数和事件监听的方式,而这里主要记录两种异步编程的新方案:ES6的新语法Promise;ES2017引入的async函数;Generator函数(略)
JS本身是一门单线程的语言,所以在执行一些需要等待的任务(eg.等待服务器响应,等待用户输入等)时就会阻塞其他代码。如果在浏览器中JS线程阻塞了,浏览器可能会失去响应,从而造成不好的用户体验。
请实现如下的函数,可以批量请求数据,所有的URL地址在urls参数中,同时可以通过max参数 控制请求的并发度。当所有的请求结束后,需要执行callback回调。发请求的函数可以直接使用fetch。
将setState()认为是一次请求而不是一次立即执行更新组件的命令。为了更为可观的性能,React可能会推迟它,稍后会一次性更新这些组件。React不会保证在setState之后,能够立刻拿到改变的结果。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!