导航菜单

  • 0.api
  • 0.Async
  • 0.module
  • 1.ES2015
  • 2.Promise
  • 3.Node
  • 4.NodeInstall
  • 5.REPL
  • 6.NodeCore
  • 7.module&NPM
  • 8.Encoding
  • 9.Buffer
  • 10.fs
  • 11.Stream-1
  • 11.Stream-2
  • 11.Stream-3
  • 11.Stream-4
  • 12-Network-2
  • 12.NetWork-3
  • 12.Network-1
  • 13.tcp
  • 14.http-1
  • 14.http-2
  • 15.compress
  • 16.crypto
  • 17.process
  • 18.yargs
  • 19.cache
  • 20.action
  • 21.https
  • 22.cookie
  • 23.session
  • 24.express-1
  • 24.express-2
  • 24.express-3
  • 24.express-4
  • 25.koa-1
  • 26.webpack-1-basic
  • 26.webpack-2-optimize
  • 26.webpack-3-file
  • 26.webpack-4.tapable
  • 26.webpack-5-AST
  • 26.webpack-6-sources
  • 26.webpack-7-loader
  • 26.webpack-8-plugin
  • 26.webpack-9-hand
  • 26.webpack-10-prepare
  • 28.redux
  • 28.redux-jwt-back
  • 28.redux-jwt-front
  • 29.mongodb-1
  • 29.mongodb-2
  • 29.mongodb-3
  • 29.mongodb-4
  • 29.mongodb-5
  • 29.mongodb-6
  • 30.cms-1-mysql
  • 30.cms-2-mysql
  • 30.cms-3-mysql
  • 30.cms-4-nunjucks
  • 30.cms-5-mock
  • 30.cms-6-egg
  • 30.cms-7-api
  • 30.cms-8-roadhog
  • 30.cms-9-yaml
  • 30.cms-10-umi
  • 30.cms-12-dva
  • 30.cms-13-dva-ant
  • 30.cms-14-front
  • 30.cms-15-deploy
  • 31.dva
  • 31.cms-13-dva-antdesign
  • 33.redis
  • 34.unittest
  • 35.jwt
  • 36.websocket-1
  • 36.websocket-2
  • 38.chat-api-1
  • 38.chat-api-2
  • 38.chat-3
  • 38.chat-api-3
  • 38.chat
  • 38.chat2
  • 38.chat2
  • 39.crawl-0
  • 39.crawl-1
  • 39.crawl-2
  • 40.deploy
  • 41.safe
  • 42.test
  • 43.nginx
  • 44.enzyme
  • 45.docker
  • 46.elastic
  • 47.oauth
  • 48.wxpay
  • index
  • 52.UML
  • 53.design
  • index
  • 54.linux
  • 57.ts
  • 56.react-ssr
  • 58.ts_react
  • 59.ketang
  • 59.ketang2
  • 61.1.devops-linux
  • 61.2.devops-vi
  • 61.3.devops-user
  • 61.4.devops-auth
  • 61.5.devops-shell
  • 61.6.devops-install
  • 61.7.devops-system
  • 61.8.devops-service
  • 61.9.devops-network
  • 61.10.devops-nginx
  • 61.11.devops-docker
  • 61.12.devops-jekins
  • 61.13.devops-groovy
  • 61.14.devops-php
  • 61.15.devops-java
  • 61.16.devops-node
  • 61.17.devops-k8s
  • 62.1.react-basic
  • 62.2.react-state
  • 62.3.react-high
  • 62.4.react-optimize
  • 62.5.react-hooks
  • 62.6.react-immutable
  • 62.7.react-mobx
  • 62.8.react-source
  • 63.1.redux
  • 63.2.redux-middleware
  • 63.3.redux-hooks
  • 63.4.redux-saga
  • 63.5.redux-saga-hand
  • 64.1.router
  • 64.2.router-connected
  • 65.1.typescript
  • 65.2.typescript
  • 65.3.typescript
  • 65.4.antd
  • 65.4.definition
  • 66-1.vue-base
  • 66-2.vue-component
  • 66-3.vue-cli3.0
  • 66-4.$message组件
  • 66-5.Form组件
  • 66-6.tree
  • 66-7.vue-router-apply
  • 66-8.axios-apply
  • 66-9.vuex-apply
  • 66-10.jwt-vue
  • 66-11.vue-ssr
  • 66-12.nuxt-apply
  • 66-13.pwa
  • 66-14.vue单元测试
  • 66-15.权限校验
  • 67-1-network
  • 68-2-wireshark
  • 7.npm2
  • 69-hooks
  • 70-deploy
  • 71-hmr
  • 72.deploy
  • 73.import
  • 74.mobile
  • 75.webpack-1.文件分析
  • 75.webpack-2.loader
  • 75.webpack-3.源码流程
  • 75.webpack-4.tapable
  • 75.webpack-5.prepare
  • 75.webpack-6.resolve
  • 75.webpack-7.loader
  • 75.webpack-8.module
  • 75.webpack-9.chunk
  • 75.webpack-10.asset
  • 75.webpack-11.实现
  • 76.react_optimize
  • 77.ts_ketang_back
  • 77.ts_ketang_front
  • 78.vue-domdiff
  • 79.grammar
  • 80.tree
  • 81.axios
  • 82.1.react
  • 82.2.react-high
  • 82.3.react-router
  • 82.4.redux
  • 82.5.redux_middleware
  • 82.6.connected
  • 82.7.saga
  • 82.8.dva
  • 82.8.dva-source
  • 82.9.roadhog
  • 82.10.umi
  • 82.11.antdesign
  • 82.12.ketang-front
  • 82.12.ketang-back
  • 83.upload
  • 84.graphql
  • 85.antpro
  • 86.1.uml
  • 86.2.design
  • 87.postcss
  • 88.react16-1
  • 89.nextjs
  • 90.react-test
  • 91.react-ts
  • 92.rbac
  • 93.tsnode
  • 94.1.JavaScript
  • 94.2.JavaScript
  • 94.3.MODULE
  • 94.4.EventLoop
  • 94.5.文件上传
  • 94.6.https
  • 94.7. nginx
  • 95.1. react
  • 95.2.react
  • 96.1.react16
  • 96.2.fiber
  • 96.3.fiber
  • 97.serverless
  • 98.websocket
  • 100.1.react-basic
  • 101.1.monitor
  • 101.2.monitor
  • 102.java
  • 103.1.webpack-usage
  • 103.2.webpack-bundle
  • 103.3.webpack-ast
  • 103.4.webpack-flow
  • 103.5.webpack-loader
  • 103.6.webpack-tapable
  • 103.7.webpack-plugin
  • 103.8.webpack-optimize1
  • 103.9.webpack-optimize2
  • 103.10.webpack-hand
  • 103.11.webpack-hmr
  • 103.11.webpack5
  • 103.13.splitChunks
  • 103.14.webpack-sourcemap
  • 103.15.webpack-compiler1
  • 103.15.webpack-compiler2
  • 103.16.rollup.1
  • 103.16.rollup.2
  • 103.16.rollup.3
  • 103.16.vite.basic
  • 103.16.vite.source
  • 103.16.vite.plugin
  • 103.16.vite.1
  • 103.16.vite.2
  • 103.17.polyfill
  • 104.1.binary
  • 104.2.binary
  • 105.skeleton
  • 106.1.react
  • 106.2.react_hooks
  • 106.3.react_router
  • 106.4.redux
  • 106.5.redux_middleware
  • 106.6.connected-react-router
  • 106.6.redux-first-history
  • 106.7.redux-saga
  • 106.8.dva
  • 106.9.umi
  • 106.10.ketang
  • 106.11.antdesign
  • 106.12.antpro
  • 106.13.router-6
  • 106.14.ssr
  • 106.15.nextjs
  • 106.16.1.cms
  • 106.16.2.cms
  • 106.16.3.cms
  • 106.16.4.cms
  • 106.16.mobx
  • 106.17.fomily
  • 107.fiber
  • 108.http
  • 109.1.webpack_usage
  • 109.2.webpack_source
  • 109.3.dll
  • 110.nest.js
  • 111.xstate
  • 112.Form
  • 113.redux-saga
  • 114.react+typescript
  • 115.immer
  • 116.pro5
  • 117.css-loader
  • 118.1.umi-core
  • 119.2.module-federation
  • 119.1.module-federation
  • 120.create-react-app
  • 121.react-scripts
  • 122.react-optimize
  • 123.jsx-runtime
  • 124.next.js
  • 125.1.linux
  • 125.2.linux-vi
  • 125.3.linux-user
  • 125.4.linux-auth
  • 125.5.linux-shell
  • 125.6.linux-install
  • 125.7.linux-system
  • 125.8.linux-service
  • 125.9.linux-network
  • 125.10.nginx
  • 125.11.docker
  • 125.12.ci
  • 125.13.k8s
  • 125.14.k8s
  • 125.15.k8s
  • 125.16.k8s
  • 126.11.react-1
  • 126.12.react-2
  • 126.12.react-3
  • 126.12.react-4
  • 126.12.react-5
  • 126.12.react-6
  • 126.12.react-7
  • 126.12.react-8
  • 127.frontend
  • 128.rollup
  • 129.px2rem-loader
  • 130.health
  • 131.hooks
  • 132.keepalive
  • 133.vue-cli
  • 134.react18
  • 134.2.react18
  • 134.3.react18
  • 135.function
  • 136.toolkit
  • 137.lerna
  • 138.create-vite
  • 139.cli
  • 140.antd
  • 141.react-dnd
  • 142.1.link
  • 143.1.gulp
  • 143.2.stream
  • 143.3.gulp
  • 144.1.closure
  • 144.2.v8
  • 144.3.gc
  • 145.react-router-v6
  • 146.browser
  • 147.lighthouse
  • 148.1.basic
  • 148.2.basic
  • 148.3.basic
  • 148.4.basic
  • 148.5.basic
  • 149.1.vite
  • 149.2.vite
  • 149.3.vite
  • 149.4.vite
  • 150.react-window
  • 151.react-query
  • 152.useRequest
  • 153.transition
  • 154.emotion
  • 155.1.formily
  • 155.2.formily
  • 155.3.formily
  • 155.3.1.mobx.usage
  • 155.3.2.mobx.source
  • 156.vue-loader
  • 103.11.mf
  • 157.1.react18
  • 158.umi4
  • 159.rxjs
  • 159.rxjs2
  • 160.bff
  • 161.zustand
  • 162.vscode
  • 163.emp
  • 164.cors
  • 1. 长列表渲染
  • 2.长列表组件
  • 3. 固定高度列表实战
    • 3.1 src\index.js
    • 3.2 fixed-size-list.js
    • 3.3 fixed-size-list.css
  • 4. 全部渲染
    • 4.1 fixed-size-list.js
    • 4.2 react-window\index.js
    • 4.3 FixedSizeList.js
    • 4.4 createListComponent.js
  • 5. 渲染首屏
    • 5.1 FixedSizeList.js
    • 5.2 createListComponent.js
  • 5. 监听滚动
    • 5.1 createListComponent.js
  • 6. overscan
    • 6.1 createListComponent.js
  • 7. VariableSizeList实战
    • 7.1 src\index.js
    • 7.2 variable-size-list.js
    • 7.3 variable-size-list.css
  • 8. initInstanceProps
    • 8.1 variable-size-list.js
    • 8.2 src\react-window\index.js
    • 8.3 VariableSizeList.js
    • 8.4 createListComponent.js
  • 9. 预估总高度
    • 9.1 src\react-window\VariableSizeList.js
    • 9.2 src\react-window\createListComponent.js
  • 10. 动态计算高度
    • 10.1 VariableSizeList.js
    • 10.2 createListComponent.js
  • 11. 优化方案
    • 11.1 缓存样式
      • 11.1.1 createListComponent.js
    • 11.2 二分查找和指数扩充
      • 11.2.1 VariableSizeList.js
    • 11.3 IntersectionObserver
      • 11.3.1 createListComponent.js
  • 13. 动态高度列表
    • 13.1 src\index.js
    • 13.2 dynamic-size-list.js
    • 13.4 VariableSizeList.js
    • 13.5 createListComponent.js
  • 14. 滚动状态
    • 14.1 src\index.js
    • 14.2 fixed-size-list.js
    • 14.3 src\react-window\createListComponent.js
  • 15. 滚动到指定条目
    • 15.1 src\fixed-size-list.js
    • 15.2 FixedSizeList.js
    • 15.3 src\react-window\createListComponent.js
  • 16.其它方案
    • 16.1 src\index.js
    • 16.2 src\Virtuoso.js
      • 16.3 ResizeObserver

