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

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. 用户体验:如懒加载的过渡效果、表单验证的实时反馈

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

相关推荐
运维帮手大橙子17 分钟前
完整的登陆学生管理系统(配置数据库)
java·前端·数据库·eclipse·intellij-idea
_Kayo_1 小时前
CSS BFC
前端·css
二哈喇子!2 小时前
Vue3 组合式API
前端·javascript·vue.js
二哈喇子!4 小时前
Vue 组件化开发
前端·javascript·vue.js
chxii4 小时前
2.9 插槽
前端·javascript·vue.js
姑苏洛言5 小时前
扫码点餐小程序产品需求分析与功能梳理
前端·javascript·后端
Freedom风间5 小时前
前端必学-完美组件封装原则
前端·javascript·设计模式
江城开朗的豌豆5 小时前
React表单控制秘籍:受控组件这样玩就对了!
前端·javascript·react.js
一枚前端小能手5 小时前
📋 代码片段管理大师 - 5个让你的代码复用率翻倍的管理技巧
前端·javascript
国家不保护废物6 小时前
Web Worker 多线程魔法:告别卡顿,轻松实现图片压缩!😎
前端·javascript·面试