前端应用场景题目(待总结优化)

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)
  • 控制层面:
    • 路由层面:通过导航守卫限制访问
    • 视图层面:隐藏无权限的按钮/区域
  • 实现方式:
    • 指令式:通过自定义属性标记需要权限控制的元素
    • 函数式:主动检查权限后渲染内容
  • 注意事项:前端权限只是辅助控制,真正的权限校验必须在后端实现

总结

以上场景覆盖了前端开发中性能优化(防抖节流、懒加载、虚拟列表)、用户交互(表单验证)、系统安全(权限控制)等核心领域。每个场景的实现都需要考虑:

  1. 性能:避免不必要的DOM操作和计算
  2. 兼容性:考虑不同浏览器的支持情况
  3. 可扩展性:设计通用方案,便于复用和修改
  4. 用户体验:如懒加载的过渡效果、表单验证的实时反馈

掌握这些场景的实现原理和最佳实践,不仅能应对面试挑战,更能在实际开发中写出高效、健壮的代码。

相关推荐
无双_Joney15 分钟前
[更新迭代 - 1] Nestjs 在24年底更新了啥?(功能篇)
前端·后端·nestjs
在云端易逍遥16 分钟前
前端必学的 CSS Grid 布局体系
前端·css
EMT16 分钟前
在 Vue 项目中使用 URL Query 保存和恢复搜索条件
javascript·vue.js
ccnocare18 分钟前
选择文件夹路径
前端
艾小码18 分钟前
还在被超长列表卡到崩溃?3招搞定虚拟滚动,性能直接起飞!
前端·javascript·react.js
闰五月19 分钟前
JavaScript作用域与作用域链详解
前端·面试
泉城老铁22 分钟前
idea 优化卡顿
前端·后端·敏捷开发
前端康师傅22 分钟前
JavaScript 作用域常见问题及解决方案
前端·javascript
司宸24 分钟前
Prompt结构化输出:从入门到精通的系统指南
前端