1. 长列表渲染 #

  • 如果有海量数据在浏览器里一次性渲染会有以下问题
    • 计算时间过长,用户需要长时间等待,体验差
    • CPU处理时间过长,滑动过程中可能卡顿
    • GPU负载过高,渲染不过来会出现闪动
    • 内存占用过多,严重会引起浏览器卡死和崩溃
  • 优化方法
    • 下拉底部加载更多实现懒加载,此方法随着内容越来越多,会引起大量的重排和重绘,依赖可能会卡顿
    • 虚拟列表 其实我们的屏幕可视区域是有限的,能看到的数据也是有限的,所以可以在用户滚动时,只渲染可视区域内的内容即可,不可见区域用空白占位填充, 这样的话页面中的DOM元素少,CPU、GPU和内存负载小

2.长列表组件 #

  • react-virtualized
  • react-window
  • react-window.vercel.app
npm i react-window --save

3. 固定高度列表实战 #

3.1 src\index.js #

import React from 'react';
import ReactDOM from 'react-dom/client';
import FixedSizeList from './fixed-size-list';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<FixedSizeList />);

3.2 fixed-size-list.js #

src\fixed-size-list.js

import {FixedSizeList} from 'react-window';
import './fixed-size-list.css';
const Row = ({index,style})=>(
    <div className={index % 2 ? 'ListItemOdd' : 'ListItemEven'} style={style}>Row{index}</div>
)
function App(){
    return (
        <FixedSizeList
          className='List'
          height={200}
          width={200}
          itemSize={50}
          itemCount={1000}
        >
            {Row}
        </FixedSizeList>
    )
}
export default App;

3.3 fixed-size-list.css #

src\fixed-size-list.css

.List {
    border: 1px solid gray;
}

.ListItemEven,
.ListItemOdd {
    display: flex;
    align-items: center;
    justify-content: center;
}
.ListItemOdd {
    background-color: lightcoral;
}
.ListItemEven {
    background-color: lightblue;
}

4. 全部渲染 #

原理

4.1 fixed-size-list.js #

src\fixed-size-list.js

import {FixedSizeList} from './react-window';
import './fixed-size-list.css';
const Row = ({index,style})=>(
    <div className={index % 2 ? 'ListItemOdd' : 'ListItemEven'} style={style}>Row{index}</div>
)
function App(){
    return (
        <FixedSizeList
          className='List'
          height={200}
          width={200}
          itemSize={50}
          itemCount={1000}
        >
            {Row}
        </FixedSizeList>
    )
}
export default App;

4.2 react-window\index.js #

src\react-window\index.js

export { default as FixedSizeList } from './FixedSizeList';

