如何为 Facebook Messenger 等聊天应用程序实现 React 无限滚动组件?
How to implement a React infinite scroll component for a chat app like Facebook Messenger?
我知道关于这个主题(React infinite scroll)有很多问题,
我的问题旨在更深入地确定实现此类组件的最佳当前可用解决方案。
我正在开发一个聊天应用程序,我已经创建了一个类似于 Facebook 的 Messenger 聊天组件 window,您可以在桌面浏览器上看到它。
脸书:
我的(到目前为止):
用无限加载实现无限滚动被证明是棘手的。
从用户体验的角度来看,我需要始终至少满足以下属性:
- 每行消息的高度应该是即时动态计算的,因为我不知道消息的高度,因为它们没有固定的高度;
- 每当用户键入新消息时,滚动必须自动到达可滚动组件的底部,直到刚刚发送的最后一条消息。可滚动组件本身有一个顶部和底部填充(或者我也可以使用边距)以便在聊天的顶部和第一条以及底部和最后一条消息之间留下一些 space(看上面图片);
- 聊天位于弹出窗口元素内,该元素以淡入动画打开,用户可以在使用页面时关闭和打开它;
现在,为了做到这一点,我已经尝试了几个库:
- react-infinite:第一次尝试,因为需要提前知道所有元素的高度而放弃;
- react-list: I found it really powerful, the thing is that if I close my popover and reopen it after sometimes it loses some already rendered messages and it seems to me that it could be a bug of the
react-list
component. Also, the component does not allow me to display the scrolling bottom upwards (see https://github.com/coderiety/react-list/issues/50);
- react-virtualized:非常强大,但我发现将
List
与 InfiniteLoader
以及 AutoSizer
、CellMeasurer
和 [=15 一起使用会很棘手=].此外,如果我调用 List.scrollToIndex(lastIndex)
自动将容器滚动到底部,当我发送一条消息时,滚动不会完全到达底部,因为可滚动容器具有顶部和底部填充。我无法使用此组件获得令人满意的结果。
- react-infinite-any-height:我想试一试,但目前它似乎还没有移植到 React 16,但如果我安装它,NPM 会警告我 React 15 的对等依赖性不满足, 但我使用 React 16.
所以我的问题更像是一种面对面的方式:你们中有人曾经不得不根据我上面写的 3 个要求实现 React 聊天组件吗?你用的是什么图书馆?
由于 Facebook Messenger 处理得很好并且他们使用 React,你们中有人知道他们是如何实现这样一个组件的吗?如果我检查 Facebook 聊天 window 的聊天消息,它似乎将所有已呈现的消息保留在 DOM 中。但是,如果是这样,这不会影响性能吗?
所以我现在的问题多于答案。我真的很想找到适合我需要的组件。另一种选择是实现我自己的。
2022 年更新
我创建了一个名为 react-really-simple-infinite-scroll
的无限滚动 React 组件,您可以在 GitHub (https://github.com/tonix-tuft/react-really-simple-infinite-scroll) and install it with npm (https://www.npmjs.com/package/react-really-simple-infinite-scroll) 上找到它:
npm install --save react-really-simple-infinite-scroll
npm install --save react react-dom # install React peer deps
用法:
import React, { useState, useCallback, useEffect } from "react";
import { ReallySimpleInfiniteScroll } from "react-really-simple-infinite-scroll";
// You can use any loading component you want. This is just an example using a spinner from "react-spinners-kit".
import { CircleSpinner } from "react-spinners-kit";
/**
* @type {number}
*/
let itemId = 0;
/**
* @type {Function}
*/
const generateMoreItems = numberOfItemsToGenerate => {
const items = [];
for (let i = 0; i < numberOfItemsToGenerate; i++) {
itemId++;
items.push({
id: itemId,
label: `Item ${itemId}`,
});
}
return items;
};
export default function App() {
const [displayInverse, setDisplayInverse] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [isInfiniteLoading, setIsInfiniteLoading] = useState(true);
const [items, setItems] = useState([]);
const onInfiniteLoadCallback = useCallback(() => {
setIsInfiniteLoading(true);
setTimeout(() => {
const moreItems = generateMoreItems(25);
setItems(items => items.concat(moreItems));
setIsInfiniteLoading(false);
}, 1000);
}, []);
useEffect(() => {
onInfiniteLoadCallback();
}, [onInfiniteLoadCallback]);
useEffect(() => {
if (items.length >= 200) {
setHasMore(false);
}
}, [items.length]);
return (
<div className="app">
<ReallySimpleInfiniteScroll
key={displayInverse}
className={`infinite-scroll ${
items.length && displayInverse
? "display-inverse"
: "display-not-inverse"
}`}
hasMore={hasMore}
length={items.length}
loadingComponent={
<div className="loading-component">
<div className="spinner">
<CircleSpinner size={20} />
</div>{" "}
<span className="loading-label">Loading...</span>
</div>
}
isInfiniteLoading={isInfiniteLoading}
onInfiniteLoad={onInfiniteLoadCallback}
displayInverse={displayInverse}
>
{(displayInverse ? items.slice().reverse() : items).map(item => (
<div key={item.id} className="item">
{item.label}
</div>
))}
</ReallySimpleInfiniteScroll>
<div>
<button
onClick={() => setDisplayInverse(displayInverse => !displayInverse)}
>
Toggle displayInverse
</button>
</div>
</div>
);
}
原始答案:
我最终实现了我自己的非常简单的无限滚动组件(不过还没有重构它以使用挂钩):
import React from "react";
import {
isUndefined,
hasVerticalScrollbar,
hasHorizontalScrollbar,
isInt,
debounce
} from "js-utl";
import { classNames } from "react-js-utl/utils";
export default class SimpleInfiniteScroll extends React.Component {
constructor(props) {
super(props);
this.handleScroll = this.handleScroll.bind(this);
this.onScrollStop = debounce(this.onScrollStop.bind(this), 100);
this.itemsIdsRefsMap = {};
this.isLoading = false;
this.isScrolling = false;
this.lastScrollStopPromise = null;
this.lastScrollStopPromiseResolve = null;
this.node = React.createRef();
}
componentDidMount() {
this.scrollToStart();
}
getNode() {
return this.node && this.node.current;
}
getSnapshotBeforeUpdate(prevProps) {
if (prevProps.children.length < this.props.children.length) {
const list = this.node.current;
const axis = this.axis();
const scrollDimProperty = this.scrollDimProperty(axis);
const scrollProperty = this.scrollProperty(axis);
const scrollDelta = list[scrollDimProperty] - list[scrollProperty];
return {
scrollDelta
};
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (
this.isLoading &&
((prevProps.isInfiniteLoading && !this.props.isInfiniteLoading) ||
((this.props.hasMore || prevProps.hasMore) &&
prevProps.children.length !==
this.props.children.length)) &&
snapshot
) {
if (this.props.displayInverse) {
const list = this.node.current;
const axis = this.axis();
const scrollDimProperty = this.scrollDimProperty(axis);
const scrollProperty = this.scrollProperty(axis);
const scrollDelta = snapshot.scrollDelta;
const scrollTo = list[scrollDimProperty] - scrollDelta;
this.scrollTo(scrollProperty, scrollTo);
}
this.isLoading = false;
}
}
loadingComponentRenderer() {
const { loadingComponent } = this.props;
return (
<div
className="simple-infinite-scroll-loading-component"
key={-2}
>
{loadingComponent}
</div>
);
}
axis() {
return this.props.axis === "x" ? "x" : "y";
}
scrollProperty(axis) {
return axis === "y" ? "scrollTop" : "scrollLeft";
}
offsetProperty(axis) {
return axis === "y" ? "offsetHeight" : "offsetWidth";
}
clientDimProperty(axis) {
return axis === "y" ? "clientHeight" : "clientWidth";
}
scrollDimProperty(axis) {
return axis === "y" ? "scrollHeight" : "scrollWidth";
}
hasScrollbarFunction(axis) {
return axis === "y" ? hasVerticalScrollbar : hasHorizontalScrollbar;
}
scrollToStart() {
const axis = this.axis();
this.scrollTo(
this.scrollProperty(axis),
!this.props.displayInverse ? 0 : this.scrollDimProperty(axis)
);
}
scrollToEnd() {
const axis = this.axis();
this.scrollTo(
this.scrollProperty(axis),
!this.props.displayInverse ? this.scrollDimProperty(axis) : 0
);
}
scrollTo(scrollProperty, scrollPositionOrPropertyOfScrollable) {
const scrollableContentNode = this.node.current;
if (scrollableContentNode) {
scrollableContentNode[scrollProperty] = isInt(
scrollPositionOrPropertyOfScrollable
)
? scrollPositionOrPropertyOfScrollable
: scrollableContentNode[scrollPositionOrPropertyOfScrollable];
}
}
scrollToId(id) {
if (this.itemsIdsRefsMap[id] && this.itemsIdsRefsMap[id].current) {
this.itemsIdsRefsMap[id].current.scrollIntoView();
}
}
scrollStopPromise() {
return (
(this.isScrolling && this.lastScrollStopPromise) ||
Promise.resolve()
);
}
onScrollStop(callback) {
callback();
this.isScrolling = false;
this.lastScrollStopPromise = null;
this.lastScrollStopPromiseResolve = null;
}
handleScroll(e) {
const {
isInfiniteLoading,
hasMore,
infiniteLoadBeginEdgeOffset,
displayInverse
} = this.props;
this.isScrolling = true;
this.lastScrollStopPromise =
this.lastScrollStopPromise ||
new Promise(resolve => {
this.lastScrollStopPromiseResolve = resolve;
});
this.onScrollStop(() => {
this.lastScrollStopPromiseResolve &&
this.lastScrollStopPromiseResolve();
});
this.props.onScroll && this.props.onScroll(e);
if (
this.props.onInfiniteLoad &&
(!isUndefined(hasMore) ? hasMore : !isInfiniteLoading) &&
this.node.current &&
!this.isLoading
) {
const axis = this.axis();
const scrollableContentNode = this.node.current;
const scrollProperty = this.scrollProperty(axis);
const offsetProperty = this.offsetProperty(axis);
const scrollDimProperty = this.scrollDimProperty(axis);
const currentScroll = scrollableContentNode[scrollProperty];
const currentDim = scrollableContentNode[offsetProperty];
const scrollDim = scrollableContentNode[scrollDimProperty];
const finalInfiniteLoadBeginEdgeOffset = !isUndefined(
infiniteLoadBeginEdgeOffset
)
? infiniteLoadBeginEdgeOffset
: currentDim / 2;
let thresoldWasReached = false;
if (!displayInverse) {
const clientDimProperty = this.clientDimProperty(axis);
const clientDim = scrollableContentNode[clientDimProperty];
thresoldWasReached =
currentScroll +
clientDim +
finalInfiniteLoadBeginEdgeOffset >=
scrollDim;
} else {
thresoldWasReached =
currentScroll <= finalInfiniteLoadBeginEdgeOffset;
}
if (thresoldWasReached) {
this.isLoading = true;
this.props.onInfiniteLoad();
}
}
}
render() {
const {
children,
displayInverse,
isInfiniteLoading,
className,
hasMore
} = this.props;
return (
<div
className={classNames("simple-infinite-scroll", className)}
ref={this.node}
onScroll={this.handleScroll}
onMouseOver={this.props.onInfiniteScrollMouseOver}
onMouseOut={this.props.onInfiniteScrollMouseOut}
onMouseEnter={this.props.onInfiniteScrollMouseEnter}
onMouseLeave={this.props.onInfiniteScrollMouseLeave}
>
{(hasMore || isInfiniteLoading) &&
displayInverse &&
this.loadingComponentRenderer()}
{children}
{(hasMore || isInfiniteLoading) &&
!displayInverse &&
this.loadingComponentRenderer()}
</div>
);
}
}
并且在 this.props.children
中,我向它传递了以下组件的 class 的 React 元素数组,它扩展了 React.PureComponent
:
...
export default class ChatMessage extends React.PureComponent {
...
}
这样,在重新渲染时,只会重新渲染自上次渲染后发生变化的组件。
我还使用了一个不可变的数据结构来存储聊天消息的集合,特别是 immutable-linked-ordered-map
(https://github.com/tonix-tuft/immutable-linked-ordered-map),它允许我实现 O(1)
的插入时间复杂度、删除和更新消息以及几乎 O(1)
的查找时间复杂度。
本质上,ImmutableLinkedOrderedMap
是一个有序的不可变映射,就像 PHP 中的关联数组一样,但不可变的是:
const map = new ImmutableLinkedOrderedMap({
mode: ImmutableLinkedOrderedMap.MODE.MULTIWAY,
initialItems: [
{
id: 1, // <--- "[keyPropName] === 'id'"
text: "Message text",
// ...
},
{
id: 2,
text: "Another message text",
// ...
},
// ...
]
})
map.get(2) // Will return: { id: 2, text: "Another message text", /* ... */ }
const newMessage = { id: 3, text: "Yet another message text", /* ... */ };
const newMap = map.set(newMessage);
console.log(map !== newMap); // true
console.log(map.length); // 2
console.log(newMap.length); // 3
let messages = newMap.replace(3, newMessage)
console.log(messages === newMap); // true, because newMessage with ID 3 didn't change
messages = newMap.replace(3, { ...newMessage, read: true })
console.log(messages === newMap); // false
然后,当我呈现存储在地图中的消息时,我只需调用它的 .values()
方法,该方法 returns 一个数组,然后映射该数组以呈现消息,例如:
<SimpleInfiniteScroll>
{messages.values().map((message) => <ChatMessage ... />)}
</SimpleInfiniteScroll>
我知道关于这个主题(React infinite scroll)有很多问题, 我的问题旨在更深入地确定实现此类组件的最佳当前可用解决方案。
我正在开发一个聊天应用程序,我已经创建了一个类似于 Facebook 的 Messenger 聊天组件 window,您可以在桌面浏览器上看到它。
脸书:
我的(到目前为止):
用无限加载实现无限滚动被证明是棘手的。 从用户体验的角度来看,我需要始终至少满足以下属性:
- 每行消息的高度应该是即时动态计算的,因为我不知道消息的高度,因为它们没有固定的高度;
- 每当用户键入新消息时,滚动必须自动到达可滚动组件的底部,直到刚刚发送的最后一条消息。可滚动组件本身有一个顶部和底部填充(或者我也可以使用边距)以便在聊天的顶部和第一条以及底部和最后一条消息之间留下一些 space(看上面图片);
- 聊天位于弹出窗口元素内,该元素以淡入动画打开,用户可以在使用页面时关闭和打开它;
现在,为了做到这一点,我已经尝试了几个库:
- react-infinite:第一次尝试,因为需要提前知道所有元素的高度而放弃;
- react-list: I found it really powerful, the thing is that if I close my popover and reopen it after sometimes it loses some already rendered messages and it seems to me that it could be a bug of the
react-list
component. Also, the component does not allow me to display the scrolling bottom upwards (see https://github.com/coderiety/react-list/issues/50); - react-virtualized:非常强大,但我发现将
List
与InfiniteLoader
以及AutoSizer
、CellMeasurer
和 [=15 一起使用会很棘手=].此外,如果我调用List.scrollToIndex(lastIndex)
自动将容器滚动到底部,当我发送一条消息时,滚动不会完全到达底部,因为可滚动容器具有顶部和底部填充。我无法使用此组件获得令人满意的结果。 - react-infinite-any-height:我想试一试,但目前它似乎还没有移植到 React 16,但如果我安装它,NPM 会警告我 React 15 的对等依赖性不满足, 但我使用 React 16.
所以我的问题更像是一种面对面的方式:你们中有人曾经不得不根据我上面写的 3 个要求实现 React 聊天组件吗?你用的是什么图书馆? 由于 Facebook Messenger 处理得很好并且他们使用 React,你们中有人知道他们是如何实现这样一个组件的吗?如果我检查 Facebook 聊天 window 的聊天消息,它似乎将所有已呈现的消息保留在 DOM 中。但是,如果是这样,这不会影响性能吗?
所以我现在的问题多于答案。我真的很想找到适合我需要的组件。另一种选择是实现我自己的。
2022 年更新
我创建了一个名为 react-really-simple-infinite-scroll
的无限滚动 React 组件,您可以在 GitHub (https://github.com/tonix-tuft/react-really-simple-infinite-scroll) and install it with npm (https://www.npmjs.com/package/react-really-simple-infinite-scroll) 上找到它:
npm install --save react-really-simple-infinite-scroll
npm install --save react react-dom # install React peer deps
用法:
import React, { useState, useCallback, useEffect } from "react";
import { ReallySimpleInfiniteScroll } from "react-really-simple-infinite-scroll";
// You can use any loading component you want. This is just an example using a spinner from "react-spinners-kit".
import { CircleSpinner } from "react-spinners-kit";
/**
* @type {number}
*/
let itemId = 0;
/**
* @type {Function}
*/
const generateMoreItems = numberOfItemsToGenerate => {
const items = [];
for (let i = 0; i < numberOfItemsToGenerate; i++) {
itemId++;
items.push({
id: itemId,
label: `Item ${itemId}`,
});
}
return items;
};
export default function App() {
const [displayInverse, setDisplayInverse] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [isInfiniteLoading, setIsInfiniteLoading] = useState(true);
const [items, setItems] = useState([]);
const onInfiniteLoadCallback = useCallback(() => {
setIsInfiniteLoading(true);
setTimeout(() => {
const moreItems = generateMoreItems(25);
setItems(items => items.concat(moreItems));
setIsInfiniteLoading(false);
}, 1000);
}, []);
useEffect(() => {
onInfiniteLoadCallback();
}, [onInfiniteLoadCallback]);
useEffect(() => {
if (items.length >= 200) {
setHasMore(false);
}
}, [items.length]);
return (
<div className="app">
<ReallySimpleInfiniteScroll
key={displayInverse}
className={`infinite-scroll ${
items.length && displayInverse
? "display-inverse"
: "display-not-inverse"
}`}
hasMore={hasMore}
length={items.length}
loadingComponent={
<div className="loading-component">
<div className="spinner">
<CircleSpinner size={20} />
</div>{" "}
<span className="loading-label">Loading...</span>
</div>
}
isInfiniteLoading={isInfiniteLoading}
onInfiniteLoad={onInfiniteLoadCallback}
displayInverse={displayInverse}
>
{(displayInverse ? items.slice().reverse() : items).map(item => (
<div key={item.id} className="item">
{item.label}
</div>
))}
</ReallySimpleInfiniteScroll>
<div>
<button
onClick={() => setDisplayInverse(displayInverse => !displayInverse)}
>
Toggle displayInverse
</button>
</div>
</div>
);
}
原始答案:
我最终实现了我自己的非常简单的无限滚动组件(不过还没有重构它以使用挂钩):
import React from "react";
import {
isUndefined,
hasVerticalScrollbar,
hasHorizontalScrollbar,
isInt,
debounce
} from "js-utl";
import { classNames } from "react-js-utl/utils";
export default class SimpleInfiniteScroll extends React.Component {
constructor(props) {
super(props);
this.handleScroll = this.handleScroll.bind(this);
this.onScrollStop = debounce(this.onScrollStop.bind(this), 100);
this.itemsIdsRefsMap = {};
this.isLoading = false;
this.isScrolling = false;
this.lastScrollStopPromise = null;
this.lastScrollStopPromiseResolve = null;
this.node = React.createRef();
}
componentDidMount() {
this.scrollToStart();
}
getNode() {
return this.node && this.node.current;
}
getSnapshotBeforeUpdate(prevProps) {
if (prevProps.children.length < this.props.children.length) {
const list = this.node.current;
const axis = this.axis();
const scrollDimProperty = this.scrollDimProperty(axis);
const scrollProperty = this.scrollProperty(axis);
const scrollDelta = list[scrollDimProperty] - list[scrollProperty];
return {
scrollDelta
};
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (
this.isLoading &&
((prevProps.isInfiniteLoading && !this.props.isInfiniteLoading) ||
((this.props.hasMore || prevProps.hasMore) &&
prevProps.children.length !==
this.props.children.length)) &&
snapshot
) {
if (this.props.displayInverse) {
const list = this.node.current;
const axis = this.axis();
const scrollDimProperty = this.scrollDimProperty(axis);
const scrollProperty = this.scrollProperty(axis);
const scrollDelta = snapshot.scrollDelta;
const scrollTo = list[scrollDimProperty] - scrollDelta;
this.scrollTo(scrollProperty, scrollTo);
}
this.isLoading = false;
}
}
loadingComponentRenderer() {
const { loadingComponent } = this.props;
return (
<div
className="simple-infinite-scroll-loading-component"
key={-2}
>
{loadingComponent}
</div>
);
}
axis() {
return this.props.axis === "x" ? "x" : "y";
}
scrollProperty(axis) {
return axis === "y" ? "scrollTop" : "scrollLeft";
}
offsetProperty(axis) {
return axis === "y" ? "offsetHeight" : "offsetWidth";
}
clientDimProperty(axis) {
return axis === "y" ? "clientHeight" : "clientWidth";
}
scrollDimProperty(axis) {
return axis === "y" ? "scrollHeight" : "scrollWidth";
}
hasScrollbarFunction(axis) {
return axis === "y" ? hasVerticalScrollbar : hasHorizontalScrollbar;
}
scrollToStart() {
const axis = this.axis();
this.scrollTo(
this.scrollProperty(axis),
!this.props.displayInverse ? 0 : this.scrollDimProperty(axis)
);
}
scrollToEnd() {
const axis = this.axis();
this.scrollTo(
this.scrollProperty(axis),
!this.props.displayInverse ? this.scrollDimProperty(axis) : 0
);
}
scrollTo(scrollProperty, scrollPositionOrPropertyOfScrollable) {
const scrollableContentNode = this.node.current;
if (scrollableContentNode) {
scrollableContentNode[scrollProperty] = isInt(
scrollPositionOrPropertyOfScrollable
)
? scrollPositionOrPropertyOfScrollable
: scrollableContentNode[scrollPositionOrPropertyOfScrollable];
}
}
scrollToId(id) {
if (this.itemsIdsRefsMap[id] && this.itemsIdsRefsMap[id].current) {
this.itemsIdsRefsMap[id].current.scrollIntoView();
}
}
scrollStopPromise() {
return (
(this.isScrolling && this.lastScrollStopPromise) ||
Promise.resolve()
);
}
onScrollStop(callback) {
callback();
this.isScrolling = false;
this.lastScrollStopPromise = null;
this.lastScrollStopPromiseResolve = null;
}
handleScroll(e) {
const {
isInfiniteLoading,
hasMore,
infiniteLoadBeginEdgeOffset,
displayInverse
} = this.props;
this.isScrolling = true;
this.lastScrollStopPromise =
this.lastScrollStopPromise ||
new Promise(resolve => {
this.lastScrollStopPromiseResolve = resolve;
});
this.onScrollStop(() => {
this.lastScrollStopPromiseResolve &&
this.lastScrollStopPromiseResolve();
});
this.props.onScroll && this.props.onScroll(e);
if (
this.props.onInfiniteLoad &&
(!isUndefined(hasMore) ? hasMore : !isInfiniteLoading) &&
this.node.current &&
!this.isLoading
) {
const axis = this.axis();
const scrollableContentNode = this.node.current;
const scrollProperty = this.scrollProperty(axis);
const offsetProperty = this.offsetProperty(axis);
const scrollDimProperty = this.scrollDimProperty(axis);
const currentScroll = scrollableContentNode[scrollProperty];
const currentDim = scrollableContentNode[offsetProperty];
const scrollDim = scrollableContentNode[scrollDimProperty];
const finalInfiniteLoadBeginEdgeOffset = !isUndefined(
infiniteLoadBeginEdgeOffset
)
? infiniteLoadBeginEdgeOffset
: currentDim / 2;
let thresoldWasReached = false;
if (!displayInverse) {
const clientDimProperty = this.clientDimProperty(axis);
const clientDim = scrollableContentNode[clientDimProperty];
thresoldWasReached =
currentScroll +
clientDim +
finalInfiniteLoadBeginEdgeOffset >=
scrollDim;
} else {
thresoldWasReached =
currentScroll <= finalInfiniteLoadBeginEdgeOffset;
}
if (thresoldWasReached) {
this.isLoading = true;
this.props.onInfiniteLoad();
}
}
}
render() {
const {
children,
displayInverse,
isInfiniteLoading,
className,
hasMore
} = this.props;
return (
<div
className={classNames("simple-infinite-scroll", className)}
ref={this.node}
onScroll={this.handleScroll}
onMouseOver={this.props.onInfiniteScrollMouseOver}
onMouseOut={this.props.onInfiniteScrollMouseOut}
onMouseEnter={this.props.onInfiniteScrollMouseEnter}
onMouseLeave={this.props.onInfiniteScrollMouseLeave}
>
{(hasMore || isInfiniteLoading) &&
displayInverse &&
this.loadingComponentRenderer()}
{children}
{(hasMore || isInfiniteLoading) &&
!displayInverse &&
this.loadingComponentRenderer()}
</div>
);
}
}
并且在 this.props.children
中,我向它传递了以下组件的 class 的 React 元素数组,它扩展了 React.PureComponent
:
...
export default class ChatMessage extends React.PureComponent {
...
}
这样,在重新渲染时,只会重新渲染自上次渲染后发生变化的组件。
我还使用了一个不可变的数据结构来存储聊天消息的集合,特别是 immutable-linked-ordered-map
(https://github.com/tonix-tuft/immutable-linked-ordered-map),它允许我实现 O(1)
的插入时间复杂度、删除和更新消息以及几乎 O(1)
的查找时间复杂度。
本质上,ImmutableLinkedOrderedMap
是一个有序的不可变映射,就像 PHP 中的关联数组一样,但不可变的是:
const map = new ImmutableLinkedOrderedMap({
mode: ImmutableLinkedOrderedMap.MODE.MULTIWAY,
initialItems: [
{
id: 1, // <--- "[keyPropName] === 'id'"
text: "Message text",
// ...
},
{
id: 2,
text: "Another message text",
// ...
},
// ...
]
})
map.get(2) // Will return: { id: 2, text: "Another message text", /* ... */ }
const newMessage = { id: 3, text: "Yet another message text", /* ... */ };
const newMap = map.set(newMessage);
console.log(map !== newMap); // true
console.log(map.length); // 2
console.log(newMap.length); // 3
let messages = newMap.replace(3, newMessage)
console.log(messages === newMap); // true, because newMessage with ID 3 didn't change
messages = newMap.replace(3, { ...newMessage, read: true })
console.log(messages === newMap); // false
然后,当我呈现存储在地图中的消息时,我只需调用它的 .values()
方法,该方法 returns 一个数组,然后映射该数组以呈现消息,例如:
<SimpleInfiniteScroll>
{messages.values().map((message) => <ChatMessage ... />)}
</SimpleInfiniteScroll>