魯斯前端布魯斯前端

文章中英模式

布魯斯前端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-TW">
<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. 根據滾動位置動態計算顯示的項目