4.3 FixedSizeList.js #

src\react-window\FixedSizeList.js

import createListComponent from './createListComponent';
const FixedSizeList = createListComponent({
    getItemSize: ({ itemSize }) => itemSize,//每个条目的高度
    getEstimatedTotalSize: ({ itemSize, itemCount }) => itemSize * itemCount, //获取预计的总高度
    getItemOffset: ({ itemSize }, index) => itemSize * index //获取每个条目的偏移量
});
export default FixedSizeList;

4.4 createListComponent.js #

src\react-window\createListComponent.js

import React from 'react';
export default function createListComponent({
    getEstimatedTotalSize,//获取预计的总高度
    getItemSize,//每个条目的高度
    getItemOffset//获取每个条目的偏移量
}) {
    return class extends React.Component {
        render() {
            const {width,height,itemCount,children:ComponentType} = this.props;
            const containerStyle = {position:'relative',width,height,overflow:'auto', willChange: 'transform'};
            const contentStyle = {height:getEstimatedTotalSize(this.props),width:'100%'};
            const items = [];
            if(itemCount>0){
                for(let index = 0;index<itemCount;index++){
                    items.push(
                        <ComponentType key={index} index={index} style={this._getItemStyle(index)}/>
                    );
                }
            }
            return (
                <div style={containerStyle}>
                    <div style={contentStyle}>
                        {items}
                    </div>
                </div>
            )
        }
        _getItemStyle=(index)=>{
            const style = {
                position:'absolute',
                width:'100%',
                height:getItemSize(this.props),
                top:getItemOffset(this.props,index)
            };
            return style;
        }
    }
}

5. 渲染首屏 #

5.1 FixedSizeList.js #

src\react-window\FixedSizeList.js

import createListComponent from './createListComponent';
const FixedSizeList = createListComponent({
    getItemSize: ({ itemSize }) => itemSize,//每个条目的高度
    getEstimatedTotalSize: ({ itemSize, itemCount }) => itemSize * itemCount, //获取预计的总高度
    getItemOffset: ({ itemSize }, index) => itemSize * index, //获取每个条目的偏移量
+   getStartIndexForOffset: ({ itemSize }, offset) => Math.floor(offset / itemSize),//获取起始索引
+   getStopIndexForStartIndex: ({ height, itemSize }, startIndex) => {//获取结束索引
+       const numVisibleItems = Math.ceil(height / itemSize);
+       return startIndex + numVisibleItems - 1;
    }
});
export default FixedSizeList;

5.2 createListComponent.js #

src\react-window\createListComponent.js

import React from 'react';
export default function createListComponent({
    getEstimatedTotalSize,//获取预计的总高度
    getItemSize,//每个条目的高度
    getItemOffset,//获取每个条目的偏移量
+   getStartIndexForOffset,
+   getStopIndexForStartIndex
}) {
    return class extends React.Component {
+       state = { scrollOffset: 0 }
        render() {
            const { width, height, itemCount, children: ComponentType } = this.props;
            const containerStyle = { position: 'relative', width, height, overflow: 'auto', willChange: 'transform' };
            const contentStyle = { height: getEstimatedTotalSize(this.props), width: '100%' };
            const items = [];
            if (itemCount > 0) {
+               const [startIndex, stopIndex] = this._getRangeToRender();
+               for (let index = startIndex; index <= stopIndex; index++) {
                    items.push(
                        <ComponentType key={index} index={index} style={this._getItemStyle(index)} />
                    );
                }
            }
            return (
                <div style={containerStyle}>
                    <div style={contentStyle}>
                        {items}
                    </div>
                </div>
            )
        }
        _getItemStyle = (index) => {
            const style = {
                position: 'absolute',
                width: '100%',
                height: getItemSize(this.props),
                top: getItemOffset(this.props, index)
            };
            return style;
        }
+       _getRangeToRender = () => {
+           const { scrollOffset } = this.state;
+           const startIndex = getStartIndexForOffset(this.props, scrollOffset);
+           const stopIndex = getStopIndexForStartIndex(this.props, startIndex);
+           return [startIndex, stopIndex];
+       }
    }
}

5. 监听滚动 #

5.1 createListComponent.js #

src\react-window\createListComponent.js

import React from 'react';
export default function createListComponent({
    getEstimatedTotalSize,//获取预计的总高度
    getItemSize,//每个条目的高度
    getItemOffset,//获取每个条目的偏移量
    getStartIndexForOffset,
    getStopIndexForStartIndex
}) {
    return class extends React.Component {
        state = { scrollOffset: 0 }
        render() {
            const { width, height, itemCount, children: ComponentType } = this.props;
            const containerStyle = { position: 'relative', width, height, overflow: 'auto', willChange: 'transform' };
            const contentStyle = { height: getEstimatedTotalSize(this.props), width: '100%' };
            const items = [];
            if (itemCount > 0) {
                const [startIndex, stopIndex] = this._getRangeToRender();
                for (let index = startIndex; index <= stopIndex; index++) {
                    items.push(
                        <ComponentType key={index} index={index} style={this._getItemStyle(index)} />
                    );
                }
            }
            return (
+               <div style={containerStyle} onScroll={this.onScroll}>
                    <div style={contentStyle}>
                        {items}
                    </div>
                </div>
            )
        }
+       onScroll = event => {
+           const { scrollTop } = event.currentTarget;
+           this.setState({ scrollOffset: scrollTop });
+       }
        _getItemStyle = (index) => {
            const style = {
                position: 'absolute',
                width: '100%',
                height: getItemSize(this.props),
                top: getItemOffset(this.props, index)
            };
            return style;
        }
        _getRangeToRender = () => {
            const { scrollOffset } = this.state;
            const startIndex = getStartIndexForOffset(this.props, scrollOffset);
            const stopIndex = getStopIndexForStartIndex(this.props, startIndex);
            return [startIndex, stopIndex]
        }
    }
}

6. overscan #

  • 过扫描实质上是切断图片的边缘,以确保所有重要的东西显示在屏幕上

6.1 createListComponent.js #

src\react-window\createListComponent.js

import React from 'react';
export default function createListComponent({
    getEstimatedTotalSize,//获取预计的总高度
    getItemSize,//每个条目的高度
    getItemOffset,//获取每个条目的偏移量
    getStartIndexForOffset,
    getStopIndexForStartIndex
}) {
    return class extends React.Component {
+       static defaultProps = {
+           overscanCount: 2
+       }
        state = { scrollOffset: 0 }
        render() {
            const { width, height, itemCount, children: ComponentType } = this.props;
            const containerStyle = { position: 'relative', width, height, overflow: 'auto', willChange: 'transform' };
            const contentStyle = { height: getEstimatedTotalSize(this.props), width: '100%' };
            const items = [];
            if (itemCount > 0) {
                const [startIndex, stopIndex] = this._getRangeToRender();
                for (let index = startIndex; index <= stopIndex; index++) {
                    items.push(
                        <ComponentType key={index} index={index} style={this._getItemStyle(index)} />
                    );
                }
            }
            return (
                <div style={containerStyle} onScroll={this.onScroll}>
                    <div style={contentStyle}>
                        {items}
                    </div>
                </div>
            )
        }
        onScroll = event => {
            const { scrollTop } = event.currentTarget;
            this.setState({ scrollOffset: scrollTop });
        }
        _getItemStyle = (index) => {
            const style = {
                position: 'absolute',
                width: '100%',
                height: getItemSize(this.props),
                top: getItemOffset(this.props, index)
            };
            return style;
        }
        _getRangeToRender = () => {
            const { scrollOffset } = this.state;
+           const { itemCount, overscanCount } = this.props;
            const startIndex = getStartIndexForOffset(this.props, scrollOffset);
            const stopIndex = getStopIndexForStartIndex(this.props, startIndex);
            return [
+               Math.max(0, startIndex - overscanCount),
+               Math.max(0, Math.min(itemCount - 1, stopIndex + overscanCount)),
                startIndex, stopIndex]
        }
    }
}

