動手做 Virtual Scroll (ft. React)
tags: JavaScript
React
category: Front-End
description: 動手做 Virtual Scroll
created_at: 2021/08/15 13:00:00
set: 技能競賽
前言
會做這個的主要原因是最近想說拿技能競賽的題目練一下手,結果意外發現這次要選手做這樣的功能,(比賽沒有提供套件也沒網路所以他要什麼都要自己做),這邊順便提一下技能競賽的模式,會在事前 2 ~ 3 個月左右公開部分題目給選手練習,正式比賽大概會有3成改變,雖然說是3成,但整題抽掉大改的情況也是有。
不過如果真的要做還是用現成的套件吧,功能也比較完整,BUG應該也比較少,畢竟經過多數人的驗證,有BUG應該有修的差不多了(?),所以當作概念看看就好,不過如果你是選手,你應該要會做。
做法
做這個功能至少有兩種做法:
- 在外層包一層容器,裡面有兩個東西,一個是把空間展開的元素(製造出Scroll),一個是你要呈現的資料,然後每次呈現的資料只有外層容器的大小
- 不在外層包一層容器,直接在裡面加上一個把空間展開的元素(一樣是製造出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')
)