From 055c69de609ec14c4953c583931f3d45050878e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 19 Sep 2023 20:48:03 +0800 Subject: [PATCH 1/6] refactor: use layout effect for scroll --- src/hooks/useScrollTo.tsx | 166 ++++++++++++++++++++++---------------- 1 file changed, 97 insertions(+), 69 deletions(-) diff --git a/src/hooks/useScrollTo.tsx b/src/hooks/useScrollTo.tsx index 4bc038f7..a41aee5d 100644 --- a/src/hooks/useScrollTo.tsx +++ b/src/hooks/useScrollTo.tsx @@ -3,6 +3,9 @@ import * as React from 'react'; import raf from 'rc-util/lib/raf'; import type { GetKey } from '../interface'; import type CacheMap from '../utils/CacheMap'; +import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect'; + +const MAX_TIMES = 3; export type ScrollAlign = 'top' | 'bottom' | 'auto'; @@ -35,6 +38,94 @@ export default function useScrollTo( ): (arg: number | ScrollTarget) => void { const scrollRef = React.useRef(); + const [syncState, setSyncState] = React.useState<{ + times: number; + index: number; + offset: number; + originAlign: ScrollAlign; + targetAlign?: 'top' | 'bottom'; + }>(null); + + // ========================== Sync Scroll ========================== + useLayoutEffect(() => { + if (syncState && syncState.times < MAX_TIMES) { + // Never reach + if (!containerRef.current) { + setSyncState((ori) => ({ ...ori })); + return; + } + + const { targetAlign, originAlign, index, offset } = syncState; + + const height = containerRef.current.clientHeight; + let needCollectHeight = false; + let newTargetAlign: 'top' | 'bottom' | null = targetAlign; + + // Go to next frame if height not exist + if (height) { + const mergedAlign = targetAlign || originAlign; + + // Get top & bottom + let stackTop = 0; + let itemTop = 0; + let itemBottom = 0; + + const maxLen = Math.min(data.length, index); + + for (let i = 0; i <= maxLen; i += 1) { + const key = getKey(data[i]); + itemTop = stackTop; + const cacheHeight = heights.get(key); + itemBottom = itemTop + (cacheHeight === undefined ? itemHeight : cacheHeight); + + stackTop = itemBottom; + + if (i === index && cacheHeight === undefined) { + needCollectHeight = true; + } + } + + // Scroll to + let targetTop: number | null = null; + + switch (mergedAlign) { + case 'top': + targetTop = itemTop - offset; + break; + case 'bottom': + targetTop = itemBottom - height + offset; + break; + + default: { + const { scrollTop } = containerRef.current; + const scrollBottom = scrollTop + height; + if (itemTop < scrollTop) { + newTargetAlign = 'top'; + } else if (itemBottom > scrollBottom) { + newTargetAlign = 'bottom'; + } + } + } + + if (targetTop !== null && targetTop !== containerRef.current.scrollTop) { + syncScrollTop(targetTop); + } + } + + // Trigger next effect + if (needCollectHeight) { + collectHeight(); + } + + setSyncState((ori) => ({ + ...ori, + times: ori.times + 1, + targetAlign: newTargetAlign, + })); + } + }, [syncState, containerRef.current]); + + // =========================== Scroll To =========================== return (arg) => { // When not argument provided, we think dev may want to show the scrollbar if (arg === null || arg === undefined) { @@ -59,75 +150,12 @@ export default function useScrollTo( const { offset = 0 } = arg; - // We will retry 3 times in case dynamic height shaking - const syncScroll = (times: number, targetAlign?: 'top' | 'bottom') => { - if (times < 0 || !containerRef.current) return; - - const height = containerRef.current.clientHeight; - let needCollectHeight = false; - let newTargetAlign: 'top' | 'bottom' | null = targetAlign; - - // Go to next frame if height not exist - if (height) { - const mergedAlign = targetAlign || align; - - // Get top & bottom - let stackTop = 0; - let itemTop = 0; - let itemBottom = 0; - - const maxLen = Math.min(data.length, index); - - for (let i = 0; i <= maxLen; i += 1) { - const key = getKey(data[i]); - itemTop = stackTop; - const cacheHeight = heights.get(key); - itemBottom = itemTop + (cacheHeight === undefined ? itemHeight : cacheHeight); - - stackTop = itemBottom; - - if (i === index && cacheHeight === undefined) { - needCollectHeight = true; - } - } - - // Scroll to - let targetTop: number | null = null; - - switch (mergedAlign) { - case 'top': - targetTop = itemTop - offset; - break; - case 'bottom': - targetTop = itemBottom - height + offset; - break; - - default: { - const { scrollTop } = containerRef.current; - const scrollBottom = scrollTop + height; - if (itemTop < scrollTop) { - newTargetAlign = 'top'; - } else if (itemBottom > scrollBottom) { - newTargetAlign = 'bottom'; - } - } - } - - if (targetTop !== null && targetTop !== containerRef.current.scrollTop) { - syncScrollTop(targetTop); - } - } - - // We will retry since element may not sync height as it described - scrollRef.current = raf(() => { - if (needCollectHeight) { - collectHeight(); - } - syncScroll(times - 1, newTargetAlign); - }, 2); // Delay 2 to wait for List collect heights - }; - - syncScroll(3); + setSyncState({ + times: 0, + index, + offset, + originAlign: align, + }); } }; } From 8e717de8b55f56039f2f1512a57cd2ded3a8afe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 19 Sep 2023 21:52:38 +0800 Subject: [PATCH 2/6] refactor: record heights --- examples/basic.tsx | 19 ++++++++-------- src/List.tsx | 2 +- src/hooks/useHeights.tsx | 19 ++++++++++++---- src/hooks/useScrollTo.tsx | 46 ++++++++++++++++++++++++++++++--------- 4 files changed, 61 insertions(+), 25 deletions(-) diff --git a/examples/basic.tsx b/examples/basic.tsx index 5d50c92a..2f14a202 100644 --- a/examples/basic.tsx +++ b/examples/basic.tsx @@ -1,18 +1,17 @@ -/* eslint-disable jsx-a11y/label-has-associated-control, jsx-a11y/label-has-for */ import * as React from 'react'; -import List, { ListRef } from '../src/List'; +import List, { type ListRef } from '../src/List'; import './basic.less'; interface Item { - id: string; + id: number; } const MyItem: React.ForwardRefRenderFunction = ({ id }, ref) => ( { console.log('Click:', id); @@ -35,7 +34,7 @@ class TestItem extends React.Component { const data: Item[] = []; for (let i = 0; i < 1000; i += 1) { data.push({ - id: String(i), + id: i, }); } @@ -44,7 +43,7 @@ const TYPES = [ { name: 'ref react node', type: 'react', component: TestItem }, ]; -const onScroll: React.UIEventHandler = e => { +const onScroll: React.UIEventHandler = (e) => { console.log('scroll:', e.currentTarget.scrollTop); }; @@ -160,7 +159,7 @@ const Demo = () => { type="button" onClick={() => { listRef.current.scrollTo({ - key: '50', + key: 50, align: 'auto', }); }} @@ -171,7 +170,7 @@ const Demo = () => {