7. VariableSizeList实战 #

7.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import FixedSizeList from './fixed-size-list';
+import VariableSizeList from './variable-size-list';
const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(<VariableSizeList />);

7.2 variable-size-list.js #

src\variable-size-list.js

import React from 'react';
import { VariableSizeList } from 'react-window';
import './variable-size-list.css';

const rowSizes = new Array(1000)
    .fill(true)
    .map(() => 25 + Math.round(Math.random() * 50));

const getItemSize = index => rowSizes[index];

const Row = ({ index, style }) => (
    <div className={index % 2 ? 'ListItemOdd' : 'ListItemEven'} style={style}>
        Row {index}
    </div>
)
const App = () => {
    return (
        <VariableSizeList
            className='List'
            height={200}
            width={200}
            itemSize={getItemSize}
            itemCount={1000}
        >
            {Row}
        </VariableSizeList>
    )
}
export default App;

7.3 variable-size-list.css #

src\variable-size-list.css

.List {
    border: 1px solid gray;
}

.ListItemEven,
.ListItemOdd {
    display: flex;
    align-items: center;
    justify-content: center;
}
.ListItemOdd {
    background-color: lightcoral;
}
.ListItemEven {
    background-color: lightblue;
}

8. initInstanceProps #

8.1 variable-size-list.js #

src\variable-size-list.js

import React from 'react';
+import { VariableSizeList } from './react-window';
import './variable-size-list.css';

const rowSizes = new Array(1000)
    .fill(true)
    .map(() => 25 + Math.round(Math.random() * 50));

const getItemSize = index => rowSizes[index];

const Row = ({ index, style }) => (
    <div className={index % 2 ? 'ListItemOdd' : 'ListItemEven'} style={style}>
        Row {index}
    </div>
)
const App = () => {
    return (
        <VariableSizeList
            className='List'
            height={200}
            width={200}
            itemSize={getItemSize}
            itemCount={1000}
        >
            {Row}
        </VariableSizeList>
    )
}
export default App;

8.2 src\react-window\index.js #

src\react-window\index.js

export { default as FixedSizeList } from './FixedSizeList';
+export { default as VariableSizeList } from './VariableSizeList';

8.3 VariableSizeList.js #

src\react-window\VariableSizeList.js

import createListComponent from './createListComponent';
+const DEFAULT_ESTIMATED_SIZE = 50;
+const getEstimatedTotalSize = () => {}
+const VariableSizeList = createListComponent({
    getEstimatedTotalSize,
    getStartIndexForOffset: () => 0,
    getStopIndexForStartIndex: () => 0,
    getItemSize: () => 0,
    getItemOffset: () => 0,
+   initInstanceProps(props) {
+       const { estimatedItemSize } = props;
+       const instanceProps = {
+           estimatedItemSize: estimatedItemSize || DEFAULT_ESTIMATED_SIZE
+       }
+       return instanceProps;
+   }
});
+export default VariableSizeList;

8.4 createListComponent.js #

src\react-window\createListComponent.js

import React from 'react';
export default function createListComponent({
    getEstimatedTotalSize,//获取预计的总高度
    getItemSize,//每个条目的高度
    getItemOffset,//获取每个条目的偏移量
    getStartIndexForOffset,
    getStopIndexForStartIndex,
+   initInstanceProps
}) {
    return class extends React.Component {
+       instanceProps = initInstanceProps&&initInstanceProps(this.props)
        static defaultProps = {
            overscanCount: 2
        }
    }
}

9. 预估总高度 #

9.1 src\react-window\VariableSizeList.js #

src\react-window\VariableSizeList.js

import createListComponent from './createListComponent';
const DEFAULT_ESTIMATED_SIZE = 50;
+const getEstimatedTotalSize = ({ itemCount }, { estimatedItemSize }) => {
+    const numUnmeasuredItems = itemCount;//未测量的条目
+    const totalSizeOfUnmeasuredItems = numUnmeasuredItems * estimatedItemSize;//未测量条目的总高度
+    return totalSizeOfUnmeasuredItems;
+}
const VariableSizeList = createListComponent({
    getEstimatedTotalSize,
    getStartIndexForOffset: () => 0,
    getStopIndexForStartIndex: () => 0,
    getItemSize: () => 0,
    getItemOffset: () => 0,
    initInstanceProps(props) {
        const { estimatedItemSize } = props;
        const instanceProps = {
            estimatedItemSize: estimatedItemSize || DEFAULT_ESTIMATED_SIZE
        }
        return instanceProps;
    }
});
export default VariableSizeList;

9.2 src\react-window\createListComponent.js #

src\react-window\createListComponent.js

import React from 'react';
export default function createListComponent({
    getEstimatedTotalSize,//获取预计的总高度
    //......
}) {
    return class extends React.Component {
        render() {
            const { width, height, itemCount, children: ComponentType } = this.props;
            const containerStyle = { position: 'relative', width, height, overflow: 'auto', willChange: 'transform' };
+           const contentStyle = { height: getEstimatedTotalSize(this.props, this.instanceProps), width: '100%' };
            const items = [];
            if (itemCount > 0) {
                const [startIndex, stopIndex] = this._getRangeToRender();
                for (let index = startIndex; index <= stopIndex; index++) {
                    items.push(
                        <ComponentType key={index} index={index} style={this._getItemStyle(index)} />
                    );
                }
            }
            return (
                <div style={containerStyle} onScroll={this.onScroll}>
                    <div style={contentStyle}>
                        {items}
                    </div>
                </div>
            )
        }
        //......
    }
}

10. 动态计算高度 #

  • lastMeasuredIndex 上次测试过高度的最大索引

10.1 VariableSizeList.js #

src\react-window\VariableSizeList.js

