鲁斯前端布鲁斯前端

文章中英模式

布鲁斯前端JS面试题目 - 实现数据获取与 UI 更新

学习如何高效实现前端数据获取与 UI 更新流程,掌握异步数据处理、加载状态管理、错误处理及优化渲染的最佳实践。

影片縮圖

懒得看文章?那就来看视频吧

数据获取与 UI 更新概述

在前端开发中,从服务器获取数据并更新用户界面是一个基本但关键的任务。这个过程涉及异步请求处理、加载状态管理、错误处理以及高效的 UI 更新策略。设计良好的数据获取与 UI 更新流程是建立良好用户体验的基础。

主要挑战与考量

  • 1. 异步处理:正确处理网络请求的异步性质
  • 2. 加载状态:在数据加载期间提供适当的视觉反馈
  • 3. 错误处理:优雅地处理可能出现的网络或数据错误
  • 4. 数据转换:将API响应转换为适合UI呈现的格式
  • 5. 优化渲染:避免不必要的渲染以提高性能
  • 6. 数据缓存:实现适当的缓存策略以减少重复请求

基本的数据获取与 UI 更新实现

以下是一个基本的数据获取与 UI 更新示例,使用原生 JavaScript 和 DOM API:

// 基本的数据获取与 UI 更新实现
document.addEventListener('DOMContentLoaded', function() {
  const userListElement = document.getElementById('user-list');
  const loadingElement = document.getElementById('loading');
  const errorElement = document.getElementById('error');
  
  // 显示加载中的状态
  function showLoading() {
    loadingElement.style.display = 'block';
    errorElement.style.display = 'none';
    userListElement.innerHTML = '';
  }
  
  // 显示错误信息
  function showError(message) {
    loadingElement.style.display = 'none';
    errorElement.style.display = 'block';
    errorElement.textContent = message || '发生错误,请稍后再试';
    userListElement.innerHTML = '';
  }
  
  // 显示数据
  function renderUsers(users) {
    loadingElement.style.display = 'none';
    errorElement.style.display = 'none';
    userListElement.innerHTML = '';
    
    if (users.length === 0) {
      userListElement.innerHTML = '<p>没有找到用户</p>';
      return;
    }
    
    const userElements = users.map(user => {
      return `
        <div class="user-card">
          <h3>${user.name}</h3>
          <p>Email: ${user.email}</p>
        </div>
      `;
    });
    
    userListElement.innerHTML = userElements.join('');
  }
  
  // 获取用户数据
  async function fetchUsers() {
    try {
      showLoading();
      
      const response = await fetch('https://jsonplaceholder.typicode.com/users');
      
      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
      }
      
      const users = await response.json();
      renderUsers(users);
    } catch (error) {
      console.error('获取用户数据失败:', error);
      showError('无法加载用户数据: ' + error.message);
    }
  }
  
  // 添加重试按钮事件处理
  document.getElementById('retry-button').addEventListener('click', fetchUsers);
  
  // 初始数据获取
  fetchUsers();
});

对应的 HTML 结构如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>用户列表</title>
  <style>
    .user-card {
      border: 1px solid #ccc;
      padding: 15px;
      margin-bottom: 10px;
      border-radius: 4px;
    }
    #loading, #error {
      display: none;
      padding: 15px;
    }
    #error {
      color: red;
    }
  </style>
</head>
<body>
  <h1>用户列表</h1>
  
  <div id="loading">加载中,请稍候...</div>
  <div id="error"></div>
  <button id="retry-button">重试</button>
  
  <div id="user-list"></div>
  
  <script src="app.js"></script>
</body>
</html>

React 虚拟列表简单实现

以下是一个简洁的 React 虚拟列表实现,只渲染可视区域内的元素:

import React, { useState, useEffect, useRef } from 'react';

const VirtualList = ({ items, itemHeight, containerHeight = 400 }) => {
  // 追踪滚动位置的状态
  const [scrollTop, setScrollTop] = useState(0);
  // 参考容器DOM元素
  const containerRef = useRef(null);
  // 用于防止过度渲染的标记
  const ticking = useRef(false);

  // 计算可见项目数量(多渲染2个项目作为缓冲)
  const visibleCount = Math.ceil(containerHeight / itemHeight) + 2;
  // 计算开始索引(减1是为了预先渲染一个项目)
  const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - 1);
  // 计算结束索引(确保不超出数组范围)
  const endIndex = Math.min(items.length - 1, startIndex + visibleCount);

  // 滚动事件处理函数,使用requestAnimationFrame优化性能
  const onScroll = (e) => {
    const newTop = e.target.scrollTop;
    if (!ticking.current) {
      requestAnimationFrame(() => {
        setScrollTop(newTop);
        ticking.current = false;
      });
      ticking.current = true;
    }
  };

  return (
    <div
      ref={containerRef}
      onScroll={onScroll}
      style={{ height: containerHeight, overflow: 'auto', position: 'relative' }}
    >
      {/* 创建一个与所有项目总高度相同的容器 */}
      <div style={{ height: items.length * itemHeight }}>
        {/* 只渲染可见范围内的项目 */}
        {items.slice(startIndex, endIndex + 1).map((item, i) => {
          const index = startIndex + i;
          return (
            <div
              key={index}
              style={{
                position: 'absolute', // 使用绝对定位
                top: index * itemHeight, // 根据索引计算正确的位置
                height: itemHeight,
                width: '100%',
              }}
            >
              {item}
            </div>
          );
        })}
      </div>
    </div>
  );
};

// 使用示例
const App = () => {
  // 生成1000个测试项目
  const items = Array.from({ length: 1000 }, (_, i) => (
    <div style={{ padding: '10px', borderBottom: '1px solid #eee' }}>
      项目 #{i + 1}
    </div>
  ));
  
  return (
    <div>
      <h2>虚拟列表示例</h2>
      <VirtualList
        items={items}
        itemHeight={50}
        containerHeight={300}
      />
    </div>
  );
};

核心原理:

  • 1. 只渲染可见区域的项目
  • 2. 使用绝对定位放置元素
  • 3. 容器高度固定,内容高度等于所有项目总高度
  • 4. 根据滚动位置动态计算显示的项目