gpt4 book ai didi

javascript - React无限滚动组件性能

转载 作者:行者123 更新时间:2023-12-02 23:20:05 25 4
gpt4 key购买 nike

我在 React 中编写了以下无限滚动组件:

import React from 'react'
import { uniqueId, isUndefined, hasVerticalScrollbar, hasHorizontalScrollbar, isInt, throttle } from '../../../js/utils';

export default class BlSimpleInfiniteScroll extends React.Component {

constructor(props) {
super(props)

this.handleScroll = this.handleScroll.bind(this)

this.itemsIdsRefsMap = {}

this.isLoading = false

this.node = React.createRef()
}

componentDidMount() {
const {
initialId
} = this.props
let id
if (initialId) {
if (typeof initialId === "function") {
id = initialId()
}
else {
id = initialId
}
this.scrollToId(id)
}
}

componentDidUpdate(prevProps) {
if (
this.isLoading
&&
prevProps.isInfiniteLoading
&&
!this.props.isInfiniteLoading
) {
const axis = this.axis()
const scrollProperty = this.scrollProperty(axis)
const offsetProperty = this.offsetProperty(axis)
this.scrollTo(scrollProperty, this.node.current[offsetProperty])
this.isLoading = false
}
}

itemsRenderer(items) {
const length = items.length

let i = 0
const renderedItems = []
for (const item of items) {
renderedItems[i] = this.itemRenderer(item.id, i, length)
i++
}
return renderedItems
}

itemRenderer(id, i, length) {
const {
itemRenderer,
isInfiniteLoading,
displayInverse
} = this.props

let renderedItem = itemRenderer(id, i)
if (isInfiniteLoading) {
if (!displayInverse && (i == length - 1)) {
renderedItem = this.standardLoadingComponentWrapperRenderer(id, renderedItem)
}
else if (i == 0) {
renderedItem = this.inverseLoadingComponentWrapperRenderer(id, renderedItem)
}
}
const ref = this.itemsIdsRefsMap[id] || (this.itemsIdsRefsMap[id] = React.createRef())
return (
<div className="bl-simple-infinite-scroll-item"
key={id}
ref={ref}>
{renderedItem}
</div>
)
}

loadingComponentRenderer() {
const {
loadingComponent
} = this.props

return (
<div className="bl-simple-infinite-scroll-loading-component"
key={uniqueId()}>
{loadingComponent}
</div>
)
}

loadingComponentWrapperRenderer(id, children) {
return (
<div className="bl-simple-infinite-scroll-loading-component-wrapper"
key={id}>
{children}
</div>
)
}

standardLoadingComponentWrapperRenderer(id, renderedItem) {
return this.loadingComponentWrapperRenderer(id, [
renderedItem,
this.loadingComponentRenderer()
])
}

inverseLoadingComponentWrapperRenderer(id, renderedItem) {
return this.loadingComponentWrapperRenderer(id, [
this.loadingComponentRenderer(),
renderedItem
])
}

axis() {
return this.props.axis === 'x' ? 'x' : 'y'
}

scrollProperty(axis) {
return axis == 'y' ? 'scrollTop' : 'scrollLeft'
}

offsetProperty(axis) {
return axis == 'y' ? 'offsetHeight' : 'offsetWidth'
}

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

handleScroll() {
const {
isInfiniteLoading,
infiniteLoadBeginEdgeOffset,
displayInverse
} = this.props
if (
this.props.onInfiniteLoad
&&
!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
let memorizeLastElementBeforeInfiniteLoad = () => { }
if (!displayInverse) {
thresoldWasReached = currentScroll >= (scrollDim - finalInfiniteLoadBeginEdgeOffset)
}
else {
memorizeLastElementBeforeInfiniteLoad = () => {
// TODO
}
thresoldWasReached = currentScroll <= finalInfiniteLoadBeginEdgeOffset
}
if (thresoldWasReached) {
this.isLoading = true
memorizeLastElementBeforeInfiniteLoad()
this.props.onInfiniteLoad()
}
}
}

render() {
const {
items
} = this.props

return (
<div className="bl-simple-infinite-scroll"
ref={this.node}
onScroll={this.handleScroll}
onMouseOver={this.props.onInfiniteScrollMouseOver}
onMouseOut={this.props.onInfiniteScrollMouseOut}
onMouseEnter={this.props.onInfiniteScrollMouseEnter}
onMouseLeave={this.props.onInfiniteScrollMouseLeave}>
{this.itemsRenderer(items)}
</div>
)
}

}

