React / Redux 中树结构和 shouldComponentUpdate 的性能问题
Performance issues with a tree structure and shouldComponentUpdate in React / Redux
我对 React、Redux 和 ImmutableJS 还很陌生,运行 遇到了一些性能问题。
我有一个大的数据树结构,我目前将其作为平面列表存储在这个结构中:
new Map({
1: new Node({
id: 1,
text: 'Root',
children: [2,3]
}),
2: new Node({
id: 2,
text: 'Child 1',
children: [4]
}),
3: new Node({
id: 3,
text: 'Child 2',
children: []
}),
4: new Node({
id: 4,
text: 'Child of child 1',
children: []
})
});
虽然将其构建为平面列表可以轻松更新节点,但我发现随着树的增长,交互变得缓慢。交互包括能够 select 一个或多个节点、切换其 child 节点的可见性、更新文本等。看起来缓慢 UI 的一个关键原因是整个树正在为每次交互重新绘制。
我想使用 shouldComponentUpdate
这样如果我更新节点 3,节点 2 和 4 不会更新。如果数据存储为树,这将很容易(我可以简单地检查是否 this.props !== nextProps
),但由于数据存储在平面列表中,检查会复杂得多。
我应该如何存储数据并使用 shouldComponentUpdate
(或其他方法)来支持具有数百或数千个树节点的平滑 UI?
编辑
我一直在顶层连接商店,然后必须将整个商店向下传递给子组件。
我的结构是:
<NodeTree>
<NodeBranch>
<Node>{{text}}</Node>
<NodeTree>...</NodeTree>
</NodeBranch>
<NodeBranch>
<Node>{{text}}</Node>
<NodeTree>...</NodeTree>
</NodeBranch>
...
</NodeTree>
<Node>
可以用 shouldComponentUpdate
做一个简单的检查,看看标题是否改变了,但我没有类似的解决方案可以用在 <NodeTree>
或 <NodeBranch>
鉴于树的递归性质。
看起来最好的解决方案(感谢@Dan Abramov)是连接每个 <NodeBranch>
,而不是仅在顶层连接。我今晚会测试这个。
我 just added 一个新例子展示了这一点。
你可以 运行 像这样:
git clone https://github.com/rackt/redux.git
cd redux/examples/tree-view
npm install
npm start
open http://localhost:3000/
Dan Abramov 解决方案在 99% 的常见情况下都很好。
但是每个增量所需的计算时间与树中的节点数成正比O(n),因为每个连接的节点 HOC 必须执行一点点代码(如身份比较...)。但是这段代码执行起来非常快。
如果您测试 Dan Abramov 解决方案,我的笔记本电脑上有 10k 个节点,我在尝试增加计数器时开始看到一些延迟(例如 100 毫秒延迟)。在便宜的移动设备上可能更糟。
有人可能会争辩说您不应该尝试一次在 DOM 中渲染 10k 个项目,我完全同意这一点,但如果您真的想显示很多项目并将它们连接起来,那就是没那么简单。
如果你想把这个 O(n) 变成 O(1),那么你可以实现你自己的 connect
HOC(高阶组件)订阅仅在底层计数器更新时触发的系统,而不是所有 HOC 的订阅。
我已经在 answer 中介绍了如何执行此操作。
我在 react-redux 上创建了一个 issue,以便 connect
变得更加灵活,并允许通过选项轻松自定义商店的订阅。这个想法是您应该能够重用 connect
代码并有效地订阅状态切片更改而不是全局状态更改(即 store.subscribe()
)。
const mapStateToProps = (state,props) => {node: selectNodeById(state,props.nodeId)}
const connectOptions = {
doSubscribe: (store,props) => store.subscribeNode(props.nodeId)
}
connect(mapStateToProps, undefined,connectOptions)(ComponentToConnect)
使用 subscribeNode
方法创建商店增强器仍然是您自己的责任。
我对 React、Redux 和 ImmutableJS 还很陌生,运行 遇到了一些性能问题。
我有一个大的数据树结构,我目前将其作为平面列表存储在这个结构中:
new Map({
1: new Node({
id: 1,
text: 'Root',
children: [2,3]
}),
2: new Node({
id: 2,
text: 'Child 1',
children: [4]
}),
3: new Node({
id: 3,
text: 'Child 2',
children: []
}),
4: new Node({
id: 4,
text: 'Child of child 1',
children: []
})
});
虽然将其构建为平面列表可以轻松更新节点,但我发现随着树的增长,交互变得缓慢。交互包括能够 select 一个或多个节点、切换其 child 节点的可见性、更新文本等。看起来缓慢 UI 的一个关键原因是整个树正在为每次交互重新绘制。
我想使用 shouldComponentUpdate
这样如果我更新节点 3,节点 2 和 4 不会更新。如果数据存储为树,这将很容易(我可以简单地检查是否 this.props !== nextProps
),但由于数据存储在平面列表中,检查会复杂得多。
我应该如何存储数据并使用 shouldComponentUpdate
(或其他方法)来支持具有数百或数千个树节点的平滑 UI?
编辑
我一直在顶层连接商店,然后必须将整个商店向下传递给子组件。
我的结构是:
<NodeTree>
<NodeBranch>
<Node>{{text}}</Node>
<NodeTree>...</NodeTree>
</NodeBranch>
<NodeBranch>
<Node>{{text}}</Node>
<NodeTree>...</NodeTree>
</NodeBranch>
...
</NodeTree>
<Node>
可以用 shouldComponentUpdate
做一个简单的检查,看看标题是否改变了,但我没有类似的解决方案可以用在 <NodeTree>
或 <NodeBranch>
鉴于树的递归性质。
看起来最好的解决方案(感谢@Dan Abramov)是连接每个 <NodeBranch>
,而不是仅在顶层连接。我今晚会测试这个。
我 just added 一个新例子展示了这一点。
你可以 运行 像这样:
git clone https://github.com/rackt/redux.git
cd redux/examples/tree-view
npm install
npm start
open http://localhost:3000/
Dan Abramov 解决方案在 99% 的常见情况下都很好。
但是每个增量所需的计算时间与树中的节点数成正比O(n),因为每个连接的节点 HOC 必须执行一点点代码(如身份比较...)。但是这段代码执行起来非常快。
如果您测试 Dan Abramov 解决方案,我的笔记本电脑上有 10k 个节点,我在尝试增加计数器时开始看到一些延迟(例如 100 毫秒延迟)。在便宜的移动设备上可能更糟。
有人可能会争辩说您不应该尝试一次在 DOM 中渲染 10k 个项目,我完全同意这一点,但如果您真的想显示很多项目并将它们连接起来,那就是没那么简单。
如果你想把这个 O(n) 变成 O(1),那么你可以实现你自己的 connect
HOC(高阶组件)订阅仅在底层计数器更新时触发的系统,而不是所有 HOC 的订阅。
我已经在 answer 中介绍了如何执行此操作。
我在 react-redux 上创建了一个 issue,以便 connect
变得更加灵活,并允许通过选项轻松自定义商店的订阅。这个想法是您应该能够重用 connect
代码并有效地订阅状态切片更改而不是全局状态更改(即 store.subscribe()
)。
const mapStateToProps = (state,props) => {node: selectNodeById(state,props.nodeId)}
const connectOptions = {
doSubscribe: (store,props) => store.subscribeNode(props.nodeId)
}
connect(mapStateToProps, undefined,connectOptions)(ComponentToConnect)
使用 subscribeNode
方法创建商店增强器仍然是您自己的责任。