Skip to content

Commit

Permalink
Optimize VirtualizedList
Browse files Browse the repository at this point in the history
  • Loading branch information
halaei committed Apr 8, 2021
1 parent 5f0bf8b commit 84da4e5
Showing 1 changed file with 140 additions and 90 deletions.
230 changes: 140 additions & 90 deletions Libraries/Lists/VirtualizedList.js
Original file line number Diff line number Diff line change
Expand Up @@ -784,8 +784,50 @@ class VirtualizedList extends React.PureComponent<Props, State> {
};
}

_cellsCache = new Map();
_cells = [];

_markCellsAsUnused() {
this._cellsCache.forEach(cell => {
cell.used = false;
});
}

_deleteUnusedCells() {
this._cellsCache.forEach((cell, key) => {
if (!cell.used) {
this._cellsCache.delete(key);
}
});
}

_pushCell(key, deps, creator) {
let cell = this._cellsCache.get(key);
let depsChanged = false;
if (!cell || cell.deps.length !== deps.length) {
depsChanged = true;
} else {
for (let i = 0; i < deps.length; i++) {
if (cell.deps[i] !== deps[i]) {
depsChanged = true;
break;
}
}
}
if (depsChanged) {
this._cellsCache.set(
key,
(cell = {
component: creator(),
deps,
}),
);
}
cell.used = true;
this._cells.push(cell.component);
}

