Dim*_*nis 68 javascript list reactjs
我正在使用React实现可过滤列表.列表的结构如下图所示.
前提
以下是它应该如何工作的描述:
Search组件中.
{
visible : boolean,
files : array,
filtered : array,
query : string,
currentlySelectedIndex : integer
}
files 是一个可能非常大的数组,包含文件路径(10000个条目是一个合理的数字).filtered是用户键入至少2个字符后的过滤数组.我知道它是衍生数据,因此可以将其存储在状态中,但需要它currentlySelectedIndex 这是筛选列表中当前所选元素的索引.
用户在Input组件中键入2个以上的字母,对数组进行过滤,对于过滤后的数组中的每个条目,Result都会呈现一个组件
每个Result组件都显示部分匹配查询的完整路径,并突出显示路径的部分匹配部分.例如,Result组件的DOM,如果用户键入'le',则类似于:
<li>this/is/a/fi<strong>le</strong>/path</li>
Input组件聚焦时按下向上或向下键,则currentlySelectedIndex基于filtered阵列进行更改.这会导致Result与索引匹配的组件被标记为选中,从而导致重新呈现问题
最初我用一个足够小的数组测试了这个files,使用了React的开发版本,并且一切正常.
当我不得不处理一个files大到10000个条目的数组时,问题出现了.在输入中键入2个字母会生成一个大列表,当我按下向上和向下键进行导航时,它会非常滞后.
起初我没有为Result元素定义组件,我只是在Search组件的每个渲染上动态制作列表,如下:
results = this.state.filtered.map(function(file, index) {
var start, end, matchIndex, match = this.state.query;
matchIndex = file.indexOf(match);
start = file.slice(0, matchIndex);
end = file.slice(matchIndex + match.length);
return (
<li onClick={this.handleListClick}
data-path={file}
className={(index === this.state.currentlySelected) ? "valid selected" : "valid"}
key={file} >
{start}
<span className="marked">{match}</span>
{end}
</li>
);
}.bind(this));
Run Code Online (Sandbox Code Playgroud)
正如您所知,每次currentlySelectedIndex更改时,都会导致重新渲染,并且每次都会重新创建列表.我认为,因为我key在每个li元素上设置了一个值,所以React会避免重新渲染其他li没有className变化的元素,但显然不是这样.
我最后为Result元素定义了一个类,它Result根据先前是否选择并基于当前用户输入显式检查每个元素是否应该重新呈现:
var ResultItem = React.createClass({
shouldComponentUpdate : function(nextProps) {
if (nextProps.match !== this.props.match) {
return true;
} else {
return (nextProps.selected !== this.props.selected);
}
},
render : function() {
return (
<li onClick={this.props.handleListClick}
data-path={this.props.file}
className={
(this.props.selected) ? "valid selected" : "valid"
}
key={this.props.file} >
{this.props.children}
</li>
);
}
});
Run Code Online (Sandbox Code Playgroud)
现在列表就是这样创建的:
results = this.state.filtered.map(function(file, index) {
var start, end, matchIndex, match = this.state.query, selected;
matchIndex = file.indexOf(match);
start = file.slice(0, matchIndex);
end = file.slice(matchIndex + match.length);
selected = (index === this.state.currentlySelected) ? true : false
return (
<ResultItem handleClick={this.handleListClick}
data-path={file}
selected={selected}
key={file}
match={match} >
{start}
<span className="marked">{match}</span>
{end}
</ResultItem>
);
}.bind(this));
}
Run Code Online (Sandbox Code Playgroud)
这使性能略好一些,但仍然不够好.事情是,当我在React的生产版本上测试时,工作黄油顺利,没有任何延迟.
BOTTOMLINE
React的开发版和生产版之间是否存在明显的差异?
当我想到React如何管理列表时,我是否理解/做错了什么?
更新2016年11月14日
我找到了迈克尔·杰克逊的这个演讲,在那里他解决了一个与这个非常类似的问题:https://youtu.be/7S8v8jfLb1Q?t = 26m2s
解决方案与AskarovBeknar的答案非常相似,如下所示
更新14-4-2018
由于这显然是一个受欢迎的问题,并且自从问到原始问题后事情已经取得进展,而我鼓励您观看上面链接的视频,为了掌握虚拟布局,我还鼓励您使用React Virtualized库如果你不想重新发明轮子.
deo*_*owk 13
与此问题的许多其他答案一样,主要问题在于,在进行过滤和处理关键事件时,在DOM中呈现如此多的元素将会很慢.
关于引起问题的React,你没有做任何本质上错误的事情,但是像许多与性能相关的问题,UI也可能占据很大一部分责任.
如果您的UI设计时没有考虑到效率,那么即使像React这样的高性能工具也会受到影响.
正如@Koen所提到的,过滤结果集是一个很好的开始
我已经玩了一下这个想法并创建了一个示例应用程序,说明我如何开始解决这类问题.
这绝不是production ready代码,但它确实充分说明了这个概念,可以修改为更健壮,随意查看代码 - 我希望它至少能给你一些想法......;)
https://github.com/deowk/react-large-list-example
Res*_*nce 12
我遇到一个非常类似的问题的经验是,如果DOM中的组件超过100-200个,那么反应真的会受到影响.即使您非常小心(通过设置所有键和/或实现shouldComponentUpdate方法)只改变重新渲染中的一个或两个组件,您仍然会处于一个受伤的世界.
目前反应的缓慢部分是它比较虚拟DOM和真实DOM之间的差异.如果你有数千个组件但只更新了一对,那没关系,反应仍然在DOM之间做了大量的差异操作.
当我现在编写页面时,我尝试设计它们以最小化组件数量,在渲染大型组件列表时,这样做的一种方法是......好吧......不渲染大型组件列表.
我的意思是:只渲染当前可以看到的组件,在向下滚动时渲染更多,用户不可能以任何方式向下滚动数千个组件....我希望.
这样做的好方法是:
https://www.npmjs.com/package/react-infinite-scroll
这里有一个很棒的方法:
http://www.reactexamples.com/react-infinite-scroll/
我担心它不会删除页面顶部的组件,所以如果滚动的时间足够长,那么性能问题就会重新出现.
我知道提供链接作为答案并不是一个好习惯,但他们提供的示例将解释如何比我在这里更好地使用这个库.希望我已经解释了为什么大名单不好,但也解决了.
Pie*_*scy 10
首先,React的开发版和生产版之间的差异很大,因为在生产中有许多绕过的健全性检查(例如道具类型验证).
然后,我认为你应该重新考虑使用Redux,因为它对你需要的东西(或任何类型的助焊器实现)非常有帮助.您应该明确地看一下这个演示文稿:Big List High Performance React&Redux.
但是在深入研究redux之前,你需要通过将组件拆分成更小的组件来对你的React代码做一些讨论,因为它shouldComponentUpdate会完全绕过孩子的渲染,所以这是一个巨大的收获.
如果有更多粒度组件,则可以使用redux和react-redux处理状态,以更好地组织数据流.
当我需要渲染一千行并且能够通过编辑其内容来修改每一行时,我最近遇到了类似的问题.这个迷你应用程序显示一个可能重复音乐会的音乐会列表,如果我想通过选中复选框将潜在副本标记为原始音乐会(不是重复),我需要为每个可能的副本选择,如有必要,编辑音乐会的名称.如果我没有为特定的潜在重复项目做任何事情,它将被视为重复,将被删除.
这是它的样子:
基本上有4个电源组件(这里只有一行但是为了示例):
以下是使用redux,react-redux,immutable,reselect和recompose的完整代码(使用CodePen:带有React和Redux的大型列表):
const initialState = Immutable.fromJS({ /* See codepen, this is a HUGE list */ })
const types = {
CONCERTS_DEDUP_NAME_CHANGED: 'diggger/concertsDeduplication/CONCERTS_DEDUP_NAME_CHANGED',
CONCERTS_DEDUP_CONCERT_TOGGLED: 'diggger/concertsDeduplication/CONCERTS_DEDUP_CONCERT_TOGGLED',
};
const changeName = (pk, name) => ({
type: types.CONCERTS_DEDUP_NAME_CHANGED,
pk,
name
});
const toggleConcert = (pk, toggled) => ({
type: types.CONCERTS_DEDUP_CONCERT_TOGGLED,
pk,
toggled
});
const reducer = (state = initialState, action = {}) => {
switch (action.type) {
case types.CONCERTS_DEDUP_NAME_CHANGED:
return state
.updateIn(['names', String(action.pk)], () => action.name)
.set('_state', 'not_saved');
case types.CONCERTS_DEDUP_CONCERT_TOGGLED:
return state
.updateIn(['concerts', String(action.pk)], () => action.toggled)
.set('_state', 'not_saved');
default:
return state;
}
};
/* configureStore */
const store = Redux.createStore(
reducer,
initialState
);
/* SELECTORS */
const getDuplicatesGroups = (state) => state.get('duplicatesGroups');
const getDuplicateGroup = (state, name) => state.getIn(['duplicatesGroups', name]);
const getConcerts = (state) => state.get('concerts');
const getNames = (state) => state.get('names');
const getConcertName = (state, pk) => getNames(state).get(String(pk));
const isConcertOriginal = (state, pk) => getConcerts(state).get(String(pk));
const getGroupNames = reselect.createSelector(
getDuplicatesGroups,
(duplicates) => duplicates.flip().toList()
);
const makeGetConcertName = () => reselect.createSelector(
getConcertName,
(name) => name
);
const makeIsConcertOriginal = () => reselect.createSelector(
isConcertOriginal,
(original) => original
);
const makeGetDuplicateGroup = () => reselect.createSelector(
getDuplicateGroup,
(duplicates) => duplicates
);
/* COMPONENTS */
const DuplicatessTableRow = Recompose.onlyUpdateForKeys(['name'])(({ name }) => {
return (
<tr>
<td>{name}</td>
<DuplicatesRowColumn name={name}/>
</tr>
)
});
const PureToggle = Recompose.onlyUpdateForKeys(['toggled'])(({ toggled, ...otherProps }) => (
<input type="checkbox" defaultChecked={toggled} {...otherProps}/>
));
/* CONTAINERS */
let DuplicatesTable = ({ groups }) => {
return (
<div>
<table className="pure-table pure-table-bordered">
<thead>
<tr>
<th>{'Concert'}</th>
<th>{'Duplicates'}</th>
</tr>
</thead>
<tbody>
{groups.map(name => (
<DuplicatesTableRow key={name} name={name} />
))}
</tbody>
</table>
</div>
)
};
DuplicatesTable.propTypes = {
groups: React.PropTypes.instanceOf(Immutable.List),
};
DuplicatesTable = ReactRedux.connect(
(state) => ({
groups: getGroupNames(state),
})
)(DuplicatesTable);
let DuplicatesRowColumn = ({ duplicates }) => (
<td>
<ul>
{duplicates.map(d => (
<DuplicateItem
key={d}
pk={d}/>
))}
</ul>
</td>
);
DuplicatessRowColumn.propTypes = {
duplicates: React.PropTypes.arrayOf(
React.PropTypes.string
)
};
const makeMapStateToProps1 = (_, { name }) => {
const getDuplicateGroup = makeGetDuplicateGroup();
return (state) => ({
duplicates: getDuplicateGroup(state, name)
});
};
DuplicatesRowColumn = ReactRedux.connect(makeMapStateToProps1)(DuplicatesRowColumn);
let DuplicateItem = ({ pk, name, toggled, onToggle, onNameChange }) => {
return (
<li>
<table>
<tbody>
<tr>
<td>{ toggled ? <input type="text" value={name} onChange={(e) => onNameChange(pk, e.target.value)}/> : name }</td>
<td>
<PureToggle toggled={toggled} onChange={(e) => onToggle(pk, e.target.checked)}/>
</td>
</tr>
</tbody>
</table>
</li>
)
}
const makeMapStateToProps2 = (_, { pk }) => {
const getConcertName = makeGetConcertName();
const isConcertOriginal = makeIsConcertOriginal();
return (state) => ({
name: getConcertName(state, pk),
toggled: isConcertOriginal(state, pk)
});
};
DuplicateItem = ReactRedux.connect(
makeMapStateToProps2,
(dispatch) => ({
onNameChange(pk, name) {
dispatch(changeName(pk, name));
},
onToggle(pk, toggled) {
dispatch(toggleConcert(pk, toggled));
}
})
)(DuplicateItem);
const App = () => (
<div style={{ maxWidth: '1200px', margin: 'auto' }}>
<DuplicatesTable />
</div>
)
ReactDOM.render(
<ReactRedux.Provider store={store}>
<App/>
</ReactRedux.Provider>,
document.getElementById('app')
);
Run Code Online (Sandbox Code Playgroud)
使用庞大的数据集时,通过这个迷你应用程序学到的经验教训
connect为组件创建ed组件,这些组件是他们所需的最接近的数据,以避免组件只传递他们不使用的道具ownProps就必须避免无用的重新渲染React 在开发版本中检查每个组件的 proptype 以简化开发过程,而在生产中它被省略。
过滤字符串列表对于每个 keyup 都是非常昂贵的操作。由于 JavaScript 的单线程特性,它可能会导致性能问题。解决方案可能是使用debounce方法延迟过滤器函数的执行,直到延迟到期。
另一个问题可能是庞大的列表本身。您可以创建虚拟布局并重用创建的项目,只需替换数据。基本上,您创建具有固定高度的可滚动容器组件,您将在其中放置列表容器。应根据可见列表的长度手动设置列表容器的高度 (itemHeight * numberOfItems),以使滚动条正常工作。然后创建一些项目组件,以便它们填充可滚动容器的高度,并可能添加额外的一两个模拟连续列表效果。使它们成为绝对位置并在滚动时移动它们的位置,以便它模仿连续列表(我想你会找到如何实现它的:)
另一件事是写入 DOM 也是一项昂贵的操作,尤其是如果你做错了。您可以使用画布来显示列表并在滚动时创建流畅的体验。签出反应画布组件。我听说他们已经在 Lists 上做了一些工作。
就像我在评论中提到的那样,我怀疑用户是否需要同时在浏览器中显示所有这 10000 个结果。
如果您翻阅结果,并且始终只显示 10 个结果的列表,该怎么办?
我使用这种技术创建了一个示例,没有使用 Redux 等任何其他库。目前仅支持键盘导航,但也可以轻松扩展以支持滚动。
该示例包含 3 个组件:容器应用程序、搜索组件和列表组件。几乎所有逻辑都已移至容器组件。
要点在于跟踪结果start和selected结果,并通过键盘交互来改变它们。
nextResult: function() {
var selected = this.state.selected + 1
var start = this.state.start
if(selected >= start + this.props.limit) {
++start
}
if(selected + start < this.state.results.length) {
this.setState({selected: selected, start: start})
}
},
prevResult: function() {
var selected = this.state.selected - 1
var start = this.state.start
if(selected < start) {
--start
}
if(selected + start >= 0) {
this.setState({selected: selected, start: start})
}
},
Run Code Online (Sandbox Code Playgroud)
虽然只是将所有文件通过过滤器:
updateResults: function() {
var results = this.props.files.filter(function(file){
return file.file.indexOf(this.state.query) > -1
}, this)
this.setState({
results: results
});
},
Run Code Online (Sandbox Code Playgroud)
start并根据和limit方法对结果进行切片render:
render: function() {
var files = this.state.results.slice(this.state.start, this.state.start + this.props.limit)
return (
<div>
<Search onSearch={this.onSearch} onKeyDown={this.onKeyDown} />
<List files={files} selected={this.state.selected - this.state.start} />
</div>
)
}
Run Code Online (Sandbox Code Playgroud)
小提琴包含完整的工作示例: https: //jsfiddle.net/koenpunt/hm1xnpqk/