我在我正在编写的聊天应用程序中使用它:

...
<BlSimpleInfiniteScroll items={chat.messages}
ref={this.infiniteScrollComponentRef}
initialId={() => lastOfArray(chat.messages).id}
itemRenderer={(id, i) => this.messageRenderer(id, i, chat.messages)}
loadingComponent={<BlLoadingSpinnerContainer />}
isInfiniteLoading={isChatLoading}
displayInverse
infiniteLoadBeginEdgeOffset={void 0}
infiniteLoadingBeginBottomOffset={void 0}
onInfiniteLoad={() => this.props.onLoadPreviousChatMessages(chat.id)}
onInfiniteScrollMouseEnter={this.handleInfiniteScrollMouseEnter}
onInfiniteScrollMouseLeave={this.handleInfiniteScrollMouseLeave} />
...

问题是,一旦我滚动到阈值并调用 onInfiniteLoad ,在显示加载微调器之前以及加载数据之后,滚动就会卡住并且组件变得无响应。我该如何解决这个问题?当我渲染微调器容器时,在之前加载的消息之后,React 不应该只是附加保留之前添加的项目的新 div 来保持组件的性能吗?如果没有,我缺少哪些 React 关键概念?

感谢您的关注!

更新:以下是附加组件:

BlOrderChat 表示聊天窗口并呈现 BlSimpleInfiniteScroll:

import React from 'react'
import BlOrderChatMessage from './BlOrderChatMessage';
import { isEmpty, uniqueId } from '../../../js/utils';
import { chatSelector } from '../selectors';
import BlLoadingSpinnerContainer from '../../core/animation/loading/BlLoadingSpinnerContainer';
import BlSimpleInfiniteScroll from '../../core/scroll/BlSimpleInfiniteScroll';

export default class BlOrderChat extends React.Component {

static BL_ORDER_CHAT_MESSAGE_ID_ATTR_PREFIX = 'blOrderChatMessage'

constructor(props) {
super(props)

this.messageRenderer = this.messageRenderer.bind(this)

this.infiniteScrollComponentRef = React.createRef()
}

scrollToBottom() {
this.infiniteScrollComponentRef.current && this.infiniteScrollComponentRef.current.scrollToStart()
}

messageRenderer(messageId, index, messages) {
const {
currentUser, chat
} = this.props
const message = messages[index]
const length = messages.length
const fromUser = chat.users.items[message.from_user_id]

const itemComponentRender = (children) => (
<div key={messageId}>
{children}
</div>
)

const messageIdAttr = `${BlOrderChat.BL_ORDER_CHAT_MESSAGE_ID_ATTR_PREFIX}${message.id}`
const renderMessageComponent = () => (
<BlOrderChatMessage id={messageIdAttr}
key={uniqueId() + message.id}
message={message.message}
sentUnixTs={message.sent_unix_ts}
currentUser={currentUser}
fromUser={fromUser}
usersInvolvedInChatLength={chat.users.order.length} />
)
let children = []
if (index === 0) {
// First message.
children = [
<div key={uniqueId()} className="bl-padding"></div>,
renderMessageComponent()
]
}
else if (index === length - 1) {
// Last message.
children = [
renderMessageComponent(onComponentDidMount),
<div key={uniqueId()} className="bl-padding"></div>
]
}
else {
// Message in the middle.
children = [
renderMessageComponent()
]
}
return itemComponentRender(children)
}

render() {
const {
chat: propsChat, isChatLoading,
currentUser
} = this.props

const chat = chatSelector(propsChat, currentUser)
const chatHasMessages = chat && !isEmpty(chat.messages)

return (
<div className="bl-order-chat">
<div className="bl-order-chat-header">
// ...
</div>
<div className="bl-order-chat-content">
{
(chatHasMessages &&
<div className="bl-order-chat-content-inner">
<div className="bl-order-chat-infinite-scroll">
<BlSimpleInfiniteScroll items={chat.messages}
ref={this.infiniteScrollComponentRef}
initialId={() => lastOfArray(chat.messages).id}
itemRenderer={(id, i) => this.messageRenderer(id, i, chat.messages)}
loadingComponent={<BlLoadingSpinnerContainer />}
isInfiniteLoading={isChatLoading}
displayInverse
infiniteLoadBeginEdgeOffset={void 0}
infiniteLoadingBeginBottomOffset={void 0}
onInfiniteLoad={() => this.props.onLoadPreviousChatMessages(chat.id)}
onInfiniteScrollMouseEnter={this.handleInfiniteScrollMouseEnter}
onInfiniteScrollMouseLeave={this.handleInfiniteScrollMouseLeave} />
</div>
</div>
)
||
(isChatLoading &&
<BlLoadingSpinnerContainer />
)
}
</div>
<div className="bl-order-chat-footer">
// ...
</div>
</div>
)
}
}