import createListComponent from './createListComponent';
const DEFAULT_ESTIMATED_SIZE = 50;
+const getEstimatedTotalSize = ({ itemCount }, { estimatedItemSize, lastMeasuredIndex, itemMetadataMap }) => {
+   let totalSizeOfMeasuredItems = 0;//计算过的条目总大小
+   if (lastMeasuredIndex >= 0) {
+       const itemMetadata = itemMetadataMap[lastMeasuredIndex];
+       totalSizeOfMeasuredItems = itemMetadata.offset + itemMetadata.size;//测试过的总大小
+   }
+   const numUnmeasuredItems = itemCount - lastMeasuredIndex - 1;//未测量的条目
    const totalSizeOfUnmeasuredItems = numUnmeasuredItems * estimatedItemSize;//未测量条目的总高度
    return totalSizeOfMeasuredItems + totalSizeOfUnmeasuredItems;
}
+function findNearestItem(props, instanceProps, offset) {
+    const { lastMeasuredIndex } = instanceProps;
+    for (let index = 0; index <= lastMeasuredIndex; index++) {
+        const currentOffset = getItemMetadata(props, index, instanceProps).offset;
+        if (currentOffset >= offset) {
+            return index;
+        }
+    }
+    return 0;
+}
+function getItemMetadata(props, index, instanceProps) {
+    const { itemSize } = props;
+    const { itemMetadataMap, lastMeasuredIndex } = instanceProps;
+    if (index > lastMeasuredIndex) {
+        let offset = 0;//先计算上一个测试过的条目的下一个offset
+        if (lastMeasuredIndex >= 0) {
+            const itemMetadata = itemMetadataMap[lastMeasuredIndex];
+            offset = itemMetadata.offset + itemMetadata.size;
+        }
+        //计算从上一个条目到本次索引的offset和size
+        for (let i = lastMeasuredIndex + 1; i <= index; i++) {
+            let size = itemSize(i);
+            itemMetadataMap[i] = { offset, size };
+            offset += size;
+        }
+        instanceProps.lastMeasuredIndex = index;
+    }
+    return itemMetadataMap[index];
+}
const VariableSizeList = createListComponent({
    getEstimatedTotalSize,
+   getStartIndexForOffset: (props, offset, instanceProps) => findNearestItem(props, instanceProps, offset),
+   getStopIndexForStartIndex: (props, startIndex, scrollOffset, instanceProps) => {
+       const { itemCount, height } = props;
+       const itemMetadata = getItemMetadata(props, startIndex, instanceProps);
+       const maxOffset = scrollOffset + height;
+       let offset = itemMetadata.offset + itemMetadata.size;
+       let stopIndex = startIndex;
+       while (stopIndex < itemCount - 1 && offset < maxOffset) {
+           stopIndex++;
+           offset += getItemMetadata(props, stopIndex, instanceProps).size;
+       }
+       return stopIndex;
+   },
+   getItemSize: (props, index, instanceProps) => getItemMetadata(props, index, instanceProps).size,
+   getItemOffset: (props, index, instanceProps) => getItemMetadata(props, index, instanceProps).offset,
    initInstanceProps(props) {
        const { estimatedItemSize } = props;
        const instanceProps = {
            estimatedItemSize: estimatedItemSize || DEFAULT_ESTIMATED_SIZE,
+           itemMetadataMap: {},//存放每个条目的高度和偏移量
+           lastMeasuredIndex: -1//最后一个测量高度的索引
        }
        return instanceProps;
    }
});
export default VariableSizeList;

10.2 createListComponent.js #

src\react-window\createListComponent.js

import React from 'react';
export default function createListComponent({}) {
    return class extends React.Component {
        _getItemStyle = (index) => {
            const style = {
                position: 'absolute',
                width: '100%',
+               height: getItemSize(this.props, index, this.instanceProps),
+               top: getItemOffset(this.props, index, this.instanceProps)
            };
            return style;
        }
        _getRangeToRender = () => {
            const { scrollOffset } = this.state;
            const { itemCount, overscanCount } = this.props;
+           const startIndex = getStartIndexForOffset(this.props, scrollOffset, this.instanceProps);
+           const stopIndex = getStopIndexForStartIndex(this.props, startIndex, scrollOffset, this.instanceProps);
            return [
                Math.max(0, startIndex - overscanCount),
                Math.max(0, Math.min(itemCount - 1, stopIndex + overscanCount)),
                startIndex, stopIndex]
        }
    }
}

11. 优化方案 #

11.1 缓存样式 #

11.1.1 createListComponent.js #

src\react-window\createListComponent.js

    return class extends React.Component {
+       itemStyleCache = new Map()
        instanceProps = initInstanceProps&&initInstanceProps(this.props)
        _getItemStyle = (index) => {
+           let style;
+           if (this.itemStyleCache.has(index)) {
+               style = this.itemStyleCache.get(index);
+           } else {
                style = {
                    position: 'absolute',
                    width: '100%',
                    height: getItemSize(this.props, index, this.instanceProps),
                    top: getItemOffset(this.props, index, this.instanceProps)
                };
+               this.itemStyleCache.set(index, style);
+           }
            return style;
        }
    }
}

11.2 二分查找和指数扩充 #

11.2.1 VariableSizeList.js #

src\react-window\VariableSizeList.js

+function findNearestItem(props, instanceProps, offset) {
+  const { itemMetadataMap, lastMeasuredIndex } = instanceProps;
+  const lastMeasuredItemOffset =
+    lastMeasuredIndex > 0 ? itemMetadataMap[lastMeasuredIndex].offset : 0;
+  if (lastMeasuredItemOffset >= offset) {
+    return findNearestItemBinarySearch(props, instanceProps, lastMeasuredIndex, 0, offset);
+  } else {
+    return findNearestItemExponentialSearch(
+      props,
+      instanceProps,
+      Math.max(0, lastMeasuredIndex),
+      offset
+    );
+  }
+  //return findNearestItemBinarySearch(props, instanceProps, lastMeasuredIndex, 0, offset);
+  //在源码里此处用的是二分查找,把时间复杂度从N=>logN
+  /* for (let index = 0; index <= lastMeasuredIndex; index++) {
+    const currentOffset = getItemMetadata(props, index, instanceProps).offset;
+    //currentOffset=当前条目的offset offset=当前容器向上卷去的高度
+    if (currentOffset >= offset) {
+      return index;
+    }
+  }
+  return 0; */
+}
+function findNearestItemExponentialSearch(props, instanceProps, index, offset) {
+  const { itemCount } = props;
+  let interval = 1;
+  while (
+    index < itemCount &&
+    getItemMetadata(props, index, instanceProps).offset < offset
+  ) {
+    index += interval;
+    interval *= 2;
+  }
+  return findNearestItemBinarySearch(props, instanceProps, Math.min(index, itemCount - 1), Math.floor(index / 2), offset);
+}
+const findNearestItemBinarySearch = (
+  props,
+  instanceProps,
+  high,
+  low,
+  offset
+) => {
+  while (low <= high) {
+    const middle = low + Math.floor((high - low) / 2);
+    const currentOffset = getItemMetadata(props, middle, instanceProps).offset;
+    if (currentOffset === offset) {
+      return middle;
+    } else if (currentOffset < offset) {
+      low = middle + 1;
+    } else if (currentOffset > offset) {
+      high = middle - 1;
+    }
+  }
+  if (low > 0) {
+    return low - 1;
+  } else {
+    return 0;
+  }
+};

11.3 IntersectionObserver #

  • IntersectionObserver接口(从属于Intersection Observer API)为开发者提供了一种可以异步监听目标元素与其祖先或视窗(viewport)交叉状态的手段。祖先元素与视窗(viewport)被称为根(root)
  • 网页开发时,常常需要判断某个元素是否进入了视口(viewport,即用户能不能看到它,然后执行相应的逻辑
  • 常见的方法是监听scroll事件,调用元素的getBoundingClientRect方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于scroll事件密集发生,计算量很大,容易造成性能问题

11.3.1 createListComponent.js #

src\react-window\createListComponent.js


function createListComponent({
  getEstimatedTotalSize,
  getItemSize,
  getItemOffset,
  getStartIndexForOffset,//根据向上卷去的高度计算开始索引
  getStopIndexForStartIndex,//根据开始索引和容器的高度计算结束索引
  initInstanceProps
}) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.instanceProps = initInstanceProps && initInstanceProps(this.props)
      this.state = { scrollOffset: 0 }
+     this.outerRef = React.createRef();
+     this.oldFirstRef = React.createRef();
+     this.oldLastRef = React.createRef();
+     this.firstRef = React.createRef();
+     this.lastRef = React.createRef();
    }
    static defaultProps = {
      overscanCount: 2
    }
