【前端学习】前端高频面试场景题
前端高频面试场景题
面试官问:如果有100个请求,你如何使用Promise控制并发?
如何使用Promise控制并发请求?
并发控制是指在处理多个任务时,限制同一时刻正在执行的任务数量,而不是一次性发起所有任务。
它关乎的是 “同时进行” 的数量,而不是 “总共要完成” 的数量。
在JavaScript中,由于它是单线程的,所谓的“并发”执行异步任务(如网络请求),实际上是指:
- 同时发起了N个请求,并且不等待其中一个完成就立即发起下一个,直到达到并发上限。
- 然后,通过事件循环和回调机制,同时处理这N个请求的响应。
所以,并发控制的核心流程可以概括为:
1. 初始化:启动 K 个任务(达到并发上限)。
2. 监听:任何一个任务完成,就立刻从等待队列中取出下一个任务开始执行。
3. 循环:重复步骤2,直到所有任务都完成。
4. 结束:返回所有任务的结果。
核心思路是:创建一个“并发池”,当池子有空位时,就放入新的任务;任务完成后,从池中移除,并放入下一个等待的任务。
可以通过一个 Pool 类或者一个函数来实现这个逻辑
- Promise
* 控制异步任务的并发执行数量
* @param {Array} tasks 异步任务数组,每个任务是一个返回Promise的函数
* @param {number} limit 最大并发数
* @returns {Promise<any[]>} 所有任务完成后的结果数组
*/
function promisePool(tasks, limit) {
// 用于存储所有任务的结果,按顺序返回
const results = [];
// 创建一个可迭代的迭代器,用于按顺序获取任务
const iterator = tasks.entries();
// 存储当前正在执行的Promise(用于Promise.all)
const executing = new Set();
// 递归函数:从迭代器中获取下一个任务并执行
const enqueue = () => {
const { value, done } = iterator.next();
// 如果所有任务都已分配,且当前没有正在执行的任务,则彻底结束
if (done && executing.size === 0) {
return Promise.resolve();
}
// 如果还有任务且当前并发数未达到上限
if (!done && executing.size < limit) {
const [index, task] = value;
// 执行当前任务
const promise = task().then(result => {
results[index] = result; // 将结果存储到对应的位置
executing.delete(promise); // 任务完成,从执行集中移除
});
executing.add(promise); // 将新任务的Promise加入执行集
// 无论当前任务成功还是失败,都继续调度下一个任务
return promise.catch(() => {}).then(() => enqueue());
}
// 如果并发数已达上限,等待任意一个任务完成后再尝试调度
return Promise.race(executing).then(() => enqueue());
};
// 启动初始的并发任务(启动limit个enqueue过程)
const pool = Array.from({ length: Math.min(limit, tasks.length) }, () => enqueue());
// 等待所有“调度流程”完成,然后返回结果数组
return Promise.all(pool).then(() => results);
}
// 使用示例
// 模拟一个异步请求函数
const createTask = (id, delay) => () =>
new Promise(resolve => {
console.log(`任务 ${id} 开始执行`);
setTimeout(() => {
console.log(`任务 ${id} 完成`);
resolve(`结果-${id}`);
}, delay);
});
// 创建100个任务
const tasks = Array.from({ length: 100 }, (_, i) => createTask(i, 1000 + Math.random() * 1000));
// 以并发数为5的方式执行
promisePool(tasks, 5).then(results => {
console.log('所有任务完成:', results);
});
- async/await
- function asyncPool(tasks, limit) {
const results = [];
const executing = new Set();
for (const [index, task] of tasks.entries()) {
// 如果当前并发数已达上限,等待其中一个任务完成
if (executing.size >= limit) {
await Promise.race(executing);
}
const promise = task().then(result => {
results[index] = result;
executing.delete(promise);
});
executing.add(promise);
}
// 等待所有剩余任务完成
await Promise.all(executing);
return results;
}
面试官:说一下GET请求和POST请求的区别?
GET 和 POST 是 HTTP 协议中最常用的两种方法
- 设计目的 (语义):
- GET 用于 获取数据。它应该是安全和幂等的。
- 安全:意味着请求不应修改服务器上的数据(只读操作)。
- 幂等:意味着多次执行相同的 GET 请求,产生的效果与执行一次完全一致。
- POST 用于 提交/创建数据。它是不安全且不幂等的,通常会对服务器状态产生影响(如更新、创建资源)。
- GET 用于 获取数据。它应该是安全和幂等的。
- 参数位置:
- GET 参数通过 URL 传递,格式为 ?key1=value1&key2=value2。
- POST 参数存放在 请求体(Request Body) 中。
- 参数大小与数据类型:
- GET 参数有长度限制,受限于浏览器和服务器对 URL 长度的规定(通常几KB)。并且,它只能进行 URL 编码,主要传递 ASCII 字符。
- POST 参数在理论上是没有大小限制的(实际受服务器配置约束),支持多种编码方式(如 multipart/form-data),可以传输二进制数据。
- 缓存机制:
- GET 请求会被浏览器 主动缓存,因为其幂等性使得缓存结果可被安全复用。
- POST 请求 不会被浏览器主动缓存,除非手动设置(例如通过 Cache-Control 头部)。
- 历史记录与书签:
- GET 请求的完整 URL(包含参数)会被保存在浏览器历史记录中,并且可以被收藏为 书签(Bookmark)。
- POST 请求的参数不会被保留在历史记录中,也无法被直接收藏为书签。
- 浏览器回退行为:
- 由于 GET 是幂等的,在浏览器中回退时,GET 请求会被无害地 重新获取,可能从缓存中加载。
- 回退到一个 POST 请求的页面时,浏览器通常会 提示用户 是否需要重新提交表单,因为再次提交可能会产生重复操作(如创建两个订单)。
- 安全性(隐私性):
- GET 参数直接暴露在 URL 上,容易被他人看到、被服务器日志记录,隐私性差,因此绝不能用于传递密码等敏感信息。
- POST 参数在请求体中,对用户不可见,隐私性相对更好。
- 重要提示:无论是 GET 还是 POST,数据都是明文传输的。真正的安全性必须依靠 HTTPS 协议来加密整个通信过程。