_pushCells(
cells: Array<Object>,
stickyHeaderIndices: Array<number>,
stickyIndicesFromProps: Set<number>,
first: number,
Expand All @@ -809,9 +851,19 @@ class VirtualizedList extends React.PureComponent<Props, State> {
const key = this._keyExtractor(item, ii);
this._indicesToKeys.set(ii, key);
if (stickyIndicesFromProps.has(ii + stickyOffset)) {
stickyHeaderIndices.push(cells.length);
stickyHeaderIndices.push(this._cells.length);
}
cells.push(
const deps = [
CellRendererComponent,
ItemSeparatorComponent,
horizontal,
ii,
inversionStyle,
item,
prevCellKey,
this.props,
];
this._pushCell(key, deps, () => (
<CellRenderer
CellRendererComponent={CellRendererComponent}
ItemSeparatorComponent={ii < end ? ItemSeparatorComponent : undefined}
Expand All @@ -830,8 +882,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
ref={ref => {
this._cellRefs[key] = ref;
}}
/>,
);
/>
));
prevCellKey = key;
}
}
Expand Down Expand Up @@ -895,37 +947,40 @@ class VirtualizedList extends React.PureComponent<Props, State> {
? styles.horizontallyInverted
: styles.verticallyInverted
: null;
const cells = [];
this._cells = [];
this._markCellsAsUnused();
const stickyIndicesFromProps = new Set(this.props.stickyHeaderIndices);
const stickyHeaderIndices = [];
if (ListHeaderComponent) {
if (stickyIndicesFromProps.has(0)) {
stickyHeaderIndices.push(0);
}
const element = React.isValidElement(ListHeaderComponent) ? (
ListHeaderComponent
) : (
// $FlowFixMe[not-a-component]
// $FlowFixMe[incompatible-type-arg]
<ListHeaderComponent />
);
cells.push(
<VirtualizedListCellContextProvider
cellKey={this._getCellKey() + '-header'}
key="$header">
<View
onLayout={this._onLayoutHeader}
style={StyleSheet.compose(
inversionStyle,
this.props.ListHeaderComponentStyle,
)}>
{
// $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors
element
}
</View>
</VirtualizedListCellContextProvider>,
);
this._pushCell('$header', [], () => {
const element = React.isValidElement(ListHeaderComponent) ? (
ListHeaderComponent
) : (
// $FlowFixMe[not-a-component]
// $FlowFixMe[incompatible-type-arg]
<ListHeaderComponent />
);
return (
<VirtualizedListCellContextProvider
cellKey={this._getCellKey() + '-header'}
key="$header">
<View
onLayout={this._onLayoutHeader}
style={StyleSheet.compose(
inversionStyle,
this.props.ListHeaderComponentStyle,
)}>
{
// $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors
element
}
</View>
</VirtualizedListCellContextProvider>
);
});
}
const itemCount = this.props.getItemCount(data);
if (itemCount > 0) {
Expand All @@ -937,7 +992,6 @@ class VirtualizedList extends React.PureComponent<Props, State> {
: initialNumToRenderOrDefault(this.props.initialNumToRender) - 1;
const {first, last} = this.state;
this._pushCells(
cells,
stickyHeaderIndices,
stickyIndicesFromProps,
0,
Expand All @@ -958,11 +1012,10 @@ class VirtualizedList extends React.PureComponent<Props, State> {
stickyBlock.offset -
initBlock.offset -
(this.props.initialScrollIndex ? 0 : initBlock.length);
cells.push(
this._cells.push(
<View key="$sticky_lead" style={{[spacerKey]: leadSpace}} />,
);
this._pushCells(
cells,
stickyHeaderIndices,
stickyIndicesFromProps,
ii,
Expand All @@ -972,7 +1025,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
const trailSpace =
this._getFrameMetricsApprox(first).offset -
(stickyBlock.offset + stickyBlock.length);
cells.push(
this._cells.push(
<View key="$sticky_trail" style={{[spacerKey]: trailSpace}} />,
);
insertedStickySpacer = true;
Expand All @@ -985,13 +1038,12 @@ class VirtualizedList extends React.PureComponent<Props, State> {
const firstSpace =
this._getFrameMetricsApprox(first).offset -
(initBlock.offset + initBlock.length);
cells.push(
this._cells.push(
<View key="$lead_spacer" style={{[spacerKey]: firstSpace}} />,
);
}
}
this._pushCells(
cells,
stickyHeaderIndices,
stickyIndicesFromProps,
firstAfterInitial,
Expand Down Expand Up @@ -1019,22 +1071,22 @@ class VirtualizedList extends React.PureComponent<Props, State> {
endFrame.offset +
endFrame.length -
(lastFrame.offset + lastFrame.length);
cells.push(
this._cells.push(
<View key="$tail_spacer" style={{[spacerKey]: tailSpacerLength}} />,
);
}
} else if (ListEmptyComponent) {
const element: React.Element<any> = ((React.isValidElement(
ListEmptyComponent,
) ? (
ListEmptyComponent
) : (
// $FlowFixMe[not-a-component]
// $FlowFixMe[incompatible-type-arg]
<ListEmptyComponent />
)): any);
cells.push(
React.cloneElement(element, {
this._pushCell('$empty', [], () => {
const element: React.Element<any> = ((React.isValidElement(
ListEmptyComponent,
) ? (
ListEmptyComponent
) : (
// $FlowFixMe[not-a-component]
// $FlowFixMe[incompatible-type-arg]
<ListEmptyComponent />
)): any);
return React.cloneElement(element, {
key: '$empty',
onLayout: event => {
this._onLayoutEmpty(event);
Expand All @@ -1043,34 +1095,36 @@ class VirtualizedList extends React.PureComponent<Props, State> {
}
},
style: StyleSheet.compose(inversionStyle, element.props.style),
}),
);
});
});
}
if (ListFooterComponent) {
const element = React.isValidElement(ListFooterComponent) ? (
ListFooterComponent
) : (
// $FlowFixMe[not-a-component]
// $FlowFixMe[incompatible-type-arg]
<ListFooterComponent />
);
cells.push(
<VirtualizedListCellContextProvider
cellKey={this._getFooterCellKey()}
key="$footer">
<View
onLayout={this._onLayoutFooter}
style={StyleSheet.compose(
inversionStyle,
this.props.ListFooterComponentStyle,
)}>
{
// $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors
element
}
</View>
</VirtualizedListCellContextProvider>,
);
this._pushCell('$footer', [], () => {
const element = React.isValidElement(ListFooterComponent) ? (
ListFooterComponent
) : (
// $FlowFixMe[not-a-component]
// $FlowFixMe[incompatible-type-arg]
<ListFooterComponent />
);
return (
<VirtualizedListCellContextProvider
cellKey={this._getFooterCellKey()}
key="$footer">
<View
onLayout={this._onLayoutFooter}
style={StyleSheet.compose(
inversionStyle,
this.props.ListFooterComponentStyle,
)}>
{
// $FlowFixMe[incompatible-type] - Typing ReactNativeComponent revealed errors
element
}
</View>
</VirtualizedListCellContextProvider>
);
});
}
const scrollProps = {
...this.props,
Expand Down Expand Up @@ -1117,11 +1171,12 @@ class VirtualizedList extends React.PureComponent<Props, State> {
{
ref: this._captureScrollRef,
},
cells,
this._cells,
)}
</VirtualizedListContextProvider>
);
let ret = innerRet;
this._deleteUnusedCells();
if (__DEV__) {
ret = (
<ScrollView.Context.Consumer>
Expand Down Expand Up @@ -1931,33 +1986,27 @@ type CellRendererProps = {
};

type CellRendererState = {
separatorProps: $ReadOnly<{|
highlighted: boolean,
leadingItem: ?Item,
|}>,
highlighted: boolean,
leadingItem: ?Item,
...
};

class CellRenderer extends React.Component<
class CellRenderer extends React.PureComponent<
CellRendererProps,
CellRendererState,
> {
state = {
separatorProps: {
highlighted: false,
leadingItem: this.props.item,
},
highlighted: false,
leadingItem: this.props.item,
};

static getDerivedStateFromProps(
props: CellRendererProps,
prevState: CellRendererState,
): ?CellRendererState {
return {
separatorProps: {
...prevState.separatorProps,
leadingItem: props.item,
},
...prevState,
leadingItem: props.item,
};
}

Expand Down Expand Up @@ -1987,7 +2036,8 @@ class CellRenderer extends React.Component<

updateSeparatorProps(newProps: Object) {
this.setState(state => ({
separatorProps: {...state.separatorProps, ...newProps},
...state,
...newProps,
}));
}

Expand Down Expand Up @@ -2060,7 +2110,7 @@ class CellRenderer extends React.Component<
// NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and
// called explicitly by `ScrollViewStickyHeader`.
const itemSeparator = ItemSeparatorComponent && (
<ItemSeparatorComponent {...this.state.separatorProps} />
<ItemSeparatorComponent {...this.state} />
);
const cellStyle = inversionStyle
? horizontal
Expand Down

0 comments on commit 84da4e5

Please sign in to comment.