BlOrderChatBox,包含BlOrderChat:

import React from 'react'
import BlOrderChat from './BlOrderChat';
import BlAlert from '../../core/alert/BlAlert';
import BlLoadingSpinnerContainer from '../../core/animation/loading/BlLoadingSpinnerContainer';

export default class BlOrderChatBox extends React.Component {

constructor(props) {
super(props)

this.node = React.createRef()
}

render() {
const {
ordId, currentChat,
isCurrentChatLoading, currentUser,
err
} = this.props

return (
<div className="bl-order-chat-box" ref={this.node}>
<div className="bl-order-chat-box-inner">
{
(err &&
<BlAlert type="error" message={err} />)
||
(currentChat && (
// ...
<div className="bl-order-chat-box-inner-chat-content">
<BlOrderChat ordId={ordId}
chat={currentChat}
isChatLoading={isCurrentChatLoading}
onLoadPreviousChatMessages={this.props.onLoadPreviousChatMessages}
currentUser={currentUser} />
</div>
))
||
<BlLoadingSpinnerContainer />
}
</div>
</div>
)
}

}

这是呈现 BlOrderChatBox 的组件(它是最顶层的有状态组件):

import React from 'react'
import { POSTJSON } from '../../../js/ajax';
import config from '../../../config/config';
import { newEmptyArrayAble, arrayToArrayAbleItemsOrder, arrayAbleItemsOrderToArray, mergeArrayAbles, newArrayAble, firstOfArrayAble, isArrayAble } from '../../../js/data_structures/arrayable';

