動手做 Virtual Scroll (ft. React)

tags: JavaScript React
category: Front-End
description: 動手做 Virtual Scroll
created_at: 2021/08/15 13:00:00
set: 技能競賽

cover image


前言

會做這個的主要原因是最近想說拿技能競賽的題目練一下手,結果意外發現這次要選手做這樣的功能,(比賽沒有提供套件也沒網路所以他要什麼都要自己做),這邊順便提一下技能競賽的模式,會在事前 2 ~ 3 個月左右公開部分題目給選手練習,正式比賽大概會有3成改變,雖然說是3成,但整題抽掉大改的情況也是有

不過如果真的要做還是用現成的套件吧,功能也比較完整,BUG應該也比較少,畢竟經過多數人的驗證,有BUG應該有修的差不多了(?),所以當作概念看看就好,不過如果你是選手,你應該要會做。


做法

做這個功能至少有兩種做法:

  1. 在外層包一層容器,裡面有兩個東西,一個是把空間展開的元素(製造出Scroll),一個是你要呈現的資料,然後每次呈現的資料只有外層容器的大小
  2. 不在外層包一層容器,直接在裡面加上一個把空間展開的元素(一樣是製造出Scroll),然後每次捲的時候把裡面的元素做位移

而這邊使用第二個方法,沒有為什麼,因為假設如果我有比賽我會這麼做

比賽會追求速度,畢竟時間不多,題目需要敲出1000多行程式碼時間卻只有約莫3小時。而且1000出頭行算少,通常都是經過多次練習重構後的。

題外話: 想當初有一題SSR(Server Side Render)題目我第一次做花了我3天寫了2000行,最後也才壓到1500行左右,不過省500行也是很不錯了,假設比賽給你3個小時,就是平均攤下來:

  • 2000行: 2000 / 180 = 11.111行 / 分
  • 1500行: 1500 / 180 = 8.3333行 / 分

這個比賽的題目不一定能做的完,所以就是相同時間大家競爭。


直接看結果

See the Pen Virtual Scroll With JavaScript by 賴俊賓 (@laijunbin) on CodePen.


先來看看HTML

雖然沒什麼好看的

<ul>
</ul>

然後看一下CSS

/* 簡單重設一些樣式 */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

/* 外層容器寬度,超出就變成卷軸,清除list預設樣式 */
ul {
    width: 500px;
    overflow-y: scroll;
    list-style-type: none;
}

/* 因為我是直接塞進去填充,所以除了最後一個都要設定樣式 */
li:not(:last-child) {
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid #000;
}

如果沒有過濾 last-child ,你會發現怎麼往下滾一些些多了一個框框(?),因為那個負責擴充空間的 li 也被套用到樣式了。


最後是JS

const itemLength = 100;  // 產生幾個假資料
const viewSize = 10;     // 畫面上要顯示幾個元素
const liHeight = 40;     // 每個清單的高度

// 建立假資料的陣列
const items = new Array(itemLength).fill(0).map((x, i) => i + 1);
const ul = document.querySelector('ul'); // 抓到ul
const list = []; // 存放所有的li

// 計算ul的高度
ul.style.height = `${viewSize * liHeight}px`;

// 從items前面走viewSize個
items.slice(0, viewSize).forEach(item => {
    const li = document.createElement('li'); // 建立li
    li.innerText = item; // 設定內容
    li.style.height = `${liHeight}px`; // 設定高度
    ul.appendChild(li); // 把li丟進ul
    list.push(li); // 把DOM存起來
});

// 建立最後一個負責撐開元素的li
const keepLi = document.createElement('li');
// 計算高度
keepLi.style.height = `${(items.length - viewSize) * liHeight}px`;
// 一樣塞進ul
ul.appendChild(keepLi);

// 監聽捲動事件
ul.addEventListener('scroll', e => {
    // 抓到scroll距離top的距離
    const scrollTop = e.target.scrollTop;
    // 算出現在應該從第幾個元素開始顯示
    const startIndex = Math.floor(scrollTop / liHeight);

    // 從items第startIndex個元素開始抓viewSize個
    items.slice(startIndex, startIndex + viewSize).forEach((item, i) => {
        // 更新list[i]的文字
        list[i].innerText = item;
        // 讓他Y軸(上下)位移
        list[i].style.transform = `translateY(${scrollTop}px)`;
    });
});

這就是原生 JavaScript 的實現方式,需要操作 DOM 顯得有點冗長,如果把它換成 React 的形式會更精簡一些。


一樣先來看成品(React)

See the Pen Virtual Scroll With JavaScript by 賴俊賓 (@laijunbin) on CodePen.

CSS 基本上沒改, HTML 只改成一行

<div id="root"></div>

再來看看 JavaScript

function App({itemLength, viewSize, liHeight}) {
  const items = new Array(itemLength).fill(0).map((x, i) => i + 1);
  const [startIndex, setStartIndex] = useState(0);
  const [scrollTop, setScrollTop] = useState(0);

  React.useEffect(() => {
    setStartIndex(Math.floor(scrollTop / liHeight));
  }, [scrollTop]);

  return (
    <ul style={{height: viewSize * liHeight}} onScroll={(e) => setScrollTop(e.target.scrollTop)}>
      {items.slice(startIndex, startIndex + viewSize).map((item, i) => (
        <li style={{height: liHeight, transform: `translateY(${scrollTop}px)`}} key={i}>
          {item}
        </li>
      ))}
      <li style={{height: (items.length - viewSize) * liHeight}}></li>
    </ul>
  );
}


ReactDOM.render(
  <App itemLength={100} viewSize={10} liHeight={40} />,
  document.getElementById('root')
)



最後更新時間: 2021年08月15日.