
Promise 前端应用场景与实战题目
Promise 作为 JavaScript 异步编程的核心解决方案,在前端开发中有非常广泛的应用。以下是常见的应用场景及对应的实战题目:
1. 并行请求数据并处理结果
场景描述
同时需要从多个接口并行请求数据,待所有数据都返回后进行汇总处理。
题目:实现并行请求并合并结果
javascript
// 模拟接口请求函数
function fetchData(url) {
return new Promise((resolve) => {
// 模拟网络延迟
setTimeout(() => {
const data = {
'user': { id: 1, name: '张三' },
'posts': [{ id: 1, title: '文章1' }, { id: 2, title: '文章2' }],
'comments': [{ id: 1, content: '评论1' }]
};
resolve(data[url]);
}, Math.random() * 1000);
});
}
// 并行请求多个接口并处理结果
async function loadAndCombineData() {
try {
// 并行发起请求
const [user, posts, comments] = await Promise.all([
fetchData('user'),
fetchData('posts'),
fetchData('comments')
]);
// 合并结果
return {
user,
posts: posts.map(post => ({
...post,
// 为每篇文章添加评论(实际场景可能根据ID关联)
comments: comments.filter(comment => comment.postId === post.id)
}))
};
} catch (error) {
console.error('请求失败:', error);
// 返回默认数据或重新抛出错误
return { user: null, posts: [] };
}
}
// 使用
loadAndCombineData().then(result => {
console.log('合并后的数据:', result);
});
关键点解析
- 使用
Promise.all()
实现并行请求,效率高于串行请求 - 所有请求都成功才会进入
then
,任何一个失败都会触发catch
- 适合需要多个接口数据才能完成的业务场景(如页面初始化加载)
2. 带重试机制的请求
场景描述
网络请求可能偶尔失败,需要实现带重试机制的请求函数,失败后自动重试指定次数。
题目:实现带重试机制的请求函数
javascript
/**
* 带重试机制的请求函数
* @param {Function} requestFn - 返回Promise的请求函数
* @param {number} maxRetries - 最大重试次数
* @param {number} delay - 重试延迟时间(ms)
* @returns {Promise}
*/
function requestWithRetry(requestFn, maxRetries = 3, delay = 1000) {
return new Promise((resolve, reject) => {
// 执行请求的递归函数
function attempt(retriesLeft) {
requestFn()
.then(resolve)
.catch(error => {
// 如果还有重试次数,延迟后重试
if (retriesLeft > 0) {
console.log(`请求失败,将在${delay}ms后重试,剩余次数: ${retriesLeft}`);
setTimeout(() => {
attempt(retriesLeft - 1);
}, delay);
} else {
// 重试次数用尽,返回失败
reject(new Error(`超过最大重试次数(${maxRetries}),请求失败: ${error.message}`));
}
});
}
// 开始第一次尝试
attempt(maxRetries);
});
}
// 使用示例
// 模拟一个有20%概率成功的请求
function unstableRequest() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() < 0.2) {
resolve('请求成功的数据');
} else {
reject(new Error('网络错误'));
}
}, 500);
});
}
// 使用带重试的请求
requestWithRetry(unstableRequest, 3, 1000)
.then(data => console.log('最终结果:', data))
.catch(error => console.error('最终失败:', error.message));
关键点解析
- 通过递归实现重试逻辑,每次失败后检查剩余重试次数
- 加入延迟避免频繁重试给服务器造成压力
- 适用于网络不稳定环境下的关键请求
3. 限制并发请求数量
场景描述
当需要发送大量请求时(如下载多个文件),无限制的并发会导致性能问题或被服务器限制,需要控制同时发起的请求数量。
题目:实现限制并发数的请求调度器
javascript
/**
* 并发请求调度器
* @param {Array<Function>} tasks - 返回Promise的任务数组
* @param {number} limit - 最大并发数
* @returns {Promise}
*/
function scheduleTasks(tasks, limit) {
return new Promise((resolve) => {
if (tasks.length === 0) {
resolve([]);
return;
}
const results = []; // 存储所有结果
let index = 0; // 当前要执行的任务索引
let completed = 0; // 已完成的任务数量
// 执行一个任务
function runTask() {
// 如果所有任务都已开始执行,直接返回
if (index >= tasks.length) return;
const taskIndex = index++;
const task = tasks[taskIndex];
task()
.then(result => {
results[taskIndex] = result; // 按原顺序存储结果
completed++;
// 继续执行下一个任务
runTask();
// 所有任务都完成时,返回结果
if (completed === tasks.length) {
resolve(results);
}
})
.catch(error => {
results[taskIndex] = { error: error.message };
completed++;
runTask();
if (completed === tasks.length) {
resolve(results);
}
});
}
// 启动初始的limit个任务
for (let i = 0; i < limit && i < tasks.length; i++) {
runTask();
}
});
}
// 使用示例
// 创建模拟任务
function createTask(id) {
return () => new Promise((resolve) => {
console.log(`开始执行任务 ${id}`);
setTimeout(() => {
console.log(`完成任务 ${id}`);
resolve(`任务${id}的结果`);
}, Math.random() * 1000);
});
}
// 创建10个任务
const tasks = Array.from({ length: 10 }, (_, i) => createTask(i + 1));
// 限制最大并发数为3
scheduleTasks(tasks, 3).then(results => {
console.log('所有任务完成,结果:', results);
});
关键点解析
- 维护任务索引和完成计数,精确控制并发数量
- 保证结果数组顺序与原任务顺序一致
- 适用于批量操作场景(如批量上传、批量下载)
4. 图片懒加载实现
场景描述
在长列表中,为了提高页面加载速度,通常会实现图片懒加载:当图片滚动到视口附近时才加载图片。
题目:使用Promise实现图片懒加载
javascript
/**
* 预加载图片
* @param {string} url - 图片URL
* @returns {Promise}
*/
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = url;
img.onload = () => {
resolve(img);
};
img.onerror = () => {
reject(new Error(`无法加载图片: ${url}`));
};
});
}
/**
* 检查元素是否在视口内
* @param {HTMLElement} el - 元素
* @param {number} offset - 偏移量
* @returns {boolean}
*/
function isInViewport(el, offset = 200) {
const rect = el.getBoundingClientRect();
return (
rect.top <= (window.innerHeight || document.documentElement.clientHeight) + offset &&
rect.bottom >= -offset &&
rect.left <= (window.innerWidth || document.documentElement.clientWidth) + offset &&
rect.right >= -offset
);
}
/**
* 图片懒加载初始化
*/
function initLazyLoad() {
// 获取所有懒加载图片
const lazyImages = document.querySelectorAll('img.lazy');
const imagePromises = new Map(); // 存储正在加载的图片Promise
// 加载可见的图片
function loadVisibleImages() {
lazyImages.forEach(img => {
// 如果已经加载或正在加载,跳过
if (img.dataset.loaded === 'true' || imagePromises.has(img)) return;
// 检查是否在视口内
if (isInViewport(img)) {
const src = img.dataset.src;
if (src) {
// 开始加载图片
const promise = loadImage(src)
.then(loadedImg => {
// 加载成功,更新图片
img.src = src;
img.dataset.loaded = 'true';
img.classList.add('loaded');
imagePromises.delete(img);
})
.catch(error => {
console.error(error);
img.dataset.loaded = 'error';
imagePromises.delete(img);
});
imagePromises.set(img, promise);
}
}
});
}
// 初始加载一次
loadVisibleImages();
// 滚动和resize时检查
const handleScroll = () => {
// 使用requestAnimationFrame优化性能
requestAnimationFrame(loadVisibleImages);
};
window.addEventListener('scroll', handleScroll);
window.addEventListener('resize', handleScroll);
// 返回清理函数
return () => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', handleScroll);
};
}
// 使用示例
// 在DOM加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
const cleanup = initLazyLoad();
// 页面卸载时清理
window.addEventListener('unload', cleanup);
});
关键点解析
- 使用 Promise 封装图片加载过程,便于处理成功/失败状态
- 结合滚动事件检测图片是否进入视口
- 使用 Map 跟踪正在加载的图片,避免重复加载
- 适用于图片较多的页面(如电商列表、相册等)
5. 实现请求缓存
场景描述
对于重复的请求(如用户信息、配置数据),可以缓存结果避免重复请求,提高性能并减少服务器压力。
题目:实现带缓存的请求函数
javascript
/**
* 创建带缓存的请求函数
* @param {Function} requestFn - 原始请求函数,返回Promise
* @param {number} ttl - 缓存过期时间(ms),默认300000ms(5分钟)
* @returns {Function} 带缓存的请求函数
*/
function createCachedRequest(requestFn, ttl = 300000) {
// 缓存存储:key -> { data, timestamp }
const cache = new Map();
// 正在进行的请求:key -> Promise,避免重复请求
const pendingRequests = new Map();
return async function cachedRequest(...args) {
// 将参数序列化为缓存键
const key = JSON.stringify(args);
// 检查缓存是否存在且未过期
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
console.log('使用缓存数据:', key);
return cached.data;
}
// 检查是否有相同的请求正在进行
if (pendingRequests.has(key)) {
console.log('等待正在进行的请求:', key);
return pendingRequests.get(key);
}
// 执行新请求
console.log('发起新请求:', key);
const promise = requestFn(...args)
.then(data => {
// 请求成功,更新缓存
cache.set(key, {
data,
timestamp: Date.now()
});
return data;
})
.finally(() => {
// 无论成功失败,都移除pending状态
pendingRequests.delete(key);
});
// 记录正在进行的请求
pendingRequests.set(key, promise);
return promise;
};
}
// 使用示例
// 模拟API请求
function fetchUserInfo(userId) {
return new Promise((resolve) => {
console.log(`实际请求用户信息: ${userId}`);
setTimeout(() => {
resolve({
id: userId,
name: `用户${userId}`,
age: 20 + userId % 10
});
}, 1000);
});
}
// 创建带缓存的请求函数,缓存10秒
const cachedFetchUserInfo = createCachedRequest(fetchUserInfo, 10000);
// 测试缓存效果
async function test() {
// 第一次请求 - 新请求
console.log(await cachedFetchUserInfo(1));
// 第二次请求 - 使用缓存
console.log(await cachedFetchUserInfo(1));
// 不同参数 - 新请求
console.log(await cachedFetchUserInfo(2));
// 3秒后再次请求 - 仍使用缓存
setTimeout(async () => {
console.log(await cachedFetchUserInfo(1));
}, 3000);
// 11秒后再次请求 - 缓存过期,新请求
setTimeout(async () => {
console.log(await cachedFetchUserInfo(1));
}, 11000);
}
test();
关键点解析
- 使用 Map 存储缓存数据和过期时间
- 处理并发请求:相同请求同时发起时,只实际请求一次
- 支持设置缓存过期时间,避免数据长期不一致
- 适用于不频繁变化的数据请求(如用户信息、字典数据)
这些场景覆盖了 Promise 在前端开发中的主要应用,包括数据请求、性能优化、资源加载等方面。掌握这些实战技巧不仅能应对面试,更能在实际开发中写出高效、健壮的代码。
前端常见应用场景题目详解
在前端开发中,许多场景具有高度的通用性和实用性,也是面试中的高频考点。以下梳理了最常见的应用场景及具体实现方案:
一、高频事件处理(防抖与节流)
场景描述
在处理滚动、窗口 resize、输入框实时搜索等高频触发的事件时,直接执行回调函数会导致性能问题,需要通过防抖或节流优化。
1. 防抖(Debounce)
核心思想:事件触发后延迟n秒执行,如果n秒内再次触发则重新计时,适用于"输入完成后再处理"的场景(如搜索联想)。
javascript
/**
* 防抖函数
* @param {Function} fn - 需要防抖的函数
* @param {number} delay - 延迟时间(ms)
* @param {boolean} immediate - 是否立即执行第一次触发
* @returns {Function} 防抖处理后的函数
*/
function debounce(fn, delay, immediate = false) {
let timer = null;
let isExecuted = false; // 标记是否已立即执行
return function(...args) {
const context = this;
// 清除现有定时器
if (timer) clearTimeout(timer);
// 立即执行模式(仅第一次触发时执行)
if (immediate && !isExecuted) {
fn.apply(context, args);
isExecuted = true;
} else {
// 延迟执行模式
timer = setTimeout(() => {
fn.apply(context, args);
// 重置立即执行标记(下一次序列可重新立即执行)
if (immediate) isExecuted = false;
timer = null;
}, delay);
}
};
}
// 使用示例:搜索框输入联想
const searchInput = document.getElementById('search-input');
const handleSearch = (e) => {
console.log('搜索:', e.target.value);
// 实际场景:调用搜索接口
};
// 输入停止500ms后执行搜索
searchInput.addEventListener('input', debounce(handleSearch, 500));
2. 节流(Throttle)
核心思想:每隔n秒最多执行一次函数,适用于"持续触发但需要定期处理"的场景(如滚动加载、动画帧)。
javascript
/**
* 节流函数
* @param {Function} fn - 需要节流的函数
* @param {number} interval - 间隔时间(ms)
* @returns {Function} 节流处理后的函数
*/
function throttle(fn, interval) {
let lastTime = 0; // 上次执行时间
let timer = null;
return function(...args) {
const context = this;
const now = Date.now();
const remaining = interval - (now - lastTime); // 剩余时间
// 清除延迟执行的定时器(避免最后一次触发延迟执行)
if (timer) clearTimeout(timer);
// 如果间隔时间已到,立即执行
if (remaining <= 0) {
fn.apply(context, args);
lastTime = now;
} else {
// 否则设置定时器,确保最后一次触发能执行
timer = setTimeout(() => {
fn.apply(context, args);
lastTime = Date.now();
timer = null;
}, remaining);
}
};
}
// 使用示例:滚动加载
window.addEventListener('scroll', throttle(() => {
console.log('滚动位置:', window.scrollY);
// 实际场景:判断是否到达底部,加载更多内容
}, 300));
关键点解析
- 防抖 vs 节流:防抖是"等待最后一次触发后执行",节流是"固定频率执行"
- 应用场景区分:输入框搜索用防抖,滚动/resize用节流
- 优化点:通过
apply
保留函数上下文,处理立即执行需求,避免内存泄漏
二、图片懒加载(Lazy Load)
场景描述
在长列表或图片密集的页面中,优先加载可视区域内的图片,滚动到附近时再加载其他图片,减少初始加载时间和带宽消耗。
javascript
/**
* 图片懒加载实现
* 1. 页面初始化时加载可视区域图片
* 2. 滚动时加载进入视口的图片
* 3. 使用IntersectionObserver优化性能
*/
class ImageLazyLoader {
constructor(selector = 'img.lazy') {
this.images = document.querySelectorAll(selector);
this.observer = null;
this.init();
}
// 初始化观察者
init() {
// 支持IntersectionObserver的浏览器(现代浏览器)
if ('IntersectionObserver' in window) {
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.observer.unobserve(entry.target); // 加载后停止观察
}
});
}, { rootMargin: '200px 0px' }); // 提前200px开始加载
// 观察所有懒加载图片
this.images.forEach(img => this.observer.observe(img));
} else {
// 降级方案:使用滚动监听
this.handleScroll = this.throttle(() => this.checkImages(), 100);
window.addEventListener('scroll', this.handleScroll);
this.checkImages(); // 初始检查
}
}
// 加载图片
loadImage(img) {
if (img.dataset.src && img.dataset.src !== img.src) {
const newImg = new Image();
newImg.src = img.dataset.src;
// 图片加载成功后更新src
newImg.onload = () => {
img.src = img.dataset.src;
img.classList.add('loaded'); // 添加加载完成的样式
};
// 处理加载失败
newImg.onerror = () => {
img.src = 'default-error-image.png'; // 占位图
};
}
}
// 检查可视区域内的图片(降级方案)
checkImages() {
this.images.forEach(img => {
if (this.isInViewport(img) && !img.classList.contains('loaded')) {
this.loadImage(img);
}
});
}
// 判断元素是否在视口内(降级方案)
isInViewport(el) {
const rect = el.getBoundingClientRect();
return (
rect.top <= window.innerHeight &&
rect.bottom >= 0 &&
rect.left <= window.innerWidth &&
rect.right >= 0
);
}
// 节流函数(用于降级方案)
throttle(fn, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
// 销毁资源
destroy() {
if (this.observer) {
this.images.forEach(img => this.observer.unobserve(img));
} else {
window.removeEventListener('scroll', this.handleScroll);
}
}
}
// 使用示例
// HTML结构:<img class="lazy" data-src="real-image.jpg" src="placeholder.jpg" alt="描述">
document.addEventListener('DOMContentLoaded', () => {
const lazyLoader = new ImageLazyLoader();
// 页面卸载时清理
window.addEventListener('unload', () => lazyLoader.destroy());
});
关键点解析
- 核心原理:通过
data-src
存储真实图片地址,进入视口后赋值给src
- 性能优化:
- 现代浏览器使用
IntersectionObserver
(性能优于滚动监听) - 提前加载(
rootMargin
)提升用户体验 - 加载失败处理(显示占位图)
- 现代浏览器使用
- 降级方案:兼容不支持
IntersectionObserver
的浏览器(如IE)
三、虚拟列表(Virtual List)
场景描述
当列表数据量极大(如10万+条)时,直接渲染所有DOM节点会导致页面卡顿,虚拟列表只渲染可视区域内的元素,大幅提升性能。
javascript
/**
* 虚拟列表组件
* 只渲染可视区域内的列表项,适用于大数据列表
*/
class VirtualList {
constructor(container, options) {
this.container = container; // 容器DOM
this.data = options.data || []; // 完整数据
this.itemHeight = options.itemHeight || 50; // 每项固定高度
this.buffer = options.buffer || 5; // 可视区域外的缓冲项数量
// 容器高度
this.containerHeight = container.clientHeight;
// 可视区域可显示的项数
this.visibleCount = Math.ceil(this.containerHeight / this.itemHeight);
// 总高度(用于滚动条)
this.totalHeight = this.data.length * this.itemHeight;
// 初始化DOM结构
this.initDOM();
// 绑定事件
this.bindEvents();
// 初始渲染
this.render();
}
// 初始化DOM结构
initDOM() {
// 外层容器(固定高度,overflow: auto)
this.container.style.overflow = 'auto';
this.container.style.position = 'relative';
// 滚动内容容器(用于撑开高度,显示滚动条)
this.scrollContent = document.createElement('div');
this.scrollContent.style.height = `${this.totalHeight}px`;
this.scrollContent.style.position = 'relative';
// 可视区域容器(绝对定位,只显示可视区域内容)
this.visibleContainer = document.createElement('div');
this.visibleContainer.style.position = 'absolute';
this.visibleContainer.style.top = '0';
this.visibleContainer.style.left = '0';
this.visibleContainer.style.width = '100%';
this.scrollContent.appendChild(this.visibleContainer);
this.container.appendChild(this.scrollContent);
}
// 绑定滚动事件
bindEvents() {
this.container.addEventListener('scroll', () => {
this.render(); // 滚动时重新渲染可视区域
});
}
// 计算需要渲染的项
getVisibleRange() {
// 滚动距离
const scrollTop = this.container.scrollTop;
// 起始索引(减去缓冲项)
const startIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.buffer);
// 结束索引(加上缓冲项和可视项数)
const endIndex = Math.min(
this.data.length - 1,
startIndex + this.visibleCount + this.buffer * 2
);
return { startIndex, endIndex };
}
// 渲染可视区域内容
render() {
const { startIndex, endIndex } = this.getVisibleRange();
// 截取需要渲染的数据
const visibleData = this.data.slice(startIndex, endIndex + 1);
// 计算可视容器的偏移量(让内容对齐滚动位置)
const offsetY = startIndex * this.itemHeight;
this.visibleContainer.style.transform = `translateY(${offsetY}px)`;
// 渲染列表项
this.visibleContainer.innerHTML = visibleData.map((item, index) => {
const actualIndex = startIndex + index;
return `
<div
class="virtual-item"
style="height: ${this.itemHeight}px; line-height: ${this.itemHeight}px; border-bottom: 1px solid #eee;"
>
第${actualIndex + 1}项: ${item.content}
</div>
`;
}).join('');
}
// 更新数据
updateData(newData) {
this.data = newData;
this.totalHeight = this.data.length * this.itemHeight;
this.scrollContent.style.height = `${this.totalHeight}px`;
this.render();
}
}
// 使用示例
// HTML: <div id="virtual-list-container" style="height: 500px; width: 300px;"></div>
document.addEventListener('DOMContentLoaded', () => {
// 生成10万条测试数据
const bigData = Array.from({ length: 100000 }, (_, i) => ({
content: `这是第${i + 1}条数据`
}));
const container = document.getElementById('virtual-list-container');
const virtualList = new VirtualList(container, {
data: bigData,
itemHeight: 50, // 每项高度50px
buffer: 5 // 上下各缓冲5项
});
// 如需更新数据
// virtualList.updateData(newData);
});
关键点解析
- 核心原理:通过计算滚动位置,只渲染"可视区域+缓冲区域"的元素
- 关键计算:
- 起始索引 = 滚动距离 / 项高 - 缓冲项
- 结束索引 = 起始索引 + 可视项数 + 缓冲项*2
- 偏移量 = 起始索引 * 项高(让内容对齐滚动位置)
- 适用场景:大数据表格、长列表(如订单列表、日志列表)
四、表单验证
场景描述
用户输入表单时,需要实时验证输入合法性(如手机号格式、密码强度),并给出友好提示,防止无效提交。
javascript
/**
* 表单验证工具
* 支持同步验证、异步验证、实时验证
*/
class FormValidator {
constructor(form, rules) {
this.form = form; // 表单DOM
this.rules = rules; // 验证规则
this.errors = {}; // 错误信息
this.init();
}
// 初始化
init() {
// 绑定输入事件(实时验证)
this.form.querySelectorAll('input, select, textarea').forEach(field => {
field.addEventListener('input', () => this.validateField(field.name));
field.addEventListener('blur', () => this.validateField(field.name));
});
// 绑定提交事件
this.form.addEventListener('submit', (e) => {
e.preventDefault();
if (this.validateAll()) {
console.log('表单验证通过,提交数据:', this.getFormData());
// 实际场景:调用接口提交
}
});
}
// 获取表单数据
getFormData() {
const data = {};
new FormData(this.form).forEach((value, key) => {
data[key] = value;
});
return data;
}
// 验证单个字段
async validateField(fieldName) {
const field = this.form.querySelector(`[name="${fieldName}"]`);
if (!field || !this.rules[fieldName]) return true;
const value = field.value;
const fieldRules = this.rules[fieldName];
let errorMessage = '';
// 遍历该字段的所有规则
for (const rule of fieldRules) {
// 同步验证
if (typeof rule.validator === 'function') {
const result = rule.validator(value, this.getFormData());
if (result !== true) {
errorMessage = result || rule.message;
break;
}
}
// 异步验证(如验证用户名是否已存在)
else if (typeof rule.asyncValidator === 'function') {
try {
await rule.asyncValidator(value, this.getFormData());
} catch (err) {
errorMessage = err.message || rule.message;
break;
}
}
}
// 更新错误信息
this.errors[fieldName] = errorMessage;
// 更新UI显示
this.updateFieldUI(field, errorMessage);
return !errorMessage;
}
// 验证所有字段
async validateAll() {
const fieldNames = Object.keys(this.rules);
let isValid = true;
// 先验证所有同步规则,再处理异步规则
for (const fieldName of fieldNames) {
const result = await this.validateField(fieldName);
if (!result) isValid = false;
}
return isValid;
}
// 更新字段UI(显示错误信息)
updateFieldUI(field, errorMessage) {
// 查找或创建错误提示元素
let errorEl = field.nextElementSibling;
if (!errorEl || !errorEl.classList.contains('error-message')) {
errorEl = document.createElement('div');
errorEl.className = 'error-message text-red-500 text-sm mt-1';
field.parentNode.insertBefore(errorEl, field.nextSibling);
}
// 更新错误信息和样式
errorEl.textContent = errorMessage;
field.classList.toggle('border-red-500', !!errorMessage);
field.classList.toggle('border-green-500', !errorMessage && field.value);
}
}
// 使用示例
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('user-form');
// 定义验证规则
const rules = {
username: [
{
validator: (value) => value.trim() !== '' || '用户名不能为空',
},
{
validator: (value) => value.length >= 3 || '用户名至少3个字符',
},
{
// 异步验证:检查用户名是否已存在
asyncValidator: async (value) => {
// 模拟API请求
const isExist = await new Promise(resolve => {
setTimeout(() => resolve(['admin', 'root'].includes(value)), 500);
});
if (isExist) throw new Error('用户名已被占用');
},
message: '用户名已被占用'
}
],
phone: [
{
validator: (value) => /^1[3-9]\d{9}$/.test(value) || '请输入正确的手机号',
}
],
password: [
{
validator: (value) => value.length >= 6 || '密码至少6个字符',
},
{
validator: (value) => /[A-Z]/.test(value) || '密码需包含大写字母',
}
],
confirmPassword: [
{
validator: (value, data) => value === data.password || '两次密码不一致',
}
]
};
// 初始化验证器
new FormValidator(form, rules);
});
关键点解析
- 核心功能:支持同步验证(如格式检查)和异步验证(如用户名唯一性)
- 实时反馈:输入和失焦时触发验证,及时提示错误
- 可扩展性:通过规则配置支持不同字段的个性化验证需求
- 常见验证场景:必填项、格式验证(手机号/邮箱)、密码强度、两次密码一致
五、权限控制
场景描述
根据用户角色(如管理员/普通用户)控制页面元素的显示/隐藏、路由访问权限,确保用户只能操作其权限范围内的功能。
javascript
/**
* 权限控制工具
* 支持路由权限、按钮权限控制
*/
class Permission {
constructor() {
// 初始化权限(实际场景从登录接口获取)
this.roles = []; // 用户角色
this.permissions = []; // 用户权限列表
}
// 初始化权限数据
init(roles, permissions) {
this.roles = roles;
this.permissions = permissions;
}
// 检查是否有指定角色
hasRole(role) {
return this.roles.includes(role);
}
// 检查是否有指定权限
hasPermission(permission) {
// 超级管理员拥有所有权限
if (this.roles.includes('admin')) return true;
return this.permissions.includes(permission);
}
// 路由权限守卫(模拟Vue Router导航守卫)
routerGuard(to, from, next) {
// 公开路由直接放行
if (to.meta.public) {
return next();
}
// 检查路由所需权限
const requiredPermission = to.meta.permission;
if (requiredPermission) {
if (this.hasPermission(requiredPermission)) {
next(); // 有权限,放行
} else {
next('/403'); // 无权限,跳转到403页面
}
} else {
// 无权限要求的路由(需登录)
if (this.roles.length > 0) {
next(); // 已登录,放行
} else {
next('/login'); // 未登录,跳转到登录页
}
}
}
// 渲染带权限的按钮
renderPermissionButton(containerId, buttons) {
const container = document.getElementById(containerId);
if (!container) return;
// 只渲染有权限的按钮
const html = buttons
.filter(btn => this.hasPermission(btn.permission))
.map(btn => `
<button
class="permission-btn ${btn.className}"
onclick="${btn.onClick}"
>
${btn.text}
</button>
`).join('');
container.innerHTML = html;
}
// 指令式权限控制(类似Vue指令v-permission)
registerPermissionDirective() {
// 查找所有带data-permission属性的元素
document.querySelectorAll('[data-permission]').forEach(el => {
const requiredPermission = el.getAttribute('data-permission');
// 无权限则隐藏元素
if (!this.hasPermission(requiredPermission)) {
el.style.display = 'none';
}
});
}
}
// 使用示例
// 1. 初始化权限(登录后)
const permission = new Permission();
// 假设登录后获取到的用户权限
permission.init(
['editor'], // 角色
['article:read', 'article:create', 'article:edit'] // 权限
);
// 2. 路由权限控制(模拟)
const toRoute = {
path: '/article/create',
meta: {
permission: 'article:create', // 该路由需要的权限
public: false
}
};
permission.routerGuard(toRoute, null, (path) => {
console.log('路由跳转:', path); // 有权限,跳转到目标路由
});
// 3. 渲染带权限的按钮
permission.renderPermissionButton('button-container', [
{
text: '查看文章',
permission: 'article:read',
className: 'btn-primary',
onClick: 'handleView()'
},
{
text: '创建文章',
permission: 'article:create',
className: 'btn-success',
onClick: 'handleCreate()'
},
{
text: '删除文章',
permission: 'article:delete', // 当前用户无此权限,不会渲染
className: 'btn-danger',
onClick: 'handleDelete()'
}
]);
// 4. 指令式权限控制
// HTML: <div data-permission="article:edit">编辑文章区域</div>
permission.registerPermissionDirective();
关键点解析
- 权限粒度:角色级(如admin)和权限项级(如article:delete)
- 控制层面:
- 路由层面:通过导航守卫限制访问
- 视图层面:隐藏无权限的按钮/区域
- 实现方式:
- 指令式:通过自定义属性标记需要权限控制的元素
- 函数式:主动检查权限后渲染内容
- 注意事项:前端权限只是辅助控制,真正的权限校验必须在后端实现
总结
以上场景覆盖了前端开发中性能优化(防抖节流、懒加载、虚拟列表)、用户交互(表单验证)、系统安全(权限控制)等核心领域。每个场景的实现都需要考虑:
- 性能:避免不必要的DOM操作和计算
- 兼容性:考虑不同浏览器的支持情况
- 可扩展性:设计通用方案,便于复用和修改
- 用户体验:如懒加载的过渡效果、表单验证的实时反馈
掌握这些场景的实现原理和最佳实践,不仅能应对面试挑战,更能在实际开发中写出高效、健壮的代码。