+   componentDidMount() {
+     this.observe(this.oldFirstRef.current = this.firstRef.current);
+     this.observe(this.oldLastRef.current = this.lastRef.current);
+   }
+   componentDidUpdate() {
+     if (this.oldFirstRef.current !== this.firstRef.current) {
+       this.oldFirstRef.current = this.firstRef.current;
+       this.observe(this.firstRef.current);
+     }
+     if (this.oldLastRef.current !== this.lastRef.current) {
+       this.oldLastRef.current = this.lastRef.current;
+       this.observe(this.lastRef.current);
+     }
+   }
+   observe = (dom) => {
+     let io = new IntersectionObserver((entries) => {
+       entries.forEach(this.onScroll);
+     }, { root: this.outerRef.current })
+     io.observe(dom);
+   }
    render() {
      const { width, height, children: Row } = this.props;
      const containerStyle = { position: 'relative', width, height, overflow: 'auto', willChange: 'transform' };
      const contentStyle = { width: '100%', height: getEstimatedTotalSize(this.props, this.instanceProps) };
      const items = [];
+     const [startIndex, stopIndex, originStartIndex, originStopIndex] = this.getRangeToRender();
      for (let index = startIndex; index <= stopIndex; index++) {
+       const style = this.getItemStyle(index);
+       if (index === originStartIndex) {
+         items.push(
+           <Row key={index} index={index} style={style} forwardRef={this.firstRef} />
+         );
+         continue;
+       } else if (index === originStopIndex) {
+         items.push(
+           <Row key={index} index={index} style={style} forwardRef={this.lastRef} />
+         );
+         continue;
+       }
        items.push(
          <Row key={index} index={index} style={style} />
        );
      }
      return (
+      <div style={containerStyle} ref={this.outerRef} >
          <div style={contentStyle} >
            {items}
          </div>
        </div>
      )
    }
    onScroll = () => {
      const { scrollTop } = this.outerRef.current;
      this.setState({ scrollOffset: scrollTop })
    }
    getRangeToRender = () => {
      const { scrollOffset } = this.state;
      const { itemCount, overscanCount } = this.props;
      const startIndex = getStartIndexForOffset(this.props, scrollOffset, this.instanceProps);
      const stopIndex = getStopIndexForStartIndex(this.props, startIndex, scrollOffset, this.instanceProps);
      return [
        Math.max(0, startIndex - overscanCount),
        Math.min(itemCount - 1, stopIndex + overscanCount),
        startIndex, stopIndex];
    }
    getItemStyle = (index) => {
      const style = {
        position: 'absolute',
        width: '100%',
        height: getItemSize(this.props, index, this.instanceProps),
        top: getItemOffset(this.props, index, this.instanceProps)
      }
      return style;
    }
  }
}

13. 动态高度列表 #

  • react-window
  • dynamic-size
  • ResizeObserver
  • react-virtual
  • virtuoso

13.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import FixedSizeList from './fixed-size-list';
import VariableSizeList from './variable-size-list';
+import DynamicSizeList from './dynamic-size-list'
const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(<DynamicSizeList />);

13.2 dynamic-size-list.js #

src\dynamic-size-list.js

import React from 'react';
import { VariableSizeList } from './react-window';

const items = [];
for (let i = 0; i < 1000; i++) {
    const height = (30 + Math.floor(Math.random() * 20)) + 'px';
    const style = {
        height,
        width: `100%`,
        backgroundColor: i % 2 ? 'green' : "orange",
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center'
    }
    items.push(<div style={style}>Row {i}</div>);
}

const Row = ({ index }) => items[index]
const App = () => {
    return (
        <VariableSizeList
            isDynamic={true}
            className='List'
            height={200}
            width={200}
            itemCount={1000}
        >
            {Row}
        </VariableSizeList>
    )
}
export default App

13.4 VariableSizeList.js #

src\react-window\VariableSizeList.js

function getItemMetadata(props, index, instanceProps) {
    const { itemSize } = props;
    const { itemMetadataMap, lastMeasuredIndex } = instanceProps;
    if (index > lastMeasuredIndex) {
        let offset = 0;//先计算上一个测试过的条目的下一个offset
        if (lastMeasuredIndex >= 0) {
            const itemMetadata = itemMetadataMap[lastMeasuredIndex];
            offset = itemMetadata.offset + itemMetadata.size;
        }
        //计算从上一个条目到本次索引的offset和size
        for (let i = lastMeasuredIndex + 1; i <= index; i++) {
+           let size = itemSize ? itemSize(i) : DEFAULT_ESTIMATED_SIZE;
            itemMetadataMap[i] = { offset, size };
            offset += size;
        }
        instanceProps.lastMeasuredIndex = index;
    }
    return itemMetadataMap[index];
}

13.5 createListComponent.js #

src\react-window\createListComponent.js

import React from 'react';

+class ListItem extends React.Component {
+    constructor(props) {
+        super(props);
+        this.domRef = React.createRef();
+        this.resizeObserver = null;
+    }
+    componentDidMount() {
+        if (this.domRef.current) {
+            const node = this.domRef.current.firstChild;
+            const { index, onSizeChange } = this.props;
+            this.resizeObserver = new ResizeObserver(() => {
+                onSizeChange(index, node);
+            });
+            this.resizeObserver.observe(node);
+        }
+    }
+    componentWillUnmount() {
+        if (this.resizeObserver && this.domRef.current.firstChild) {
+            this.resizeObserver.unobserve(this.domRef.current.firstChild);
+        }
+    }
+    render() {
+        const { index, style, ComponentType } = this.props;
+        return (
+            <div style={style} ref={this.domRef}>
+                <ComponentType index={index} />
+            </div>
+        )
+    }
+}
export default function createListComponent({
    getEstimatedTotalSize,//获取预计的总高度
    getItemSize,//每个条目的高度
    getItemOffset,//获取每个条目的偏移量
    getStartIndexForOffset,
    getStopIndexForStartIndex,
    initInstanceProps
}) {
    return class extends React.Component {
        itemStyleCache = new Map()
        instanceProps = initInstanceProps&&initInstanceProps(this.props)
        static defaultProps = {
            overscanCount: 2
        }
        state = { scrollOffset: 0 }
+       onSizeChange = (index, node) => {
+           const height = node.offsetHeight;
+           const { itemMetadataMap, lastMeasuredIndex } = this.instanceProps;
+           const itemMetadata = itemMetadataMap[index];
+           itemMetadata.size = height;
+           let offset = 0;
+           for (let i = 0; i <= lastMeasuredIndex; i++) {
+               const itemMetadata = itemMetadataMap[i];
+               itemMetadata.offset = offset;
+               offset = offset + itemMetadata.size;
+           }
+           this.itemStyleCache.clear();
+           this.forceUpdate();
+       }
        render() {
+           const { width, height, itemCount, children: ComponentType, isDynamic } = this.props;
            const containerStyle = { position: 'relative', width, height, overflow: 'auto', willChange: 'transform' };
            const contentStyle = { height: getEstimatedTotalSize(this.props, this.instanceProps), width: '100%' };
            const items = [];
            if (itemCount > 0) {
                const [startIndex, stopIndex] = this._getRangeToRender();
                for (let index = startIndex; index <= stopIndex; index++) {
+                   if (isDynamic) {
+                       items.push(
+                           <ListItem
+                               key={index} index={index}
+                               style={this._getItemStyle(index)}
+                               ComponentType={ComponentType}
+                               onSizeChange={this.onSizeChange}
+                           />
+                       );
+                   } else {
+                       items.push(
+                           <ComponentType
+                               key={index} index={index}
+                               style={this._getItemStyle(index)}
+                           />
+                       );
+                   }
               }
            }
            return (
                <div style={containerStyle} onScroll={this.onScroll}>
                    <div style={contentStyle}>
                        {items}
                    </div>
                </div>
            )
        }
        onScroll = event => {
            const { scrollTop } = event.currentTarget;
            this.setState({ scrollOffset: scrollTop });
        }
        _getItemStyle = (index) => {
            let style;
            if (this.itemStyleCache.has(index)) {
                style = this.itemStyleCache.get(index);
            } else {
                style = {
                    position: 'absolute',
                    width: '100%',
                    height: getItemSize(this.props, index, this.instanceProps),
                    top: getItemOffset(this.props, index, this.instanceProps)
                };
                this.itemStyleCache.set(index, style);
            }
            return style;
        }
        _getRangeToRender = () => {
            const { scrollOffset } = this.state;
            const { itemCount, overscanCount } = this.props;
            const startIndex = getStartIndexForOffset(this.props, scrollOffset, this.instanceProps);
            const stopIndex = getStopIndexForStartIndex(this.props, startIndex, scrollOffset, this.instanceProps);
            return [
                Math.max(0, startIndex - overscanCount),
                Math.max(0, Math.min(itemCount - 1, stopIndex + overscanCount)),
                startIndex, stopIndex]
        }
    }
}

