React JS: 양방향 무한 스크롤 모델링
이 어플리케이션에서는 무한 스크롤을 사용하여 이종 아이템의 방대한 목록을 탐색합니다.몇 가지 주름이 있습니다.
- 사용자에게는 10,000개의 아이템 리스트가 있어 3k+를 스크롤해야 하는 것이 일반적입니다.
- 이것들은 풍부한 아이템이기 때문에 브라우저의 퍼포먼스가 받아들여지지 않게 되기 전에 DOM에는 몇 백 개밖에 없습니다.
- 물건의 높이가 다르다.
- 아이템에는 이미지가 포함되어 있을 수 있으며 사용자가 특정 날짜로 이동할 수 있습니다.사용자가 리스트에서 뷰포트 위에 이미지를 로드해야 하는 지점으로 점프할 수 있기 때문에 콘텐츠를 로드할 때 아래로 밀릴 수 있습니다.이를 처리하지 못하면 사용자가 날짜로 점프했다가 이전 날짜로 이동할 수 있습니다.
알려진 불완전한 솔루션:
(react-infinite-scroll) - 이것은 단순한 "바닥에 닿았을 때 더 많은 부하를 가하는" 컴포넌트입니다.DOM은 모두 도태되지 않기 때문에 수천 개의 아이템으로 사망합니다.
(Respect로 스크롤 위치) - 상단에 삽입하거나 하단에 삽입할 때 스크롤 위치를 저장 및 복원하는 방법을 보여 줍니다. 단, 둘 다 함께 사용할 수는 없습니다.
완전한 솔루션을 위한 코드를 찾고 있는 것은 아닙니다(그렇다면 좋겠지만).대신, 저는 이 상황을 모델링하기 위한 "React 방법"을 찾고 있습니다.스크롤 위치 상태입니까?리스트 내 위치를 유지하려면 어떤 상태를 추적해야 합니까?렌더링된 내용의 하단 또는 상단 부근으로 스크롤할 때 새 렌더를 트리거하려면 어떤 상태를 유지해야 합니까?
이것은 무한 테이블과 무한 스크롤 시나리오가 혼합된 것입니다.이를 위해 찾은 최고의 추상화는 다음과 같습니다.
개요
<List>
모든 자식의 배열을 차지하는 컴포넌트.렌더링하지 않기 때문에 할당하고 폐기하는 것은 매우 저렴합니다.10k 할당이 너무 큰 경우 범위를 지정하는 함수를 전달하고 요소를 반환할 수 있습니다.
<List>
{thousandelements.map(function() { return <Element /> })}
</List>
의 ★★★★★★★★★★★★★★★★★.List
컴포넌트는 스크롤 위치를 추적하고 뷰에 있는 아이만 렌더링합니다.렌더링되지 않은 이전 항목을 위조하기 위해 시작 부분에 큰 빈 div를 추가합니다.
한 번 ㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇ는거에요.Element
합니다.List
이를 통해 스페이서의 높이를 계산하고 뷰에 표시되는 요소의 수를 알 수 있습니다.
이미지
되면 모든이 '가 된다는 죠.를 위한 태그에서 입니다. img 는 img입니다.<img src="..." width="100" height="58" />
이렇게 하면 브라우저가 어떤 크기로 표시되는지 알기 전에 다운로드 할 필요가 없습니다.여기에는 인프라스트럭처가 필요하지만 가치가 있습니다.
알 수 는, 「사이즈」를 붙여 .onload
이미지를 로드한 다음 표시된 치수를 측정하고 저장된 행 높이를 업데이트하고 스크롤 위치를 보정합니다.
랜덤 요소에 점프하기
리스트내의 임의의 요소에 액세스 할 필요가 있는 경우는, 그 사이에 있는 요소의 사이즈를 모르기 때문에, 스크롤 위치의 트릭이 필요합니다.제가 제안하는 것은 이미 계산한 요소의 높이를 평균화하고 마지막으로 알려진 높이 +(요소 수 * 평균)의 스크롤 위치로 점프하는 것입니다.
이 방법은 정확하지 않기 때문에 마지막으로 확인된 정상 위치로 돌아왔을 때 문제가 발생합니다.충돌이 발생하면 스크롤 위치를 변경하여 문제를 해결합니다.이렇게 하면 스크롤 막대가 약간 이동하지만 큰 영향은 없습니다.
대응의 상세
렌더링된 모든 요소에 키를 제공하여 렌더링된 요소가 여러 렌더링에서 유지되도록 합니다.두 가지 전략이 있습니다. (1) 키(0, 1, 2, ... n)는 n개뿐입니다.여기서 n은 표시 및 사용할 수 있는 요소의 최대 수이며, (2)는 요소별로 다른 키를 가집니다.모든 요소가 동일한 구조를 공유하는 경우 (1)을 사용하여 DOM 노드를 재사용하는 것이 좋습니다.그렇지 않으면 (2)를 사용한다.
반응 상태는 첫 번째 요소의 인덱스와 표시되는 요소의 수 두 개뿐입니다. '원소'에 되어 있습니다.this
·사용시setState
실제로 렌더를 실행하고 있는 것은 범위가 변경되었을 때 뿐입니다.
다음은 이 답변에서 설명하는 몇 가지 기술을 사용한 무한 목록의 예입니다.많은 작업이 필요하겠지만 무한 목록을 구현하는 데는 React가 단연 좋습니다.
http://adazzle.github.io/react-data-grid/index.html # Excel과 같은 기능과 풍부한 편집 기능(MIT 라이센스 있음)을 갖춘 느린 로드/최적화 렌더링을 갖춘 강력하고 퍼포먼스 높은 데이터 그래피드처럼 보입니다.아직 우리 프로젝트에서 시도하지 않았지만 곧 그렇게 할 것이다.
이러한 검색에는 http://react.rocks/ 이 경우 http://react.rocks/tag/InfiniteScroll도 도움이 됩니다.
단방향 무한 스크롤을 이종 아이템의 높이로 모델링하는 것과 같은 과제에 직면하고 있었기 때문에 솔루션에서 npm 패키지를 만들었습니다.
https://www.npmjs.com/package/react-variable-height-infinite-scroller
및 데모: http://tnrich.github.io/react-variable-height-infinite-scroller/
로직은 소스 코드를 확인하실 수 있지만, 저는 기본적으로 위의 답변에서 설명한 레시피 @Vjeux를 따랐습니다.아직 특정 아이템으로 넘어가는 것을 목표로 하고 있지 않지만, 그것을 빨리 실시해 나가고 싶다고 생각하고 있습니다.
현재 코드의 개요는 다음과 같습니다.
var React = require('react');
var areNonNegativeIntegers = require('validate.io-nonnegative-integer-array');
var InfiniteScoller = React.createClass({
propTypes: {
averageElementHeight: React.PropTypes.number.isRequired,
containerHeight: React.PropTypes.number.isRequired,
preloadRowStart: React.PropTypes.number.isRequired,
renderRow: React.PropTypes.func.isRequired,
rowData: React.PropTypes.array.isRequired,
},
onEditorScroll: function(event) {
var infiniteContainer = event.currentTarget;
var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
var currentAverageElementHeight = (visibleRowsContainer.getBoundingClientRect().height / this.state.visibleRows.length);
this.oldRowStart = this.rowStart;
var newRowStart;
var distanceFromTopOfVisibleRows = infiniteContainer.getBoundingClientRect().top - visibleRowsContainer.getBoundingClientRect().top;
var distanceFromBottomOfVisibleRows = visibleRowsContainer.getBoundingClientRect().bottom - infiniteContainer.getBoundingClientRect().bottom;
var rowsToAdd;
if (distanceFromTopOfVisibleRows < 0) {
if (this.rowStart > 0) {
rowsToAdd = Math.ceil(-1 * distanceFromTopOfVisibleRows / currentAverageElementHeight);
newRowStart = this.rowStart - rowsToAdd;
if (newRowStart < 0) {
newRowStart = 0;
}
this.prepareVisibleRows(newRowStart, this.state.visibleRows.length);
}
} else if (distanceFromBottomOfVisibleRows < 0) {
//scrolling down, so add a row below
var rowsToGiveOnBottom = this.props.rowData.length - 1 - this.rowEnd;
if (rowsToGiveOnBottom > 0) {
rowsToAdd = Math.ceil(-1 * distanceFromBottomOfVisibleRows / currentAverageElementHeight);
newRowStart = this.rowStart + rowsToAdd;
if (newRowStart + this.state.visibleRows.length >= this.props.rowData.length) {
//the new row start is too high, so we instead just append the max rowsToGiveOnBottom to our current preloadRowStart
newRowStart = this.rowStart + rowsToGiveOnBottom;
}
this.prepareVisibleRows(newRowStart, this.state.visibleRows.length);
}
} else {
//we haven't scrolled enough, so do nothing
}
this.updateTriggeredByScroll = true;
//set the averageElementHeight to the currentAverageElementHeight
// setAverageRowHeight(currentAverageElementHeight);
},
componentWillReceiveProps: function(nextProps) {
var rowStart = this.rowStart;
var newNumberOfRowsToDisplay = this.state.visibleRows.length;
this.props.rowData = nextProps.rowData;
this.prepareVisibleRows(rowStart, newNumberOfRowsToDisplay);
},
componentWillUpdate: function() {
var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
this.soonToBeRemovedRowElementHeights = 0;
this.numberOfRowsAddedToTop = 0;
if (this.updateTriggeredByScroll === true) {
this.updateTriggeredByScroll = false;
var rowStartDifference = this.oldRowStart - this.rowStart;
if (rowStartDifference < 0) {
// scrolling down
for (var i = 0; i < -rowStartDifference; i++) {
var soonToBeRemovedRowElement = visibleRowsContainer.children[i];
if (soonToBeRemovedRowElement) {
var height = soonToBeRemovedRowElement.getBoundingClientRect().height;
this.soonToBeRemovedRowElementHeights += this.props.averageElementHeight - height;
// this.soonToBeRemovedRowElementHeights.push(soonToBeRemovedRowElement.getBoundingClientRect().height);
}
}
} else if (rowStartDifference > 0) {
this.numberOfRowsAddedToTop = rowStartDifference;
}
}
},
componentDidUpdate: function() {
//strategy: as we scroll, we're losing or gaining rows from the top and replacing them with rows of the "averageRowHeight"
//thus we need to adjust the scrollTop positioning of the infinite container so that the UI doesn't jump as we
//make the replacements
var infiniteContainer = React.findDOMNode(this.refs.infiniteContainer);
var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
var self = this;
if (this.soonToBeRemovedRowElementHeights) {
infiniteContainer.scrollTop = infiniteContainer.scrollTop + this.soonToBeRemovedRowElementHeights;
}
if (this.numberOfRowsAddedToTop) {
//we're adding rows to the top, so we're going from 100's to random heights, so we'll calculate the differenece
//and adjust the infiniteContainer.scrollTop by it
var adjustmentScroll = 0;
for (var i = 0; i < this.numberOfRowsAddedToTop; i++) {
var justAddedElement = visibleRowsContainer.children[i];
if (justAddedElement) {
adjustmentScroll += this.props.averageElementHeight - justAddedElement.getBoundingClientRect().height;
var height = justAddedElement.getBoundingClientRect().height;
}
}
infiniteContainer.scrollTop = infiniteContainer.scrollTop - adjustmentScroll;
}
var visibleRowsContainer = React.findDOMNode(this.refs.visibleRowsContainer);
if (!visibleRowsContainer.childNodes[0]) {
if (this.props.rowData.length) {
//we've probably made it here because a bunch of rows have been removed all at once
//and the visible rows isn't mapping to the row data, so we need to shift the visible rows
var numberOfRowsToDisplay = this.numberOfRowsToDisplay || 4;
var newRowStart = this.props.rowData.length - numberOfRowsToDisplay;
if (!areNonNegativeIntegers([newRowStart])) {
newRowStart = 0;
}
this.prepareVisibleRows(newRowStart , numberOfRowsToDisplay);
return; //return early because we need to recompute the visible rows
} else {
throw new Error('no visible rows!!');
}
}
var adjustInfiniteContainerByThisAmount;
//check if the visible rows fill up the viewport
//tnrtodo: maybe put logic in here to reshrink the number of rows to display... maybe...
if (visibleRowsContainer.getBoundingClientRect().height / 2 <= this.props.containerHeight) {
//visible rows don't yet fill up the viewport, so we need to add rows
if (this.rowStart + this.state.visibleRows.length < this.props.rowData.length) {
//load another row to the bottom
this.prepareVisibleRows(this.rowStart, this.state.visibleRows.length + 1);
} else {
//there aren't more rows that we can load at the bottom so we load more at the top
if (this.rowStart - 1 > 0) {
this.prepareVisibleRows(this.rowStart - 1, this.state.visibleRows.length + 1); //don't want to just shift view
} else if (this.state.visibleRows.length < this.props.rowData.length) {
this.prepareVisibleRows(0, this.state.visibleRows.length + 1);
}
}
} else if (visibleRowsContainer.getBoundingClientRect().top > infiniteContainer.getBoundingClientRect().top) {
//scroll to align the tops of the boxes
adjustInfiniteContainerByThisAmount = visibleRowsContainer.getBoundingClientRect().top - infiniteContainer.getBoundingClientRect().top;
// this.adjustmentScroll = true;
infiniteContainer.scrollTop = infiniteContainer.scrollTop + adjustInfiniteContainerByThisAmount;
} else if (visibleRowsContainer.getBoundingClientRect().bottom < infiniteContainer.getBoundingClientRect().bottom) {
//scroll to align the bottoms of the boxes
adjustInfiniteContainerByThisAmount = visibleRowsContainer.getBoundingClientRect().bottom - infiniteContainer.getBoundingClientRect().bottom;
// this.adjustmentScroll = true;
infiniteContainer.scrollTop = infiniteContainer.scrollTop + adjustInfiniteContainerByThisAmount;
}
},
componentWillMount: function(argument) {
//this is the only place where we use preloadRowStart
var newRowStart = 0;
if (this.props.preloadRowStart < this.props.rowData.length) {
newRowStart = this.props.preloadRowStart;
}
this.prepareVisibleRows(newRowStart, 4);
},
componentDidMount: function(argument) {
//call componentDidUpdate so that the scroll position will be adjusted properly
//(we may load a random row in the middle of the sequence and not have the infinte container scrolled properly initially, so we scroll to the show the rowContainer)
this.componentDidUpdate();
},
prepareVisibleRows: function(rowStart, newNumberOfRowsToDisplay) { //note, rowEnd is optional
//setting this property here, but we should try not to use it if possible, it is better to use
//this.state.visibleRowData.length
this.numberOfRowsToDisplay = newNumberOfRowsToDisplay;
var rowData = this.props.rowData;
if (rowStart + newNumberOfRowsToDisplay > this.props.rowData.length) {
this.rowEnd = rowData.length - 1;
} else {
this.rowEnd = rowStart + newNumberOfRowsToDisplay - 1;
}
// var visibleRows = this.state.visibleRowsDataData.slice(rowStart, this.rowEnd + 1);
// rowData.slice(rowStart, this.rowEnd + 1);
// setPreloadRowStart(rowStart);
this.rowStart = rowStart;
if (!areNonNegativeIntegers([this.rowStart, this.rowEnd])) {
var e = new Error('Error: row start or end invalid!');
console.warn('e.trace', e.trace);
throw e;
}
var newVisibleRows = rowData.slice(this.rowStart, this.rowEnd + 1);
this.setState({
visibleRows: newVisibleRows
});
},
getVisibleRowsContainerDomNode: function() {
return this.refs.visibleRowsContainer.getDOMNode();
},
render: function() {
var self = this;
var rowItems = this.state.visibleRows.map(function(row) {
return self.props.renderRow(row);
});
var rowHeight = this.currentAverageElementHeight ? this.currentAverageElementHeight : this.props.averageElementHeight;
this.topSpacerHeight = this.rowStart * rowHeight;
this.bottomSpacerHeight = (this.props.rowData.length - 1 - this.rowEnd) * rowHeight;
var infiniteContainerStyle = {
height: this.props.containerHeight,
overflowY: "scroll",
};
return (
<div
ref="infiniteContainer"
className="infiniteContainer"
style={infiniteContainerStyle}
onScroll={this.onEditorScroll}
>
<div ref="topSpacer" className="topSpacer" style={{height: this.topSpacerHeight}}/>
<div ref="visibleRowsContainer" className="visibleRowsContainer">
{rowItems}
</div>
<div ref="bottomSpacer" className="bottomSpacer" style={{height: this.bottomSpacerHeight}}/>
</div>
);
}
});
module.exports = InfiniteScoller;
언급URL : https://stackoverflow.com/questions/20870448/reactjs-modeling-bi-directional-infinite-scrolling
'programing' 카테고리의 다른 글
ORA-01652: 테이블스페이스에서 온도 세그먼트를 128까지 연장할 수 없습니다.시스템: 연장 방법 (0) | 2023.03.05 |
---|---|
AngularJS 1.5의 구성 요소와 지침 간의 주요 차이점은 무엇입니까? (0) | 2023.03.05 |
엔드 투 엔드 테스트에 프로젝터 또는 카르마를 사용해야 합니까? (0) | 2023.02.28 |
AuthorizationRequest를 대체한 후 HttpSession이 null입니다. (0) | 2023.02.28 |
node.js를 사용하여 Excel 파일을 읽는 중 (0) | 2023.02.28 |