React

时间:2016-06-25 22:18:01

标签: javascript list reactjs

我正在使用React实现可过滤列表。列表的结构如下图所示。

enter image description here

PREMISE

以下是对它如何运作的描述:

  • 状态位于最高级别组件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的开发版本,并且一切正常。

当我不得不处理一个大到10000个条目的files数组时出现问题。在输入中键入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));

正如您所知,每次currentlySelectedIndex更改时,都会导致重新渲染,并且每次都会重新创建列表。我认为,因为我在每个key元素上设置了li值,所以React会避免重新渲染没有li更改的所有其他className元素,但显然事实并非如此。

我最终为Result元素定义了一个类,它明确地检查每个Result元素是否应根据之前是否选择并基于当前用户输入重新呈现: / p>

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));
}

这使得性能稍微更好,但它仍然不够好。事情是,当我在React的生产版本上测试时,工作黄油顺利,没有任何延迟。

BOTTOMLINE

React的开发版和生产版之间有这么明显的差异吗?

当我考虑React如何管理列表时,我是否理解/做错了什么?

更新2016年11月14日

我找到了迈克尔杰克逊的这个演讲,在那里他解决了一个与这个非常类似的问题:https://youtu.be/7S8v8jfLb1Q?t=26m2s

该解决方案非常类似于AskarovBeknar answer提出的解决方案,

更新14-4-2018

由于这显然是一个受欢迎的问题,自从问到原始问题以来事情已经取得进展,我鼓励你观看上面链接的视频,为了掌握虚拟布局,我也鼓励你使用如果您不想重新发明轮子,请React Virtualized库。

9 个答案:

答案 0 :(得分:11)

与此问题的许多其他答案一样,主要问题在于,在进行过滤和处理关键事件时,在DOM中渲染如此多的元素将会很慢。

对于导致问题的React,你没有做任何本质上错误的事情,但是像许多与性能相关的问题,UI也可能占据很大一部分责任。

如果您的用户界面设计时没有考虑到效率,那么即使像React这样的高性能工具也会受到影响。

如@Koen

所述,过滤结果集是一个很好的开始

我已经尝试了一下这个想法并创建了一个示例应用程序,说明我将如何开始解决这类问题。

这绝不是production ready代码,但它确实充分说明了这个概念,可以修改为更健壮,随意查看代码 - 我希望它至少能给你一些想法...;)

https://github.com/deowk/react-large-list-example

enter image description here

答案 1 :(得分:10)

我遇到一个非常类似的问题的经验是,如果DOM中有超过100-200个左右的组件,那么反应真的会受到影响。即使你非常小心(通过设置所有键和/或实现shouldComponentUpdate方法)只改变重新渲染中的一个或两个组件,你仍然会在一个伤害。

此刻反应的缓慢部分是它比较虚拟DOM和真实DOM之间的差异。如果你有数以千计的组件,但只更新了一对,那没关系,反应仍然在DOM之间做了大量的差异操作。

当我现在编写页面时,我尝试将它们设计为最小化组件数量,在渲染大型组件列表时,这样做的一种方法是......好吧......不会渲染大型组件列表。

我的意思是:只渲染当前可以看到的组件,向下滚动渲染更多,用户不可能以任何方式向下滚动数千个组件....我希望。

这样做的好方法是:

https://www.npmjs.com/package/react-infinite-scroll

这里有一个很棒的方法:

http://www.reactexamples.com/react-infinite-scroll/

我担心它不会删除页面顶部的组件,所以如果滚动的时间足够长,那么性能问题就会重新出现。

我知道提供链接作为答案并不是一个好习惯,但他们提供的示例将解释如何比我在这里更好地使用这个库。希望我已经解释了为什么大名单很糟糕,但也解决了。

答案 2 :(得分:8)

首先,React的开发版和生产版之间的差异很大,因为在生产中有许多绕过的健全性检查(例如道具类型验证)。

然后,我认为您应该重新考虑使用Redux,因为它对您所需要的(或任何类型的通量实现)非常有用。您应该明确地看一下这个演示文稿:Big List High Performance React & Redux

但是在深入研究redux之前,你需要通过将组件拆分成更小的组件来对你的React代码做一些讨论,因为 shouldComponentUpdate将完全绕过孩子的渲染,所以它是一个巨大的增益

如果有更多粒度组件,可以使用redux和react-redux处理状态,以更好地组织数据流。