14. 滚动状态 #

14.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import FixedSizeList from './fixed-size-list';
import VariableSizeList from './variable-size-list';
import DynamicSizeList from './dynamic-size-list'
const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(<FixedSizeList />);

14.2 fixed-size-list.js #

src\fixed-size-list.js

import { FixedSizeList } from './react-window';
import './fixed-size-list.css';
+const Row = ({ index, style, isScrolling }) => (
+    <div className={index % 2 ? 'ListItemOdd' : 'ListItemEven'} style={style}>
+        {isScrolling ? 'Scrolling' : `Row ${index}`}
+    </div>
+)

function App() {
    return (
        <FixedSizeList
            className='List'
            height={200}
            width={200}
            itemSize={50}
            itemCount={1000}
+           useIsScrolling
        >
            {Row}
        </FixedSizeList>
    )
}
export default App;

14.3 src\react-window\createListComponent.js #

src\react-window\createListComponent.js

import React from 'react';
+import { requestTimeout, cancelTimeout } from './timer';
+const IS_SCROLLING_DEBOUNCE_INTERVAL = 150;
class ListItem extends React.Component {
    constructor(props) {
        super(props);
        this.domRef = React.createRef();
        this.resizeObserver = null;
    }
    componentDidMount() {
        if (this.domRef.current) {
            const node = this.domRef.current.firstChild;
            const { index, onSizeChange } = this.props;
            this.resizeObserver = new ResizeObserver(() => {
                onSizeChange(index, node);
            });
            this.resizeObserver.observe(node);
        }
    }
    componentWillUnmount() {
        if (this.resizeObserver && this.domRef.current.firstChild) {
            this.resizeObserver.unobserve(this.domRef.current.firstChild);
        }
    }
    render() {
        const { index, style, ComponentType } = this.props;
        return (
            <div style={style} ref={this.domRef}>
                <ComponentType index={index} />
            </div>
        )
    }
}
export default function createListComponent({
    getEstimatedTotalSize,//获取预计的总高度
    getItemSize,//每个条目的高度
    getItemOffset,//获取每个条目的偏移量
    getStartIndexForOffset,
    getStopIndexForStartIndex,
    initInstanceProps
}) {
    return class extends React.Component {
        itemStyleCache = new Map()
        instanceProps = initInstanceProps && initInstanceProps(this.props)
        static defaultProps = {
            overscanCount: 2,
+           useIsScrolling: false
        }
+       state = { scrollOffset: 0, isScrolling: false }
        onSizeChange = (index, node) => {
            const height = node.offsetHeight;
            const { itemMetadataMap, lastMeasuredIndex } = this.instanceProps;
            const itemMetadata = itemMetadataMap[index];
            itemMetadata.size = height;
            let offset = 0;
            for (let i = 0; i <= lastMeasuredIndex; i++) {
                const itemMetadata = itemMetadataMap[i];
                itemMetadata.offset = offset;
                offset = offset + itemMetadata.size;
            }
            this.itemStyleCache.clear();
            this.forceUpdate();
        }
        render() {
+           const { width, height, itemCount, children: ComponentType, isDynamic, useIsScrolling } = this.props;
+           const { isScrolling } = this.state;
            const containerStyle = { position: 'relative', width, height, overflow: 'auto', willChange: 'transform' };
            const contentStyle = { height: getEstimatedTotalSize(this.props, this.instanceProps), width: '100%' };
            const items = [];
            if (itemCount > 0) {
                const [startIndex, stopIndex] = this._getRangeToRender();
                for (let index = startIndex; index <= stopIndex; index++) {
                    if (isDynamic) {
                        items.push(
                            <ListItem
                                key={index} index={index}
                                style={this._getItemStyle(index)}
                                ComponentType={ComponentType}
                                onSizeChange={this.onSizeChange}
+                               isScrolling={useIsScrolling && isScrolling}
                            />
                        );
                    } else {
                        items.push(
                            <ComponentType
                                key={index} index={index}
                                style={this._getItemStyle(index)}
+                               isScrolling={useIsScrolling && isScrolling}
                            />
                        );
                    }
                }
            }
            return (
                <div style={containerStyle} onScroll={this.onScroll}>
                    <div style={contentStyle}>
                        {items}
                    </div>
                </div>
            )
        }
        onScroll = event => {
            const { scrollTop } = event.currentTarget;
+           this.setState({ scrollOffset: scrollTop, isScrolling: true }, this._resetIsScrollingDebounced);
        }
+       _resetIsScrollingDebounced = () => {
+           if (this._resetIsScrollingTimeoutId) {
+               cancelTimeout(this._resetIsScrollingTimeoutId);
+           }
+           this._resetIsScrollingTimeoutId = requestTimeout(
+               this._resetIsScrolling,
+               IS_SCROLLING_DEBOUNCE_INTERVAL
+           );
+       };
+       _resetIsScrolling = () => {
+           this._resetIsScrollingTimeoutId = null;
+           this.setState({ isScrolling: false });
+       }
        _getItemStyle = (index) => {
            let style;
            if (this.itemStyleCache.has(index)) {
                style = this.itemStyleCache.get(index);
            } else {
                style = {
                    position: 'absolute',
                    width: '100%',
                    height: getItemSize(this.props, index, this.instanceProps),
                    top: getItemOffset(this.props, index, this.instanceProps)
                };
                this.itemStyleCache.set(index, style);
            }
            return style;
        }
        _getRangeToRender = () => {
            const { scrollOffset } = this.state;
            const { itemCount, overscanCount } = this.props;
            const startIndex = getStartIndexForOffset(this.props, scrollOffset, this.instanceProps);
            const stopIndex = getStopIndexForStartIndex(this.props, startIndex, scrollOffset, this.instanceProps);
            return [
                Math.max(0, startIndex - overscanCount),
                Math.max(0, Math.min(itemCount - 1, stopIndex + overscanCount)),
                startIndex, stopIndex]
        }
    }
}

15. 滚动到指定条目 #

15.1 src\fixed-size-list.js #

src\fixed-size-list.js

import React from 'react';
import { FixedSizeList } from './react-window';
import './fixed-size-list.css';
const Row = ({ index, style, isScrolling }) => (
    <div className={index % 2 ? 'ListItemOdd' : 'ListItemEven'} style={style}>
        {isScrolling ? 'Scrolling' : `Row ${index}`}
    </div>
)

function App() {
+   const listRef = React.useRef();
    return (
+       <>
+           <button onClick={() => listRef.current.scrollToItem(50)}>滚动到50</button>
            <FixedSizeList
                className='List'
                height={200}
                width={200}
                itemSize={50}
                itemCount={1000}
                useIsScrolling
+               ref={listRef}
            >
                {Row}
            </FixedSizeList>
+       </>
    )
}
export default App;

15.2 FixedSizeList.js #

src\react-window\FixedSizeList.js

