Javascript 大名单性能与反应
我正在使用React实现一个可过滤列表。列表的结构如下图所示 前提 以下是对其工作原理的描述:Javascript 大名单性能与反应,javascript,list,reactjs,Javascript,List,Reactjs,我正在使用React实现一个可过滤列表。列表的结构如下图所示 前提 以下是对其工作原理的描述: 状态位于最高级别的组件中,搜索组件 该州描述如下: 可以看出,每次更改currentlySelectedIndex时,都会导致重新渲染,并且每次都会重新创建列表。我认为,由于我在每个li元素上设置了键值,因此React可以避免重新呈现每个没有更改className的li元素,但显然不是这样 最后,我为结果元素定义了一个类,在该类中,它显式地检查每个结果元素是否应根据之前是否选中以及当前用户输入重
- 状态位于最高级别的组件中,
组件搜索
- 该州描述如下:
currentlySelectedIndex
时,都会导致重新渲染,并且每次都会重新创建列表。我认为,由于我在每个li
元素上设置了键
值,因此React可以避免重新呈现每个没有更改className
的li
元素,但显然不是这样
最后,我为结果
元素定义了一个类,在该类中,它显式地检查每个结果
元素是否应根据之前是否选中以及当前用户输入重新呈现:
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>
);
}
});
var ResultItem=React.createClass({
shouldComponentUpdate:函数(下一步){
if(nextrops.match!==this.props.match){
返回true;
}否则{
return(nextrops.selected!==this.props.selected);
}
},
render:function(){
返回(
{this.props.children}
);
}
});
现在创建的列表如下所示:
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));
}
results=this.state.filtered.map(函数(文件,索引){
var start,end,matchIndex,match=this.state.query,选中;
matchIndex=file.indexOf(匹配);
start=file.slice(0,匹配索引);
end=file.slice(matchIndex+match.length);
已选择=(索引==此.state.currentlySelected)?真:假
返回(
{start}
{match}
{end}
);
}.约束(这个);
}
这使得性能稍有提高,但仍然不够好。问题是,当我在React的生产版本上进行测试时,一切都非常顺利,完全没有延迟
底线
React的开发版本和生产版本之间是否存在如此明显的差异正常?
当我思考React如何管理列表时,我是否理解/做错事了?
更新日期14-11-2016
我发现这是迈克尔·杰克逊的演讲,他在演讲中处理了一个非常类似的问题:
该解决方案与下面AskarovBeknar提出的方案非常相似
更新日期14-4-2018
由于这显然是一个受欢迎的问题,而且自最初的问题提出以来,事情已经取得了进展,因此我鼓励您观看上面链接的视频,为了掌握虚拟布局,如果您不想重新发明轮子,我也鼓励您使用库 首先,React的开发版本和生产版本之间的差异是巨大的,因为在生产中有许多绕过的健全性检查(如道具类型验证) 然后,我认为您应该重新考虑使用Redux,因为它对于您所需要的(或任何类型的通量实现)非常有用。您应该明确地看一下此演示文稿: 但是在进入redux之前,您需要通过将组件拆分为更小的组件来对React代码进行一些调整,因为
shouldComponentUpdate
将完全绕过子组件的渲染,因此这是一个巨大的收益
当您有更细粒度的组件时,您可以使用redux处理状态,并对redux作出反应以更好地组织数据流
我最近遇到了一个类似的问题,当时我需要渲染1000行,并能够通过编辑每一行的内容来修改每一行。这个迷你应用程序显示了一个可能重复的音乐会列表,如果我想通过选中复选框将可能重复的音乐会标记为原始音乐会(而不是重复的),我需要为每个可能重复的音乐会进行选择,并在必要时编辑音乐会的名称。如果我对某个特定的潜在重复项不做任何操作,则该项将被视为重复项并将被删除
下面是它的外观:
基本上有4个主电源组件(这里只有一行,但为了示例起见):
以下是使用、和的完整代码(工作代码笔:):
const initialState=Immutable.fromJS({/*请参见codepen,这是一个巨大的列表*/})
常量类型={
音乐会名称更改:“Digger/Concertsdeplication/CONCERTS名称更改”,
音乐会\u重复数据消除\u音乐会\u切换:“Digger/Concertsdeplication/CONCERTS\u重复数据消除\u音乐会\u切换”,
};
const changeName=(主键,名称)=>({
类型:types.CONCERTS\u重复数据消除\u名称\u已更改,
主键,
名称
});
const-toggleConcert=(主键,切换)=>({
type:types.CONCERTS\u重复数据消除\u CONCERT\u已切换,
主键,
拴牢
});
const reducer=(state=initialState,action={})=>{
开关(动作类型){
案例类型.CONCERTS\u重复数据消除\u名称\u更改:
返回状态
.updateIn(['names',String(action.pk)],()=>action.name)
.set(“U状态”、“未保存”);
案例类型.CONCERTS\u重复数据消除\u CONCERT\u切换:
返回状态
.updateIn(['concerts',String(action.pk)],()=>action.toggled)
.set(“U状态”、“未保存”);
违约:
返回状态;
}
};
/*配置存储*/
const store=Redux.createStore(
减速器,
初始状态
);
/*选择器*/
常量getDuplicatesGroups=(状态)=>state
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>
);
}
});
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));
}
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')
);
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})
}
},
updateResults: function() {
var results = this.props.files.filter(function(file){
return file.file.indexOf(this.state.query) > -1
}, this)
this.setState({
results: results
});
},
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>
)
}