export default class BlOrderChatApp extends React.Component {

static NEW_CHAT_ID = 0

static MAX_NUMBER_OF_MESSAGES_TO_LOAD_PER_AJAX = 30

constructor(props) {
super(props)

this.currentUser = globals.USER
this.lastHandleSendMessagePromise = Promise.resolve()
this.newMessagesMap = {}
this.typingUsersDebouncedMap = {}

// Imagine this comes from a database.
const chat = {
// ...
}

const initialState = {
chats: newArrayAble(this.newChat(chat)),
currentChatId: null,
shouldSelectUserForNewChat: false,
newChatReceivingUsers: newEmptyArrayAble(),
isChatListLoading: false,
isCurrentChatLoading: false,
popoverIsOpen: false,
popoverHasOpened: false,
err: void 0,
focusSendMessageTextarea: false,
newChatsIdsMap: {},
currentChatAuthActs: {},
BlOrderChatComponent: null,
}
this.state = initialState

this.handleLoadPreviousChatMessages = this.handleLoadPreviousChatMessages.bind(this)
}

POST(jsonData, callback) {
let requestJSONData
if (typeof jsonData === "string") {
requestJSONData = {
action: jsonData
}
}
else {
requestJSONData = jsonData
}
return POSTJSON(config.ORDER_CHAT_ENDPOINT_URI, {
...requestJSONData,
order_chat_type: this.props.orderChatType,
}).then(response => response.json()).then(json => {
this.POSTResponseData(json, callback)
})
}

POSTResponseData(data, callback) {
if (data.err) {
this.setState({
err: data.err
})
}
else {
callback && callback(data)
}
}

newChat(chat) {
const newChat = {
id: (chat && chat.id) || BlOrderChatApp.NEW_CHAT_ID,
ord_id: this.props.ordId,
users: (chat && chat.users && (isArrayAble(chat.users) ? chat.users : arrayToArrayAbleItemsOrder(chat.users))) || newEmptyArrayAble(),
messages: (chat && chat.messages && (isArrayAble(chat.messages) ? chat.messages : arrayToArrayAbleItemsOrder(chat.messages))) || newEmptyArrayAble(),
first_message_id: (chat && chat.first_message_id) || null,
typing_users_ids_map: (chat && chat.typing_users_ids_map) || {},
}
return newChat
}

isChatNew(chat) {
return (
chat
&&
(chat.id == BlOrderChatApp.NEW_CHAT_ID || this.state.newChatsIdsMap[chat.id])
)
}

loadPreviousChatMessages(chatId, lowestMessageIdOrNull, makeChatIdCurrent) {
this.POST({
act: 'loadPreviousChatMessages',
chat_id: chatId,
lowest_message_id: lowestMessageIdOrNull,
max_number_of_messages_to_load: BlOrderChatApp.MAX_NUMBER_OF_MESSAGES_TO_LOAD_PER_AJAX
}, json => {
this.setState(prevState => {
const chat = prevState.chats.items[chatId]
const messages = arrayToArrayAbleItemsOrder(json.messages)

const newChat = {
...chat,
messages: mergeArrayAbles(messages, chat.messages)
}
const chats = mergeArrayAbles(prevState.chats, newArrayAble(newChat))
return {
...(makeChatIdCurrent ?
{
currentChatId: chatId,
focusSendMessageTextarea: true,
}
:
{
currentChatId: prevState.currentChatId,
}
),
chats,
isCurrentChatLoading: false,
}
})
})
}

loadPreviousChatMessagesIfNotAllLoaded(chatId) {
let lowestMessageIdOrNull
const chat = this.state.chats.items[chatId]
if (
!this.isChatNew(chat)
&&
(lowestMessageIdOrNull = (chat.messages.order.length && firstOfArrayAble(chat.messages).id) || null)
&&
lowestMessageIdOrNull != chat.first_message_id
) {
this.setState({
isCurrentChatLoading: true
}, () => {
this.loadPreviousChatMessages(chat.id, lowestMessageIdOrNull)
})
}
}

handleLoadPreviousChatMessages(chatId) {
this.loadPreviousChatMessagesIfNotAllLoaded(chatId)
}

// ...

render() {
const currentChat = this.state.chats.items[this.state.currentChatId] || null
const err = this.state.err

return (
<div className="bl-order-chat-app">
<BlOrderChatBox currentUser={this.currentUser}
chats={arrayAbleItemsOrderToArray(this.state.chats)}
currentChat={currentChat}
isCurrentChatLoading={this.state.isCurrentChatLoading}
onLoadPreviousChatMessages={this.handleLoadPreviousChatMessages}
err={err} />
</div>
)
}

}

我尝试删除所有不相关的代码以简化阅读。另外,这里是包含 chatSelector 函数(标准化聊天可数组对象)和 *ArrayAble* 函数(对我而言,可数组对象基本上是一个对象,它通过 items 中的 id 映射对象,并具有一个 order 属性来保持排序):

import { isUndefined, unshiftArray, findIndex } from "../utils";

export function chatSelector(chat, currentUser) {
const newChat = { ...chat }
newChat.messages = arrayAbleItemsOrderToArray(chat.messages).sort((a, b) => {
const sortByUnixTs = a.sent_unix_ts - b.sent_unix_ts
if (sortByUnixTs == 0) {
return a.id - b.id
}
return sortByUnixTs
})
newChat.users = arrayAbleItemsOrderToArray(chat.users).filter(user => user.id != currentUser.id)
return newChat
}