import createListComponent from './createListComponent';
const FixedSizeList = createListComponent({
    getItemSize: ({ itemSize }) => itemSize,//每个条目的高度
    getEstimatedTotalSize: ({ itemSize, itemCount }) => itemSize * itemCount, //获取预计的总高度
    getItemOffset: ({ itemSize }, index) => itemSize * index, //获取每个条目的偏移量
    getStartIndexForOffset: ({ itemSize }, offset) => Math.floor(offset / itemSize),//获取起始索引
    getStopIndexForStartIndex: ({ height, itemSize }, startIndex) => {//获取结束索引
        const numVisibleItems = Math.ceil(height / itemSize);
        return startIndex + numVisibleItems - 1;
    },
+   getOffsetForIndex: (props, index) => {
+       const { itemSize } = props;
+       return itemSize * index;
+   }
});
export default FixedSizeList;

15.3 src\react-window\createListComponent.js #

src\react-window\createListComponent.js

import React from 'react';
import { requestTimeout, cancelTimeout } from './timer';
const IS_SCROLLING_DEBOUNCE_INTERVAL = 150;
class ListItem extends React.Component {
    constructor(props) {
        super(props);
        this.domRef = React.createRef();
        this.resizeObserver = null;
    }
    componentDidMount() {
        if (this.domRef.current) {
            const node = this.domRef.current.firstChild;
            const { index, onSizeChange } = this.props;
            this.resizeObserver = new ResizeObserver(() => {
                onSizeChange(index, node);
            });
            this.resizeObserver.observe(node);
        }
    }
    componentWillUnmount() {
        if (this.resizeObserver && this.domRef.current.firstChild) {
            this.resizeObserver.unobserve(this.domRef.current.firstChild);
        }
    }
    render() {
        const { index, style, ComponentType } = this.props;
        return (
            <div style={style} ref={this.domRef}>
                <ComponentType index={index} />
            </div>
        )
    }
}
export default function createListComponent({
    getEstimatedTotalSize,//获取预计的总高度
    getItemSize,//每个条目的高度
    getItemOffset,//获取每个条目的偏移量
    getStartIndexForOffset,
    getStopIndexForStartIndex,
    initInstanceProps,
+   getOffsetForIndex
}) {
    return class extends React.Component {
+       outerRef = React.createRef();
        itemStyleCache = new Map()
        instanceProps = initInstanceProps && initInstanceProps(this.props)
        static defaultProps = {
            overscanCount: 2,
            useIsScrolling: false
        }
+       scrollTo = (scrollOffset) => {
+           this.setState({ scrollOffset: Math.max(0, scrollOffset) });
+       }
+       scrollToItem = (index) => {
+           const { itemCount } = this.props;
+           index = Math.max(0, Math.min(index, itemCount - 1))
+           this.scrollTo(
+               getOffsetForIndex(this.props, index)
+           )
+       }
+       componentDidUpdate() {
+           const { scrollOffset } = this.state;
+           this.outerRef.current.scrollTop = scrollOffset;
+       }
        state = { scrollOffset: 0, isScrolling: false }
        onSizeChange = (index, node) => {
            const height = node.offsetHeight;
            const { itemMetadataMap, lastMeasuredIndex } = this.instanceProps;
            const itemMetadata = itemMetadataMap[index];
            itemMetadata.size = height;
            let offset = 0;
            for (let i = 0; i <= lastMeasuredIndex; i++) {
                const itemMetadata = itemMetadataMap[i];
                itemMetadata.offset = offset;
                offset = offset + itemMetadata.size;
            }
            this.itemStyleCache.clear();
            this.forceUpdate();
        }
        render() {
            const { width, height, itemCount, children: ComponentType, isDynamic, useIsScrolling } = this.props;
            const { isScrolling } = this.state;
            const containerStyle = { position: 'relative', width, height, overflow: 'auto', willChange: 'transform' };
            const contentStyle = { height: getEstimatedTotalSize(this.props, this.instanceProps), width: '100%' };
            const items = [];
            if (itemCount > 0) {
                const [startIndex, stopIndex] = this._getRangeToRender();
                for (let index = startIndex; index <= stopIndex; index++) {
                    if (isDynamic) {
                        items.push(
                            <ListItem
                                key={index} index={index}
                                style={this._getItemStyle(index)}
                                ComponentType={ComponentType}
                                onSizeChange={this.onSizeChange}
                                isScrolling={useIsScrolling && isScrolling}
                            />
                        );
                    } else {
                        items.push(
                            <ComponentType
                                key={index} index={index}
                                style={this._getItemStyle(index)}
                                isScrolling={useIsScrolling && isScrolling}
                            />
                        );
                    }
                }
            }
            return (
+               <div style={containerStyle} onScroll={this.onScroll} ref={this.outerRef}>
                    <div style={contentStyle}>
                        {items}
                    </div>
                </div>
            )
        }
        onScroll = event => {
            const { scrollTop } = event.currentTarget;
            this.setState({ scrollOffset: scrollTop, isScrolling: true }, this._resetIsScrollingDebounced);
        }
        _resetIsScrollingDebounced = () => {
            if (this._resetIsScrollingTimeoutId) {
                cancelTimeout(this._resetIsScrollingTimeoutId);
            }
            this._resetIsScrollingTimeoutId = requestTimeout(
                this._resetIsScrolling,
                IS_SCROLLING_DEBOUNCE_INTERVAL
            );
        };
        _resetIsScrolling = () => {
            this._resetIsScrollingTimeoutId = null;
            this.setState({ isScrolling: false });
        }
        _getItemStyle = (index) => {
            let style;
            if (this.itemStyleCache.has(index)) {
                style = this.itemStyleCache.get(index);
            } else {
                style = {
                    position: 'absolute',
                    width: '100%',
                    height: getItemSize(this.props, index, this.instanceProps),
                    top: getItemOffset(this.props, index, this.instanceProps)
                };
                this.itemStyleCache.set(index, style);
            }
            return style;
        }
        _getRangeToRender = () => {
            const { scrollOffset } = this.state;
            const { itemCount, overscanCount } = this.props;
            const startIndex = getStartIndexForOffset(this.props, scrollOffset, this.instanceProps);
            const stopIndex = getStopIndexForStartIndex(this.props, startIndex, scrollOffset, this.instanceProps);
            return [
                Math.max(0, startIndex - overscanCount),
                Math.max(0, Math.min(itemCount - 1, stopIndex + overscanCount)),
                startIndex, stopIndex]
        }
    }
}

16.其它方案 #

  • react-virtual
  • virtuoso

16.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import FixedSizeList from './fixed-size-list';
import VariableSizeList from './variable-size-list';
import DynamicSizeList from './dynamic-size-list'
+import Virtuoso from './Virtuoso'
const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(<DynamicSizeList />);

16.2 src\Virtuoso.js #

src\Virtuoso.js

import React from 'react'
import { Virtuoso } from 'react-virtuoso'
const items = [];
for (let i = 0; i < 200; i++) {
  const height = (30+Math.random() * 20) + 'px';
  const style = {
    height,
    width: `100%`,
    backgroundColor:i%2?'green':"orange"
  }
  items.push(<div style={ style }>Row {i}</div>);
}
const App = () => (
    <Virtuoso
        style={{ height: '200px',width:'200px' }}
        totalCount={200}
        itemContent={index => items[index]}
    />
)
export default App;

16.3 ResizeObserver #

    • ResizeObserver
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ResizeObserver</title>
</head>

<body>
    <img id="logo" />
    <script>
        let logo = document.getElementById('logo');
        console.log(logo.offsetHeight);
        const resizeObserver = new ResizeObserver(entries => {
            console.log(logo.offsetHeight);
        });
        resizeObserver.observe(logo);
        setTimeout(()=>{
            logo.src = 'https://img.zhufengpeixun.com/zfjglogo.png';
        },1000);
    </script>
</body>
</html>

访问验证

请输入访问令牌

Token不正确,请重新输入