当我需要渲染一千行并且能够通过编辑其内容来修改每一行时,我最近遇到了类似的问题。这个迷你应用程序显示一个可能重复音乐会的音乐会列表,如果我想通过选中复选框将潜在副本标记为原始音乐会(不是重复),我需要为每个可能的副本选择,并且,如有必要,编辑音乐会的名称。如果我没有为特定的潜在重复项目做任何事情,它将被视为重复项目并将被删除。

这是它的样子:

enter image description here

基本上有4个主电源组件(此处只有一行,但为了示例,它是

enter image description here

以下是使用Huge List with React & Reduxreduxreact-reduximmutablereselect的完整代码(使用CodePen:recompose):< / p>

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')
);

使用大型数据集时通过执行此迷你应用程序所获得的经验

  • React组件在保持较小时效果最佳
  • 重新选择对于避免重新计算非常有用,并且在给定相同参数的情况下保持相同的引用对象(使用immutable.js时)。
  • 为组件创建connect ed组件,这些组件是他们所需的最接近的数据,以避免组件只传递他们不使用的道具
  • 当您只需要ownProps中给出的初始道具时,使用fabric函数创建mapDispatchToProps是必要的,以避免无用的重新渲染
  • React&amp; redux肯定会一起摇滚!

答案 3 :(得分:3)

  1. 在开发版本中进行反应检查每个组件的proptypes以简化开发过程,而在生产中则省略。

  2. 对每个keyup过滤字符串列表都是非常昂贵的操作。由于JavaScript的单线程特性,它可能会导致性能问题。 解决方案可能是使用 debounce 方法来延迟过滤功能的执行,直到延迟过期。

  3. 另一个问题可能是巨大的列表本身。您可以创建虚拟布局并重新使用创建的项目来替换数据。基本上,您创建具有固定高度的可滚动容器组件,在其中您将放置列表容器。列表容器的高度应该手动设置(itemHeight * numberOfItems),具体取决于可见列表的长度,以使滚动条工作。然后创建一些项目组件,以便它们将填充可滚动容器的高度,并可能添加额外的一个或两个模拟连续列表效果。使它们成为绝对位置并在滚动时移动它们的位置以便它将模仿连续列表(我想你会发现如何实现它):

  4. 另外一件事是写入DOM也是一项昂贵的操作,尤其是如果你做错了。您可以使用画布显示列表并在滚动时创建流畅的体验。 Checkout react-canvas组件。我听说他们已经在列表上做了一些工作。

答案 4 :(得分:3)

就像我在my comment中提到的,我怀疑用户是否同时需要浏览器中的所有10000个结果。

如果您翻阅结果,并且只显示10个结果列表,该怎么办?

created an example使用这种技术,而不使用像Redux这样的任何其他库。 目前只有键盘导航,但也可以很容易地扩展到滚动工作。

该示例存在3个组件,容器应用程序,搜索组件和列表组件。 几乎所有逻辑都已移至容器组件。

要点在于跟踪startselected结果,并将其转移到键盘交互上。

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
  });
},

根据start方法中的limitrender对结果进行切片:

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>
  )
}

包含完整工作示例的小提琴:https://jsfiddle.net/koenpunt/69z2wepo/47841/

答案 5 :(得分:3)

查看React Virtualized Select,它旨在解决这个问题,并在我的经验中表现出色。从描述:

  

使用react-virtualized和react-select的HOC在下拉列表中显示大型选项列表

https://github.com/bvaughn/react-virtualized-select

答案 6 :(得分:2)

在加载到React组件之前尝试过滤,并且只在组件中显示合理数量的项目并按需加载更多项目。没有人可以同时查看这么多项目。

I don't think you are, but don't use indexes as keys

要找出开发和生产版本不同的真正原因,您可以尝试使用profiling代码。

加载页面,开始录制,执行更改,停止录制,然后查看时间。请参阅here for instructions for performance profiling in Chrome

答案 7 :(得分:1)

对于那些为解决此问题而苦恼的人,我编写了一个组件react-big-list,该组件可以处理多达一百万条记录的列表。

最重要的是,它还提供了一些新颖的功能,例如:

  • 排序
  • 缓存
  • 自定义过滤
  • ...

我们在很多应用中都在生产中使用它,并且效果很好。

答案 8 :(得分:1)

React推荐了react-window库:https://www.npmjs.com/package/react-window

它比react-vitualized好。你可以试试