/**
* Given an array-able object, returns its array representation using an order property.
* This function acts as a selector function.
*
* The array-able object MUST have the following shape:
*
* {
* items: {},
* order: []
* }
*
* Where "items" is the object containing the elements of the array mapped by values found in "order"
* in order.
*
* @see https://medium.com/javascript-in-plain-english/https-medium-com-javascript-in-plain-english-why-you-should-use-an-object-not-an-array-for-lists-bee4a1fbc8bd
* @see https://medium.com/@antonytuft/maybe-you-would-do-something-like-this-a1ab7f436808
*
* @param {Object} obj An object.
* @param {Object} obj.items The items of the object mapped by keys.
* @param {Array} obj.order The ordered keys.
* @return {Array} The ordered array representation of the given object.
*/
export function arrayAbleItemsOrderToArray(obj) {
const ret = []
for (const key of obj.order) {
if (!isUndefined(obj.items[key])) {
ret[ret.length] = obj.items[key]
}
}
return ret
}

export function arrayToArrayAbleItemsOrder(array, keyProp = "id") {
const obj = newEmptyArrayAble()
for (const elem of array) {
const key = elem[keyProp]
obj.items[key] = elem
obj.order[obj.order.length] = key
}
return obj
}

export function newEmptyArrayAble() {
return {
items: {},
order: []
}
}

export function isEmptyArrayAble(arrayAbleObj) {
return !arrayAbleObj.order.length
}

export function mergeArrayAbles(arrayAbleObj1, arrayAbleObj2, prependObj2 = false) {
const obj = newEmptyArrayAble()
for (const key of arrayAbleObj1.order) {
if (isUndefined(arrayAbleObj1.items[key])) {
continue
}
obj.items[key] = arrayAbleObj1.items[key]
obj.order[obj.order.length] = key
}
for (const key of arrayAbleObj2.order) {
if (isUndefined(arrayAbleObj2.items[key])) {
continue
}
if (!(key in obj.items)) {
if (!prependObj2) {
// Default.
obj.order[obj.order.length] = key
}
else {
unshiftArray(obj.order, key)
}
}
obj.items[key] = arrayAbleObj2.items[key]
}
return obj
}

export function newArrayAble(initialItem = void 0, keyProp = "id") {
const arrayAble = newEmptyArrayAble()
if (initialItem) {
arrayAble.items[initialItem[keyProp]] = initialItem
arrayAble.order[arrayAble.order.length] = initialItem[keyProp]
}
return arrayAble
}

export function lastOfArrayAble(obj) {
return (
(
obj.order.length
&&
obj.items[obj.order[obj.order.length - 1]]
)
||
void 0
)
}

感谢您的帮助。如果缺少我应该包含的内容,请告诉我!

更新:感谢Sultan H.,它已经有所改进,尽管当我收到服务器的回复时滚动仍然会阻塞。在这里查看:https://streamable.com/3nzu0关于如何进一步改善这种行为有什么想法吗?

谢谢!

最佳答案

这是解决性能问题的尝试,最好不要在计算新状态的箭头函数内执行任务,在这种情况下,在 loadPreviousChatMessages 处,您正在回调中计算内容,在该上下文上设置状态时可能会产生负载。

最好的更改,用此代码替换函数内的 this.setState,我在这里所做的只是通过移出所有任务来清除上下文:

            const chat = this.state.chats.items[chatId];
const messages = arrayToArrayAbleItemsOrder(json.messages);
const newChat = {
...chat,
messages: mergeArrayAbles(messages, chat.messages);
}
const chats = mergeArrayAbles(prevState.chats, newArrayAble(newChat));
const newState = {
...(
makeChatIdCurrent ?
{
currentChatId: chatId,
focusSendMessageTextarea: true,
}
:
{
currentChatId: this.state.currentChatId,
}
),
chats,
isCurrentChatLoading: false,
};

this.setState(() => newState);

如果这不能完全解决问题,您能否判断是否至少有改进?

关于javascript - React无限滚动组件性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56987356/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com