├── .env.production ├── src ├── react-app-env.d.ts ├── setupTests.ts ├── utils │ ├── data-generator.ts │ ├── binary-search.ts │ └── BinaryIndexedTree.ts ├── App.test.tsx ├── components │ ├── Counter.tsx │ ├── NormalList.tsx │ ├── FixedHeight.tsx │ ├── PropHeight.tsx │ ├── OptimizedReactiveHeight.tsx │ └── ReactiveHeight.tsx ├── index.css ├── index.tsx ├── App.css ├── hooks │ ├── useFixedHeightVirtualList.ts │ ├── useBinaryIndexedTreeVirtualList.ts │ ├── useReactiveHeightVirtualList.ts │ └── usePropHeightVirtualList.ts ├── logo.svg ├── App.tsx └── serviceWorker.ts ├── docs ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── static │ ├── css │ │ ├── main.5256252c.chunk.css │ │ └── main.5256252c.chunk.css.map │ └── js │ │ ├── 2.b83cbb9c.chunk.js.LICENSE.txt │ │ ├── runtime-main.48274765.js │ │ ├── runtime-main.48274765.js.map │ │ ├── main.91ca23b2.chunk.js │ │ └── main.91ca23b2.chunk.js.map ├── manifest.json ├── 404.html ├── precache-manifest.2df06899de105cf2dfd58f237364468d.js ├── service-worker.js ├── asset-manifest.json └── index.html ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json ├── 404.html └── index.html ├── README.md ├── .gitignore ├── tsconfig.json ├── package.json └── Process.md /.env.production: -------------------------------------------------------------------------------- 1 | PUBLIC_URL=/virtual-list-demo-react 2 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /docs/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChuChencheng/virtual-list-demo-react/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /docs/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChuChencheng/virtual-list-demo-react/HEAD/docs/logo192.png -------------------------------------------------------------------------------- /docs/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChuChencheng/virtual-list-demo-react/HEAD/docs/logo512.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChuChencheng/virtual-list-demo-react/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChuChencheng/virtual-list-demo-react/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChuChencheng/virtual-list-demo-react/HEAD/public/logo512.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # virtual-list-demo-react 2 | 3 | [在线 demo](https://chuchencheng.com/virtual-list-demo-react/) 4 | 5 | # 感谢 6 | 7 | 感谢 [francecil](https://github.com/francecil) 的算法优化方案 8 | 9 | [Vue 版本 demo](https://github.com/francecil/virtual-list-demo) 10 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/utils/data-generator.ts: -------------------------------------------------------------------------------- 1 | export default function dataGen (amount = 200000) { 2 | const data = [] 3 | for (let i = 0; i < amount; i++) { 4 | data.push({ 5 | id: Math.random().toString(36).substr(2), 6 | index: i, 7 | value: i + 1, 8 | }) 9 | } 10 | return data 11 | } 12 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | export default function Counter () { 4 | const [count, setCount] = useState(0) 5 | 6 | return ( 7 | 8 | Count: {count} 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /docs/static/css/main.5256252c.chunk.css: -------------------------------------------------------------------------------- 1 | body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.container{margin:0 auto;width:500px;height:500px;position:relative;overflow-y:auto}.total-list,.visible-list{position:absolute;top:0;width:100%} 2 | /*# sourceMappingURL=main.5256252c.chunk.css.map */ -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | .container { 11 | margin: 0 auto; 12 | width: 500px; 13 | height: 500px; 14 | position: relative; 15 | overflow-y: auto; 16 | } 17 | 18 | .total-list, .visible-list { 19 | position: absolute; 20 | top: 0; 21 | width: 100%; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/NormalList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface IProps { 4 | data: T[]; 5 | itemHeight: number; 6 | itemRender: (item: T) => JSX.Element 7 | } 8 | 9 | function NormalList ({ data, itemHeight, itemRender }: IProps) { 10 | 11 | return ( 12 |
13 |
14 | {data.map((d) => ( 15 |
{itemRender(d)}
16 | ))} 17 |
18 |
19 | ) 20 | } 21 | 22 | export default NormalList 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /docs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | import { BrowserRouter } from 'react-router-dom'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | 17 | // If you want your app to work offline and load faster, you can change 18 | // unregister() to register() below. Note this comes with some pitfalls. 19 | // Learn more about service workers: https://bit.ly/CRA-PWA 20 | serviceWorker.unregister(); 21 | -------------------------------------------------------------------------------- /src/utils/binary-search.ts: -------------------------------------------------------------------------------- 1 | // 二分查找修改,找到最接近且大于等于 target 的索引 2 | 3 | export default function binarySearch (list: number[], target: number): number { 4 | const length = list.length 5 | if (!length) return -1 6 | let result = -1 7 | let start = 0 8 | let end = length - 1 9 | while (start <= end) { 10 | if (start === end) return list[start] >= target ? start : -1 11 | const mid = (start + end) >> 1 12 | const midValue = list[mid] 13 | if (midValue === target) return mid 14 | if (target < midValue) { 15 | if (result === -1 || list[result] > midValue) { 16 | result = mid 17 | } 18 | end-- 19 | } else { 20 | start = mid + 1 21 | } 22 | } 23 | return result 24 | } 25 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/static/css/main.5256252c.chunk.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["index.css"],"names":[],"mappings":"AAAA,KACE,QAAS,CACT,mJAEY,CACZ,kCAAmC,CACnC,iCACF,CAEA,WACE,aAAc,CACd,WAAY,CACZ,YAAa,CACb,iBAAkB,CAClB,eACF,CAEA,0BACE,iBAAkB,CAClB,KAAM,CACN,UACF","file":"main.5256252c.chunk.css","sourcesContent":["body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n.container {\n margin: 0 auto;\n width: 500px;\n height: 500px;\n position: relative;\n overflow-y: auto;\n}\n\n.total-list, .visible-list {\n position: absolute;\n top: 0;\n width: 100%;\n}\n"]} -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/hooks/useFixedHeightVirtualList.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | interface IParams { 4 | data: T[] 5 | itemHeight: number 6 | scrollTop: number 7 | clientHeight: number 8 | } 9 | 10 | export default function useFixedHeightVirtualList ({ 11 | data, 12 | itemHeight, 13 | scrollTop, 14 | clientHeight, 15 | }: IParams) { 16 | const totalHeight = useMemo(() => data.length * itemHeight, [data.length, itemHeight]) 17 | const startIndex = Math.floor(scrollTop / itemHeight) 18 | const endIndex = Math.ceil(clientHeight / itemHeight) + startIndex + 1 19 | const visibleData = useMemo(() => data.slice(startIndex, endIndex), [data, endIndex, startIndex]) 20 | // translateY 21 | const offset = useMemo(() => startIndex * itemHeight, [itemHeight, startIndex]) 22 | 23 | return { 24 | totalHeight, 25 | visibleData, 26 | offset, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/precache-manifest.2df06899de105cf2dfd58f237364468d.js: -------------------------------------------------------------------------------- 1 | self.__precacheManifest = (self.__precacheManifest || []).concat([ 2 | { 3 | "revision": "da8554acafa23987e00f7465809a54e9", 4 | "url": "/virtual-list-demo-react/index.html" 5 | }, 6 | { 7 | "revision": "2ed693ccf8b430a4e85a", 8 | "url": "/virtual-list-demo-react/static/css/main.5256252c.chunk.css" 9 | }, 10 | { 11 | "revision": "b9e30bd76f7737ca7c2e", 12 | "url": "/virtual-list-demo-react/static/js/2.b83cbb9c.chunk.js" 13 | }, 14 | { 15 | "revision": "c64c486544348f10a6d6c716950bc223", 16 | "url": "/virtual-list-demo-react/static/js/2.b83cbb9c.chunk.js.LICENSE.txt" 17 | }, 18 | { 19 | "revision": "2ed693ccf8b430a4e85a", 20 | "url": "/virtual-list-demo-react/static/js/main.91ca23b2.chunk.js" 21 | }, 22 | { 23 | "revision": "58e6818ec23445c28810", 24 | "url": "/virtual-list-demo-react/static/js/runtime-main.48274765.js" 25 | } 26 | ]); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virtual-list-demo-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@types/jest": "^24.0.0", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^16.9.0", 12 | "@types/react-dom": "^16.9.0", 13 | "@types/react-router-dom": "^5.1.5", 14 | "react": "^16.13.1", 15 | "react-dom": "^16.13.1", 16 | "react-router": "^5.2.0", 17 | "react-router-dom": "^5.2.0", 18 | "react-scripts": "3.4.1", 19 | "typescript": "~3.7.2" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build && rm -rf docs && mv build docs", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/static/js/2.b83cbb9c.chunk.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /** @license React v0.19.1 8 | * scheduler.production.min.js 9 | * 10 | * Copyright (c) Facebook, Inc. and its affiliates. 11 | * 12 | * This source code is licensed under the MIT license found in the 13 | * LICENSE file in the root directory of this source tree. 14 | */ 15 | 16 | /** @license React v16.13.1 17 | * react-dom.production.min.js 18 | * 19 | * Copyright (c) Facebook, Inc. and its affiliates. 20 | * 21 | * This source code is licensed under the MIT license found in the 22 | * LICENSE file in the root directory of this source tree. 23 | */ 24 | 25 | /** @license React v16.13.1 26 | * react-is.production.min.js 27 | * 28 | * Copyright (c) Facebook, Inc. and its affiliates. 29 | * 30 | * This source code is licensed under the MIT license found in the 31 | * LICENSE file in the root directory of this source tree. 32 | */ 33 | 34 | /** @license React v16.13.1 35 | * react.production.min.js 36 | * 37 | * Copyright (c) Facebook, Inc. and its affiliates. 38 | * 39 | * This source code is licensed under the MIT license found in the 40 | * LICENSE file in the root directory of this source tree. 41 | */ 42 | -------------------------------------------------------------------------------- /docs/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your Workbox-powered service worker! 3 | * 4 | * You'll need to register this file in your web app and you should 5 | * disable HTTP caching for this file too. 6 | * See https://goo.gl/nhQhGp 7 | * 8 | * The rest of the code is auto-generated. Please don't update this file 9 | * directly; instead, make changes to your Workbox build configuration 10 | * and re-run your build process. 11 | * See https://goo.gl/2aRDsh 12 | */ 13 | 14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); 15 | 16 | importScripts( 17 | "/virtual-list-demo-react/precache-manifest.2df06899de105cf2dfd58f237364468d.js" 18 | ); 19 | 20 | self.addEventListener('message', (event) => { 21 | if (event.data && event.data.type === 'SKIP_WAITING') { 22 | self.skipWaiting(); 23 | } 24 | }); 25 | 26 | workbox.core.clientsClaim(); 27 | 28 | /** 29 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 30 | * requests for URLs in the manifest. 31 | * See https://goo.gl/S9QRab 32 | */ 33 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 34 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 35 | 36 | workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/virtual-list-demo-react/index.html"), { 37 | 38 | blacklist: [/^\/_/,/\/[^/?]+\.[^/]+$/], 39 | }); 40 | -------------------------------------------------------------------------------- /docs/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/virtual-list-demo-react/static/css/main.5256252c.chunk.css", 4 | "main.js": "/virtual-list-demo-react/static/js/main.91ca23b2.chunk.js", 5 | "main.js.map": "/virtual-list-demo-react/static/js/main.91ca23b2.chunk.js.map", 6 | "runtime-main.js": "/virtual-list-demo-react/static/js/runtime-main.48274765.js", 7 | "runtime-main.js.map": "/virtual-list-demo-react/static/js/runtime-main.48274765.js.map", 8 | "static/js/2.b83cbb9c.chunk.js": "/virtual-list-demo-react/static/js/2.b83cbb9c.chunk.js", 9 | "static/js/2.b83cbb9c.chunk.js.map": "/virtual-list-demo-react/static/js/2.b83cbb9c.chunk.js.map", 10 | "index.html": "/virtual-list-demo-react/index.html", 11 | "precache-manifest.2df06899de105cf2dfd58f237364468d.js": "/virtual-list-demo-react/precache-manifest.2df06899de105cf2dfd58f237364468d.js", 12 | "service-worker.js": "/virtual-list-demo-react/service-worker.js", 13 | "static/css/main.5256252c.chunk.css.map": "/virtual-list-demo-react/static/css/main.5256252c.chunk.css.map", 14 | "static/js/2.b83cbb9c.chunk.js.LICENSE.txt": "/virtual-list-demo-react/static/js/2.b83cbb9c.chunk.js.LICENSE.txt" 15 | }, 16 | "entrypoints": [ 17 | "static/js/runtime-main.48274765.js", 18 | "static/js/2.b83cbb9c.chunk.js", 19 | "static/css/main.5256252c.chunk.css", 20 | "static/js/main.91ca23b2.chunk.js" 21 | ] 22 | } -------------------------------------------------------------------------------- /docs/static/js/runtime-main.48274765.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(t){for(var n,l,i=t[0],a=t[1],c=t[2],p=0,s=[];p { 5 | data: T[]; 6 | itemHeight: number; 7 | itemRender: (item: T) => JSX.Element 8 | } 9 | 10 | function FixedHeight ({ data, itemHeight, itemRender }: IProps) { 11 | const [scrollTop, setScrollTop] = useState(0) 12 | const [clientHeight, setClientHeight] = useState(0) 13 | 14 | const { totalHeight, visibleData, offset } = useFixedHeightVirtualList({ 15 | data, 16 | itemHeight, 17 | scrollTop, 18 | clientHeight, 19 | }) 20 | 21 | const containerRef = useRef(null) 22 | 23 | const handleScroll = useCallback(() => { 24 | if (containerRef.current) { 25 | setScrollTop(containerRef.current.scrollTop) 26 | } 27 | }, []) 28 | 29 | const containerRefCallback = useCallback((node: HTMLDivElement) => { 30 | if (node) { 31 | containerRef.current = node 32 | setClientHeight(node.clientHeight) 33 | } else { 34 | containerRef.current = null 35 | } 36 | }, []) 37 | 38 | return ( 39 |
44 |
48 |
52 | {visibleData.map((data) => ( 53 |
{itemRender(data)}
54 | ))} 55 |
56 |
57 | ) 58 | } 59 | 60 | export default FixedHeight 61 | -------------------------------------------------------------------------------- /src/components/PropHeight.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useRef, useState } from 'react' 2 | import usePropHeightVirtualList from '../hooks/usePropHeightVirtualList' 3 | 4 | interface IProps { 5 | data: T[]; 6 | estimatedItemHeight: number 7 | getItemHeight: (index: number) => number 8 | itemRender: (item: T) => JSX.Element 9 | } 10 | 11 | function PropHeight ({ data, estimatedItemHeight, getItemHeight, itemRender }: IProps) { 12 | const [scrollTop, setScrollTop] = useState(0) 13 | const [clientHeight, setClientHeight] = useState(0) 14 | 15 | const { startIndex, positions, totalHeight, visibleData, offset } = usePropHeightVirtualList({ 16 | data, 17 | estimatedItemHeight, 18 | getItemHeight, 19 | scrollTop, 20 | clientHeight, 21 | }) 22 | 23 | const containerRef = useRef(null) 24 | 25 | const handleScroll = useCallback(() => { 26 | if (containerRef.current) { 27 | setScrollTop(containerRef.current.scrollTop) 28 | } 29 | }, []) 30 | 31 | const containerRefCallback = useCallback((node: HTMLDivElement) => { 32 | if (node) { 33 | containerRef.current = node 34 | setClientHeight(node.clientHeight) 35 | } else { 36 | containerRef.current = null 37 | } 38 | }, []) 39 | 40 | return ( 41 |
46 |
50 |
54 | {visibleData.map((data, index) => ( 55 |
{itemRender(data)}
56 | ))} 57 |
58 |
59 | ) 60 | } 61 | 62 | export default PropHeight 63 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 37 | 38 | 39 | 40 |
41 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/hooks/useBinaryIndexedTreeVirtualList.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useCallback, useRef } from 'react' 2 | import BinaryIndexedTree from '../utils/BinaryIndexedTree' 3 | 4 | interface IParams { 5 | data: T[] 6 | estimatedItemHeight: number 7 | scrollTop: number 8 | clientHeight: number 9 | itemRefs: React.MutableRefObject<(HTMLDivElement | null)[]> 10 | } 11 | 12 | export default function useBinaryIndexedTreeVirtualList ({ 13 | data, 14 | estimatedItemHeight, 15 | scrollTop, 16 | clientHeight, 17 | itemRefs, 18 | }: IParams) { 19 | const treeRef = useRef() 20 | 21 | // 初始化树状数组 22 | useEffect(() => { 23 | const initPositions: number[] = [] 24 | const length = data.length 25 | for (let i = 0; i < length; i++) { 26 | initPositions[i] = estimatedItemHeight 27 | } 28 | treeRef.current = new BinaryIndexedTree(initPositions) 29 | }, [data.length, estimatedItemHeight]) 30 | 31 | // 查找 `startIndex` 32 | const t1 = performance.now() 33 | const startIndex = (treeRef.current?.findGe(scrollTop) || 1) - 1 34 | const t2 = performance.now() 35 | console.log('查找 startIndex 耗时: ', t2 - t1) 36 | const endIndex = Math.ceil(clientHeight / estimatedItemHeight) + startIndex + 1 37 | const visibleData = useMemo(() => data.slice(startIndex, endIndex), [data, endIndex, startIndex]) 38 | 39 | // 根据渲染的列表项,获取实际高度并更新树状数组 40 | const updatePositions = useCallback(() => { 41 | if (!itemRefs.current.length) return 42 | if (!data.length || startIndex === -1) return 43 | const t1 = performance.now() 44 | itemRefs.current.forEach((node, index) => { 45 | if (node) { 46 | const i = index + startIndex 47 | const realHeight = node.getBoundingClientRect().height 48 | const currentHeight = treeRef.current?.getValue(i + 1) || 0 49 | if (realHeight !== currentHeight) { 50 | treeRef.current?.update(i + 1, realHeight - currentHeight) 51 | } 52 | } 53 | }) 54 | const t2 = performance.now() 55 | console.log('更新缓存耗时: ', t2 - t1) 56 | }, [data.length, itemRefs, startIndex]) 57 | 58 | return { 59 | totalHeight: treeRef.current?.prefixSum(data.length) || 0, 60 | visibleData, 61 | offset: treeRef.current?.prefixSum(startIndex) || 0, 62 | updatePositions, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Process.md: -------------------------------------------------------------------------------- 1 | # 实现思路 2 | 3 | ## 1. 目的 4 | 5 | 首先要明确目的,我们要只渲染部分数据,就要弄清楚要渲染的数据从哪里开始,到哪里结束,也就是寻找数据范围。 6 | 7 | ## 2. 已知条件 8 | 9 | 列出要达到目的的已知条件: 10 | 11 | 1. 数据总条数 `total` 12 | 2. 可见容器的高度 `clientHeight` 13 | 3. 滚动的距离,也就是 `scrollTop` 14 | 4. 每个列表项的高度或者大致高度 `itemHeight` 15 | 16 | ## 3. 计算方法 17 | 18 | ### 3.1. 问题转换 19 | 20 | 要求渲染的数据范围,也就是寻找开始跟结束渲染的列表项索引 `startIndex`, `endIndex` 21 | 22 | 1. 要求 `startIndex` ,就要计算滚过的距离 `scrollTop` 占用了多少个列表项 23 | 2. 求 `endIndex` 则计算可见高度 `clientHeight` 占了多少列表项 `visibleCount` ,然后加上 `startIndex` 即可 24 | 25 | ### 3.2. 具体实现 26 | 27 | #### 3.2.1. 定高 28 | 29 | 如果每个列表项高度都是一样的,那就非常好算了,只需要简单的加减乘除而无需遍历就能算出想要的数据: 30 | 31 | ```javascript 32 | // floor 跟 ceil 是为了保证列表项能完全填充可见容器,而不会在上下留空白 33 | const startIndex = Math.floor(scrollTop / itemHeight) 34 | const endIndex = Math.ceil(clientHeight / itemHeight) + startIndex 35 | ``` 36 | 37 | #### 3.2.2. 不定高 38 | 39 | 如果每个列表项的高度不是一样的,那计算会麻烦一些,会多一些变量,不过大体的思路是一样的。 40 | 41 | 这边还需要分两种情况考虑 42 | 43 | ##### 3.2.2.1. 用户传入高度 44 | 45 | 这种情况其实是一种妥协,还是要求用户传入每个列表项的高度 46 | 47 | 可以让用户传入一个函数 `getItemHeight` ,接收列表项 index 作为参数,返回这个列表项的实际高度 48 | 49 | 但也需要一个预估的最小高度 `estimatedItemHeight` ,来处理列表项没渲染时的高度获取问题 50 | 51 | 具体的计算方式相应也要做出改变: 52 | 53 | 1. 维护一个数组 `positions` ,缓存每个列表项的高度与列表项底部距离总高度容器顶部的距离,根据 `estimatedItemHeight` 初始化这个数组 54 | 55 | ```typescript 56 | let positions: Array<{ height: number; offset: number }> 57 | ``` 58 | 59 | 2. 从 `positions` 中查找 `offset` 最接近且大于等于 `scrollTop` 的索引,即 `startIndex` 60 | 3. 根据 `clientHeight` 与 `estimatedItemHeight` 算出可视范围占了多少列表项,加上 `startIndex` 得到 `endIndex` 61 | 4. 根据传入的 `getItemHeight` 函数,计算出 `startIndex` 到 `endIndex` 范围节点的真实高度,更新 `positions` 数组(包括更新渲染的节点高度以及 `startIndex` 之后每一个列表项的 `offset`) 62 | 63 | ##### 3.2.2.2. 渲染后再获取实际高度(自适应) 64 | 65 | 渲染后再获取实际高度,就不需要用户传入 `getItemHeight` 了,而是在上述第 4 步骤中,遍历渲染出来的节点,调用 DOM 方法得到实际高度,再进行更新。 66 | 67 | ## 4. 优化点 68 | 69 | 能优化的地方(主要是有遍历数据的地方): 70 | 71 | 1. 查找 `offset` 过程 72 | 2. 更新 `positions` 数组过程 73 | 3. 多渲染一定个数列表项缓解白屏现象 74 | 4. 控制渲染节点变化频率 75 | 5. 减少能造成浏览器重排的动作,例如获取 `clientHeight` 只需获取一遍,一般情况下不会再改变 76 | 6. 监听 DOM 尺寸变化(使用 ResizeObserver 或者嵌入一个隐藏的 iframe 或 object 元素,监听其 window.onresize 事件) 77 | 7. 使用缓存池复用节点(参考 vue-virtual-scroller) 78 | 79 | ## 5. 优缺点 80 | 81 | 优点: 解决长列表问题,可渲染大量数据、减少内存占用 82 | 缺点: 滚动时 CPU 占用高,列表项组件无法保留内部状态(表格行内编辑难做的原因之一) 83 | 84 | ## 6. 可能的疑问 85 | 86 | 1. 在 scroll 事件上为什么不做节流 87 | 2. 鼠标与滚动条不同步 88 | 3. 真的有这么丝滑吗? 89 | -------------------------------------------------------------------------------- /src/components/OptimizedReactiveHeight.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useRef, useState, useEffect } from 'react' 2 | import useBinaryIndexedTreeVirtualList from '../hooks/useBinaryIndexedTreeVirtualList' 3 | 4 | interface IProps { 5 | data: T[]; 6 | estimatedItemHeight: number 7 | itemRender: (item: T) => JSX.Element 8 | } 9 | 10 | function OptimizedReactiveHeight ({ data, estimatedItemHeight, itemRender }: IProps) { 11 | const [scrollTop, setScrollTop] = useState(0) 12 | const [clientHeight, setClientHeight] = useState(0) 13 | const itemRefs = useRef>([]) 14 | 15 | const { totalHeight, visibleData, offset, updatePositions } = useBinaryIndexedTreeVirtualList({ 16 | data, 17 | estimatedItemHeight, 18 | scrollTop, 19 | clientHeight, 20 | itemRefs, 21 | }) 22 | 23 | const containerRef = useRef(null) 24 | 25 | const handleScroll = useCallback(() => { 26 | if (containerRef.current) { 27 | setScrollTop(containerRef.current.scrollTop) 28 | } 29 | }, []) 30 | 31 | const containerRefCallback = useCallback((node: HTMLDivElement) => { 32 | if (node) { 33 | containerRef.current = node 34 | setClientHeight(node.clientHeight) 35 | } else { 36 | containerRef.current = null 37 | } 38 | }, []) 39 | 40 | useEffect(() => { 41 | if (visibleData) { 42 | itemRefs.current = [] 43 | } 44 | }, [visibleData]) 45 | 46 | return ( 47 |
52 |
56 |
60 | {visibleData.map((data, index) => ( 61 |
{ 64 | itemRefs.current[index] = node 65 | if (visibleData.length === itemRefs.current.filter(Boolean).length) { 66 | updatePositions() 67 | } 68 | }} 69 | >{itemRender(data)}
70 | ))} 71 |
72 |
73 | ) 74 | } 75 | 76 | export default OptimizedReactiveHeight 77 | -------------------------------------------------------------------------------- /src/components/ReactiveHeight.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useRef, useState, useEffect } from 'react' 2 | import useReactiveHeightVirtualList from '../hooks/useReactiveHeightVirtualList' 3 | 4 | interface IProps { 5 | data: T[]; 6 | estimatedItemHeight: number 7 | itemRender: (item: T) => JSX.Element 8 | } 9 | 10 | function ReactiveHeight ({ data, estimatedItemHeight, itemRender }: IProps) { 11 | const [scrollTop, setScrollTop] = useState(0) 12 | const [clientHeight, setClientHeight] = useState(0) 13 | const itemRefs = useRef>([]) 14 | 15 | const { totalHeight, visibleData, offset, updatePositions } = useReactiveHeightVirtualList({ 16 | data, 17 | estimatedItemHeight, 18 | scrollTop, 19 | clientHeight, 20 | itemRefs, 21 | }) 22 | 23 | const containerRef = useRef(null) 24 | 25 | const handleScroll = useCallback(() => { 26 | if (containerRef.current) { 27 | setScrollTop(containerRef.current.scrollTop) 28 | } 29 | }, []) 30 | 31 | const containerRefCallback = useCallback((node: HTMLDivElement) => { 32 | if (node) { 33 | containerRef.current = node 34 | setClientHeight(node.clientHeight) 35 | } else { 36 | containerRef.current = null 37 | } 38 | }, []) 39 | 40 | useEffect(() => { 41 | if (visibleData) { 42 | itemRefs.current = [] 43 | } 44 | }, [visibleData]) 45 | 46 | return ( 47 |
52 |
56 |
60 | {visibleData.map((data, index) => ( 61 |
{ 65 | itemRefs.current[index] = node 66 | if (visibleData.length === itemRefs.current.filter(Boolean).length) { 67 | updatePositions() 68 | } 69 | }} 70 | >{itemRender(data)}
71 | ))} 72 |
73 |
74 | ) 75 | } 76 | 77 | export default ReactiveHeight 78 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | React App
-------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/hooks/useReactiveHeightVirtualList.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useMemo, useCallback } from 'react' 2 | import binarySearch from '../utils/binary-search' 3 | 4 | interface IParams { 5 | data: T[] 6 | estimatedItemHeight: number 7 | scrollTop: number 8 | clientHeight: number 9 | itemRefs: React.MutableRefObject<(HTMLDivElement | null)[]> 10 | } 11 | 12 | interface IPosition { 13 | height: number 14 | offset: number 15 | } 16 | 17 | export default function useReactiveHeightVirtualList ({ 18 | data, 19 | estimatedItemHeight, 20 | scrollTop, 21 | clientHeight, 22 | itemRefs, 23 | }: IParams) { 24 | const [positions, setPositions] = useState([]) 25 | 26 | // 以 `estimatedItemHeight` 初始化 `positions` 数组 27 | useEffect(() => { 28 | const initPositions: IPosition[] = [] 29 | const length = data.length 30 | for (let i = 0; i < length; i++) { 31 | initPositions[i] = { 32 | height: estimatedItemHeight, 33 | offset: estimatedItemHeight + (initPositions[i - 1]?.offset || 0) 34 | } 35 | } 36 | setPositions(initPositions) 37 | }, [data.length, estimatedItemHeight]) 38 | 39 | // 二分查找 `startIndex` 40 | const t1 = performance.now() 41 | const startIndex = binarySearch(positions.slice(0, Math.ceil(scrollTop / estimatedItemHeight) + 1).map((p) => p.offset), scrollTop) 42 | const t2 = performance.now() 43 | console.log('查找 startIndex 耗时: ', t2 - t1) 44 | const endIndex = Math.ceil(clientHeight / estimatedItemHeight) + startIndex + 1 45 | const visibleData = useMemo(() => data.slice(startIndex, endIndex), [data, endIndex, startIndex]) 46 | 47 | // 根据渲染的列表项,获取实际高度并更新 `positions` 数组 48 | const updatePositions = useCallback(() => { 49 | if (!itemRefs.current.length) return 50 | if (!positions.length || startIndex === -1) return 51 | const newPositions: IPosition[] = [] 52 | let firstUpdatedIndex = -1 53 | const t1 = performance.now() 54 | itemRefs.current.forEach((node, index) => { 55 | if (node) { 56 | const i = index + startIndex 57 | const realHeight = node.getBoundingClientRect().height 58 | if (realHeight !== positions[i].height) { 59 | if (firstUpdatedIndex === -1) firstUpdatedIndex = i 60 | newPositions[i] = { 61 | height: realHeight, 62 | // 先随便赋个值,后面再统一更新 63 | offset: 0 64 | } 65 | } 66 | } 67 | }) 68 | if (firstUpdatedIndex !== -1) { 69 | // 有更新的节点 70 | positions.forEach((p, i) => { 71 | if (!newPositions[i]) newPositions[i] = p 72 | }) 73 | // 从 `firstUpdatedIndex` 开始,更新后面的 `offset` 74 | const length = positions.length 75 | for (let i = firstUpdatedIndex; i < length; i++) { 76 | newPositions[i].offset = newPositions[i].height + (newPositions[i - 1]?.offset || 0) 77 | } 78 | const t2 = performance.now() 79 | console.log('更新缓存耗时: ', t2 - t1) 80 | setPositions(newPositions) 81 | } 82 | }, [itemRefs, positions, startIndex]) 83 | 84 | return { 85 | totalHeight: positions[positions.length - 1]?.offset || 0, 86 | visibleData, 87 | offset: (positions[startIndex]?.offset || 0) - (positions[startIndex]?.height || 0), 88 | updatePositions, 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/hooks/usePropHeightVirtualList.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useMemo, useRef } from 'react' 2 | import binarySearch from '../utils/binary-search' 3 | 4 | interface IParams { 5 | data: T[] 6 | getItemHeight: (index: number) => number 7 | estimatedItemHeight: number 8 | scrollTop: number 9 | clientHeight: number 10 | } 11 | 12 | interface IPosition { 13 | height: number 14 | offset: number 15 | } 16 | 17 | export default function usePropHeightVirtualList ({ 18 | data, 19 | getItemHeight, 20 | estimatedItemHeight, 21 | scrollTop, 22 | clientHeight, 23 | }: IParams) { 24 | const [positions, setPositions] = useState([]) 25 | 26 | // 以 `estimatedItemHeight` 初始化 `positions` 数组 27 | useEffect(() => { 28 | const initPositions: IPosition[] = [] 29 | const length = data.length 30 | for (let i = 0; i < length; i++) { 31 | initPositions[i] = { 32 | height: estimatedItemHeight, 33 | offset: estimatedItemHeight + (initPositions[i - 1]?.offset || 0) 34 | } 35 | } 36 | setPositions(initPositions) 37 | }, [data.length, estimatedItemHeight]) 38 | 39 | // 二分查找 `startIndex` 40 | const t1 = performance.now() 41 | const startIndex = binarySearch(positions.slice(0, Math.ceil(scrollTop / estimatedItemHeight) + 1).map((p) => p.offset), scrollTop) 42 | const t2 = performance.now() 43 | console.log('查找 startIndex 耗时: ', t2 - t1) 44 | const endIndex = Math.ceil(clientHeight / estimatedItemHeight) + startIndex + 1 45 | const visibleData = useMemo(() => data.slice(startIndex, endIndex), [data, endIndex, startIndex]) 46 | 47 | const positionsRef = useRef() 48 | positionsRef.current = positions 49 | // 根据渲染的列表项,获取实际高度并更新 `positions` 数组 50 | useEffect(() => { 51 | if (!positionsRef.current || !positionsRef.current.length || startIndex === -1) return 52 | const positions = positionsRef.current 53 | const newPositions: IPosition[] = [] 54 | let firstUpdatedIndex = -1 55 | const limit = Math.min(positions.length - 1, endIndex) 56 | const t1 = performance.now() 57 | for (let i = startIndex; i <= limit; i++) { 58 | const realHeight = getItemHeight(i) 59 | if (realHeight !== positions[i].height) { 60 | if (firstUpdatedIndex === -1) firstUpdatedIndex = i 61 | newPositions[i] = { 62 | height: realHeight, 63 | // 先随便赋个值,后面再统一更新 64 | offset: 0 65 | } 66 | } 67 | } 68 | if (firstUpdatedIndex !== -1) { 69 | // 有更新的节点 70 | positions.forEach((p, i) => { 71 | if (!newPositions[i]) newPositions[i] = p 72 | }) 73 | // 从 `firstUpdatedIndex` 开始,更新后面的 `offset` 74 | const length = positions.length 75 | for (let i = firstUpdatedIndex; i < length; i++) { 76 | newPositions[i].offset = newPositions[i].height + (newPositions[i - 1]?.offset || 0) 77 | } 78 | const t2 = performance.now() 79 | console.log('更新缓存耗时: ', t2 - t1) 80 | setPositions(newPositions) 81 | } 82 | }, [endIndex, getItemHeight, startIndex]) 83 | 84 | return { 85 | startIndex, 86 | positions, 87 | totalHeight: positions[positions.length - 1]?.offset || 0, 88 | visibleData, 89 | offset: (positions[startIndex]?.offset || 0) - (positions[startIndex]?.height || 0), 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/utils/BinaryIndexedTree.ts: -------------------------------------------------------------------------------- 1 | // eslint-disabled 2 | 3 | // <= n 的最大的 2^x 4 | function power2le (n: number) { 5 | let sum = 1 6 | let tmp = 2 7 | while (tmp <= n) { 8 | sum = tmp 9 | tmp = sum << 1 10 | } 11 | return sum 12 | } 13 | class BinaryIndexedTree { 14 | tree!: any[] 15 | bitMask!: number 16 | constructor(nums: number[]) { 17 | this.__init(nums) 18 | } 19 | // 初始化树状数组, 20 | // O(n) 21 | __init (nums: number[]) { 22 | this.tree = Array(nums.length + 1).fill(0) 23 | for (let i = 0; i < nums.length; i++) { 24 | this.tree[i + 1] = nums[i] 25 | } 26 | for (let i = 1; i < this.tree.length; i++) { 27 | let j = i + (i & -i) 28 | if (j < this.tree.length) { 29 | this.tree[j] += this.tree[i] 30 | } 31 | } 32 | this.bitMask = power2le(nums.length - 1) 33 | } 34 | // 更改第 i 项的, 1<=i 0) { 47 | sum += this.tree[n] 48 | n -= n & -n 49 | } 50 | return sum 51 | } 52 | // 计算 i ~ j 项的和, 53 | // 也可用来获取某个位置的实际值,不过建议使用 getValue 方法,效率更高 54 | // 2*O(log n) 55 | rangeSum (i: number, j: number | undefined) { 56 | return this.prefixSum(j) - this.prefixSum(i - 1) 57 | } 58 | // 获取第 i 项的实际值, 1<=i 0) { 62 | let z = i - (i & -i) 63 | i-- 64 | while (i !== z) { 65 | sum -= this.tree[i] 66 | i -= (i & -i) 67 | } 68 | } 69 | return sum 70 | } 71 | // 找到一个n,其前n项和为 target 72 | // 要求数组非负,否则只能前n项迭代计算 73 | // 由于存在 0 的情况,满足条件的 n 有多个,返回其中任意一个 74 | // O(logn) 75 | find (target: number) { 76 | let idx = 0 77 | let len = this.tree.length 78 | let bitMask = this.bitMask 79 | while (bitMask !== 0 && (idx < len)) { 80 | let tIdx = idx + bitMask 81 | if (target === this.tree[tIdx]) { 82 | return tIdx 83 | } else if (target > this.tree[tIdx]) { 84 | idx = tIdx 85 | target -= this.tree[tIdx] 86 | } 87 | bitMask >>= 1 88 | } 89 | if (target !== 0) { 90 | return -1 91 | } else { 92 | return idx 93 | } 94 | } 95 | // 找到最大的一个n,其前n项和为 target 96 | // 要求数组非负,否则只能前n项迭代计算 97 | // O(logn) 98 | findG (target: number) { 99 | let idx = 0 100 | let len = this.tree.length 101 | let bitMask = this.bitMask 102 | while (bitMask !== 0 && (idx < len)) { 103 | let tIdx = idx + bitMask 104 | if (target >= this.tree[tIdx]) { 105 | idx = tIdx 106 | target -= this.tree[tIdx] 107 | } 108 | bitMask >>= 1 109 | } 110 | if (target !== 0) { 111 | return -1 112 | } else { 113 | return idx 114 | } 115 | } 116 | // 找到最小的一个n,其前n项和大于等于 target 117 | // O(logn) 118 | findGe (target: number) { 119 | let idx = 0 120 | let len = this.tree.length 121 | let bitMask = this.bitMask 122 | while (bitMask !== 0 && (idx < len)) { 123 | let tIdx = idx + bitMask 124 | if (target === this.tree[tIdx]) { 125 | return tIdx 126 | } else if (target > this.tree[tIdx]) { 127 | idx = tIdx 128 | target -= this.tree[tIdx] 129 | } 130 | bitMask >>= 1 131 | } 132 | return target === 0 ? idx : ( 133 | idx + 1 < this.tree.length ? idx + 1 : -1 134 | ) 135 | } 136 | } 137 | export default BinaryIndexedTree -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import { Switch, Route, useHistory, useLocation, Redirect } from 'react-router-dom' 3 | import Counter from './components/Counter' 4 | import FixedHeight from './components/FixedHeight' 5 | import PropHeight from './components/PropHeight' 6 | import ReactiveHeight from './components/ReactiveHeight' 7 | import OptimizedReactiveHeight from './components/OptimizedReactiveHeight' 8 | import NormalList from './components/NormalList' 9 | import dataGen from './utils/data-generator' 10 | 11 | const data = dataGen() 12 | 13 | const normalListData = dataGen(50000) 14 | 15 | const radioButtons = [ 16 | { 17 | path: '/fixed-height', 18 | text: '定高', 19 | }, 20 | { 21 | path: '/prop-height', 22 | text: '不定高(二分),' 23 | }, 24 | { 25 | path: '/reactive-height', 26 | text: '自适应(二分)', 27 | }, 28 | { 29 | path: '/optimized-reactive-height', 30 | text: '自适应(树状数组)', 31 | }, 32 | { 33 | path: '/normal-list', 34 | text: '普通列表', 35 | }, 36 | { 37 | path: '/fixed-height-with-counter', 38 | text: '带计数器', 39 | }, 40 | ] 41 | 42 | export default function App () { 43 | const history = useHistory() 44 | const location = useLocation() 45 | 46 | const itemRender = useCallback((item) => { 47 | return ( 48 |
{item.value}
55 | ) 56 | }, []) 57 | 58 | const itemRenderWithCounter = useCallback((item) => { 59 | return ( 60 |
{item.value} |
67 | ) 68 | }, []) 69 | 70 | const reactiveHeightItemRender = useCallback((item) => { 71 | return ( 72 |
{item.value}
79 | ) 80 | }, []) 81 | 82 | const getItemHeight = useCallback((index: number) => { 83 | return 50 + (index % 5) * 10 84 | }, []) 85 | 86 | return ( 87 | <> 88 |
89 | { 90 | radioButtons.map((radio) => { 91 | return ( 92 | 100 | ) 101 | }) 102 | } 103 |
104 |
105 | 106 | 107 | 108 | 109 | 110 | 115 | 116 | 117 | 123 | 124 | 125 | 130 | 131 | 132 | 137 | 138 | 139 | 144 | 145 | 146 | 151 | 152 | 153 |
154 | 155 | ) 156 | } 157 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /docs/static/js/runtime-main.48274765.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../webpack/bootstrap"],"names":["webpackJsonpCallback","data","moduleId","chunkId","chunkIds","moreModules","executeModules","i","resolves","length","Object","prototype","hasOwnProperty","call","installedChunks","push","modules","parentJsonpFunction","shift","deferredModules","apply","checkDeferredModules","result","deferredModule","fulfilled","j","depId","splice","__webpack_require__","s","installedModules","1","exports","module","l","m","c","d","name","getter","o","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","p","jsonpArray","this","oldJsonpFunction","slice"],"mappings":"aACE,SAASA,EAAqBC,GAQ7B,IAPA,IAMIC,EAAUC,EANVC,EAAWH,EAAK,GAChBI,EAAcJ,EAAK,GACnBK,EAAiBL,EAAK,GAIHM,EAAI,EAAGC,EAAW,GACpCD,EAAIH,EAASK,OAAQF,IACzBJ,EAAUC,EAASG,GAChBG,OAAOC,UAAUC,eAAeC,KAAKC,EAAiBX,IAAYW,EAAgBX,IACpFK,EAASO,KAAKD,EAAgBX,GAAS,IAExCW,EAAgBX,GAAW,EAE5B,IAAID,KAAYG,EACZK,OAAOC,UAAUC,eAAeC,KAAKR,EAAaH,KACpDc,EAAQd,GAAYG,EAAYH,IAKlC,IAFGe,GAAqBA,EAAoBhB,GAEtCO,EAASC,QACdD,EAASU,OAATV,GAOD,OAHAW,EAAgBJ,KAAKK,MAAMD,EAAiBb,GAAkB,IAGvDe,IAER,SAASA,IAER,IADA,IAAIC,EACIf,EAAI,EAAGA,EAAIY,EAAgBV,OAAQF,IAAK,CAG/C,IAFA,IAAIgB,EAAiBJ,EAAgBZ,GACjCiB,GAAY,EACRC,EAAI,EAAGA,EAAIF,EAAed,OAAQgB,IAAK,CAC9C,IAAIC,EAAQH,EAAeE,GACG,IAA3BX,EAAgBY,KAAcF,GAAY,GAE3CA,IACFL,EAAgBQ,OAAOpB,IAAK,GAC5Be,EAASM,EAAoBA,EAAoBC,EAAIN,EAAe,KAItE,OAAOD,EAIR,IAAIQ,EAAmB,GAKnBhB,EAAkB,CACrBiB,EAAG,GAGAZ,EAAkB,GAGtB,SAASS,EAAoB1B,GAG5B,GAAG4B,EAAiB5B,GACnB,OAAO4B,EAAiB5B,GAAU8B,QAGnC,IAAIC,EAASH,EAAiB5B,GAAY,CACzCK,EAAGL,EACHgC,GAAG,EACHF,QAAS,IAUV,OANAhB,EAAQd,GAAUW,KAAKoB,EAAOD,QAASC,EAAQA,EAAOD,QAASJ,GAG/DK,EAAOC,GAAI,EAGJD,EAAOD,QAKfJ,EAAoBO,EAAInB,EAGxBY,EAAoBQ,EAAIN,EAGxBF,EAAoBS,EAAI,SAASL,EAASM,EAAMC,GAC3CX,EAAoBY,EAAER,EAASM,IAClC5B,OAAO+B,eAAeT,EAASM,EAAM,CAAEI,YAAY,EAAMC,IAAKJ,KAKhEX,EAAoBgB,EAAI,SAASZ,GACX,qBAAXa,QAA0BA,OAAOC,aAC1CpC,OAAO+B,eAAeT,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DrC,OAAO+B,eAAeT,EAAS,aAAc,CAAEe,OAAO,KAQvDnB,EAAoBoB,EAAI,SAASD,EAAOE,GAEvC,GADU,EAAPA,IAAUF,EAAQnB,EAAoBmB,IAC/B,EAAPE,EAAU,OAAOF,EACpB,GAAW,EAAPE,GAA8B,kBAAVF,GAAsBA,GAASA,EAAMG,WAAY,OAAOH,EAChF,IAAII,EAAKzC,OAAO0C,OAAO,MAGvB,GAFAxB,EAAoBgB,EAAEO,GACtBzC,OAAO+B,eAAeU,EAAI,UAAW,CAAET,YAAY,EAAMK,MAAOA,IACtD,EAAPE,GAA4B,iBAATF,EAAmB,IAAI,IAAIM,KAAON,EAAOnB,EAAoBS,EAAEc,EAAIE,EAAK,SAASA,GAAO,OAAON,EAAMM,IAAQC,KAAK,KAAMD,IAC9I,OAAOF,GAIRvB,EAAoB2B,EAAI,SAAStB,GAChC,IAAIM,EAASN,GAAUA,EAAOiB,WAC7B,WAAwB,OAAOjB,EAAgB,SAC/C,WAA8B,OAAOA,GAEtC,OADAL,EAAoBS,EAAEE,EAAQ,IAAKA,GAC5BA,GAIRX,EAAoBY,EAAI,SAASgB,EAAQC,GAAY,OAAO/C,OAAOC,UAAUC,eAAeC,KAAK2C,EAAQC,IAGzG7B,EAAoB8B,EAAI,4BAExB,IAAIC,EAAaC,KAAK,uCAAyCA,KAAK,wCAA0C,GAC1GC,EAAmBF,EAAW5C,KAAKuC,KAAKK,GAC5CA,EAAW5C,KAAOf,EAClB2D,EAAaA,EAAWG,QACxB,IAAI,IAAIvD,EAAI,EAAGA,EAAIoD,EAAWlD,OAAQF,IAAKP,EAAqB2D,EAAWpD,IAC3E,IAAIU,EAAsB4C,EAI1BxC,I","file":"static/js/runtime-main.48274765.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tfunction webpackJsonpCallback(data) {\n \t\tvar chunkIds = data[0];\n \t\tvar moreModules = data[1];\n \t\tvar executeModules = data[2];\n\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [];\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(data);\n\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n\n \t\t// add entry modules from loaded chunk to deferred list\n \t\tdeferredModules.push.apply(deferredModules, executeModules || []);\n\n \t\t// run deferred modules when all chunks ready\n \t\treturn checkDeferredModules();\n \t};\n \tfunction checkDeferredModules() {\n \t\tvar result;\n \t\tfor(var i = 0; i < deferredModules.length; i++) {\n \t\t\tvar deferredModule = deferredModules[i];\n \t\t\tvar fulfilled = true;\n \t\t\tfor(var j = 1; j < deferredModule.length; j++) {\n \t\t\t\tvar depId = deferredModule[j];\n \t\t\t\tif(installedChunks[depId] !== 0) fulfilled = false;\n \t\t\t}\n \t\t\tif(fulfilled) {\n \t\t\t\tdeferredModules.splice(i--, 1);\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = deferredModule[0]);\n \t\t\t}\n \t\t}\n\n \t\treturn result;\n \t}\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// object to store loaded and loading chunks\n \t// undefined = chunk not loaded, null = chunk preloaded/prefetched\n \t// Promise = chunk loading, 0 = chunk loaded\n \tvar installedChunks = {\n \t\t1: 0\n \t};\n\n \tvar deferredModules = [];\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/virtual-list-demo-react/\";\n\n \tvar jsonpArray = this[\"webpackJsonpvirtual-list-demo-react\"] = this[\"webpackJsonpvirtual-list-demo-react\"] || [];\n \tvar oldJsonpFunction = jsonpArray.push.bind(jsonpArray);\n \tjsonpArray.push = webpackJsonpCallback;\n \tjsonpArray = jsonpArray.slice();\n \tfor(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);\n \tvar parentJsonpFunction = oldJsonpFunction;\n\n\n \t// run deferred modules from other chunks\n \tcheckDeferredModules();\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/main.91ca23b2.chunk.js: -------------------------------------------------------------------------------- 1 | (this["webpackJsonpvirtual-list-demo-react"]=this["webpackJsonpvirtual-list-demo-react"]||[]).push([[0],{17:function(e,t,n){e.exports=n(28)},22:function(e,t,n){},28:function(e,t,n){"use strict";n.r(t);var a=n(0),r=n.n(a),i=n(13),c=n.n(i),l=(n(22),n(1)),o=n(2);function s(){var e=Object(a.useState)(0),t=Object(o.a)(e,2),n=t[0],i=t[1];return r.a.createElement("span",null,r.a.createElement("span",null,"Count: ",n),r.a.createElement("button",{onClick:function(){return i(n+1)}},"Increase"))}var u=function(e){var t=e.data,n=e.itemHeight,i=e.itemRender,c=Object(a.useState)(0),l=Object(o.a)(c,2),s=l[0],u=l[1],h=Object(a.useState)(0),f=Object(o.a)(h,2),m=f[0],d=f[1],v=function(e){var t=e.data,n=e.itemHeight,r=e.scrollTop,i=e.clientHeight,c=Object(a.useMemo)((function(){return t.length*n}),[t.length,n]),l=Math.floor(r/n),o=Math.ceil(i/n)+l+1;return{totalHeight:c,visibleData:Object(a.useMemo)((function(){return t.slice(l,o)}),[t,o,l]),offset:Object(a.useMemo)((function(){return l*n}),[n,l])}}({data:t,itemHeight:n,scrollTop:s,clientHeight:m}),g=v.totalHeight,b=v.visibleData,p=v.offset,j=Object(a.useRef)(null),E=Object(a.useCallback)((function(){j.current&&u(j.current.scrollTop)}),[]),O=Object(a.useCallback)((function(e){e?(j.current=e,d(e.clientHeight)):j.current=null}),[]);return r.a.createElement("div",{className:"container",ref:O,onScroll:E},r.a.createElement("div",{className:"total-list",style:{height:"".concat(g,"px")}}),r.a.createElement("div",{className:"visible-list",style:{transform:"translateY(".concat(p,"px)")}},b.map((function(e){return r.a.createElement("div",{key:e.id,style:{height:"".concat(n,"px")}},i(e))}))))};function h(e,t){var n=e.length;if(!n)return-1;for(var a=-1,r=0,i=n-1;r<=i;){if(r===i)return e[r]>=t?r:-1;var c=r+i>>1,l=e[c];if(l===t)return c;tl)&&(a=c),i--):r=c+1}return a}var f=function(e){var t=e.data,n=e.estimatedItemHeight,i=e.getItemHeight,c=e.itemRender,l=Object(a.useState)(0),s=Object(o.a)(l,2),u=s[0],f=s[1],m=Object(a.useState)(0),d=Object(o.a)(m,2),v=d[0],g=d[1],b=function(e){var t,n,r,i=e.data,c=e.getItemHeight,l=e.estimatedItemHeight,s=e.scrollTop,u=e.clientHeight,f=Object(a.useState)([]),m=Object(o.a)(f,2),d=m[0],v=m[1];Object(a.useEffect)((function(){for(var e=[],t=i.length,n=0;n0&&void 0!==arguments[0]?arguments[0]:this.tree.length-1,t=0;e>0;)t+=this.tree[e],e-=e&-e;return t}},{key:"rangeSum",value:function(e,t){return this.prefixSum(t)-this.prefixSum(e-1)}},{key:"getValue",value:function(e){var t=this.tree[e];if(e>0){var n=e-(e&-e);for(e--;e!==n;)t-=this.tree[e],e-=e&-e}return t}},{key:"find",value:function(e){for(var t=0,n=this.tree.length,a=this.bitMask;0!==a&&tthis.tree[r]&&(t=r,e-=this.tree[r]),a>>=1}return 0!==e?-1:t}},{key:"findG",value:function(e){for(var t=0,n=this.tree.length,a=this.bitMask;0!==a&&t=this.tree[r]&&(t=r,e-=this.tree[r]),a>>=1}return 0!==e?-1:t}},{key:"findGe",value:function(e){for(var t=0,n=this.tree.length,a=this.bitMask;0!==a&&tthis.tree[r]&&(t=r,e-=this.tree[r]),a>>=1}return 0===e?t:t+10&&void 0!==arguments[0]?arguments[0]:2e5,t=[],n=0;n\n Count: {count}\n \n \n )\n}\n","import React, { useCallback, useRef, useState } from 'react'\nimport useFixedHeightVirtualList from '../hooks/useFixedHeightVirtualList'\n\ninterface IProps {\n data: T[];\n itemHeight: number;\n itemRender: (item: T) => JSX.Element\n}\n\nfunction FixedHeight ({ data, itemHeight, itemRender }: IProps) {\n const [scrollTop, setScrollTop] = useState(0)\n const [clientHeight, setClientHeight] = useState(0)\n\n const { totalHeight, visibleData, offset } = useFixedHeightVirtualList({\n data,\n itemHeight,\n scrollTop,\n clientHeight,\n })\n\n const containerRef = useRef(null)\n\n const handleScroll = useCallback(() => {\n if (containerRef.current) {\n setScrollTop(containerRef.current.scrollTop)\n }\n }, [])\n\n const containerRefCallback = useCallback((node: HTMLDivElement) => {\n if (node) {\n containerRef.current = node\n setClientHeight(node.clientHeight)\n } else {\n containerRef.current = null\n }\n }, [])\n\n return (\n \n \n \n {visibleData.map((data) => (\n
{itemRender(data)}
\n ))}\n \n \n )\n}\n\nexport default FixedHeight\n","import { useMemo } from 'react'\n\ninterface IParams {\n data: T[]\n itemHeight: number\n scrollTop: number\n clientHeight: number\n}\n\nexport default function useFixedHeightVirtualList ({\n data,\n itemHeight,\n scrollTop,\n clientHeight,\n}: IParams) {\n const totalHeight = useMemo(() => data.length * itemHeight, [data.length, itemHeight])\n const startIndex = Math.floor(scrollTop / itemHeight)\n const endIndex = Math.ceil(clientHeight / itemHeight) + startIndex + 1\n const visibleData = useMemo(() => data.slice(startIndex, endIndex), [data, endIndex, startIndex])\n // translateY\n const offset = useMemo(() => startIndex * itemHeight, [itemHeight, startIndex])\n\n return {\n totalHeight,\n visibleData,\n offset,\n }\n}\n","// 二分查找修改,找到最接近且大于等于 target 的索引\n\nexport default function binarySearch (list: number[], target: number): number {\n const length = list.length\n if (!length) return -1\n let result = -1\n let start = 0\n let end = length - 1\n while (start <= end) {\n if (start === end) return list[start] >= target ? start : -1\n const mid = (start + end) >> 1\n const midValue = list[mid]\n if (midValue === target) return mid\n if (target < midValue) {\n if (result === -1 || list[result] > midValue) {\n result = mid\n }\n end--\n } else {\n start = mid + 1\n }\n }\n return result\n}\n","import React, { useCallback, useRef, useState } from 'react'\nimport usePropHeightVirtualList from '../hooks/usePropHeightVirtualList'\n\ninterface IProps {\n data: T[];\n estimatedItemHeight: number\n getItemHeight: (index: number) => number\n itemRender: (item: T) => JSX.Element\n}\n\nfunction PropHeight ({ data, estimatedItemHeight, getItemHeight, itemRender }: IProps) {\n const [scrollTop, setScrollTop] = useState(0)\n const [clientHeight, setClientHeight] = useState(0)\n\n const { startIndex, positions, totalHeight, visibleData, offset } = usePropHeightVirtualList({\n data,\n estimatedItemHeight,\n getItemHeight,\n scrollTop,\n clientHeight,\n })\n\n const containerRef = useRef(null)\n\n const handleScroll = useCallback(() => {\n if (containerRef.current) {\n setScrollTop(containerRef.current.scrollTop)\n }\n }, [])\n\n const containerRefCallback = useCallback((node: HTMLDivElement) => {\n if (node) {\n containerRef.current = node\n setClientHeight(node.clientHeight)\n } else {\n containerRef.current = null\n }\n }, [])\n\n return (\n \n \n \n {visibleData.map((data, index) => (\n
{itemRender(data)}
\n ))}\n \n \n )\n}\n\nexport default PropHeight\n","import { useState, useEffect, useMemo, useRef } from 'react'\nimport binarySearch from '../utils/binary-search'\n\ninterface IParams {\n data: T[]\n getItemHeight: (index: number) => number\n estimatedItemHeight: number\n scrollTop: number\n clientHeight: number\n}\n\ninterface IPosition {\n height: number\n offset: number\n}\n\nexport default function usePropHeightVirtualList ({\n data,\n getItemHeight,\n estimatedItemHeight,\n scrollTop,\n clientHeight,\n}: IParams) {\n const [positions, setPositions] = useState([])\n\n // 以 `estimatedItemHeight` 初始化 `positions` 数组\n useEffect(() => {\n const initPositions: IPosition[] = []\n const length = data.length\n for (let i = 0; i < length; i++) {\n initPositions[i] = {\n height: estimatedItemHeight,\n offset: estimatedItemHeight + (initPositions[i - 1]?.offset || 0)\n }\n }\n setPositions(initPositions)\n }, [data.length, estimatedItemHeight])\n\n // 二分查找 `startIndex`\n const t1 = performance.now()\n const startIndex = binarySearch(positions.slice(0, Math.ceil(scrollTop / estimatedItemHeight) + 1).map((p) => p.offset), scrollTop)\n const t2 = performance.now()\n console.log('查找 startIndex 耗时: ', t2 - t1)\n const endIndex = Math.ceil(clientHeight / estimatedItemHeight) + startIndex + 1\n const visibleData = useMemo(() => data.slice(startIndex, endIndex), [data, endIndex, startIndex])\n\n const positionsRef = useRef()\n positionsRef.current = positions\n // 根据渲染的列表项,获取实际高度并更新 `positions` 数组\n useEffect(() => {\n if (!positionsRef.current || !positionsRef.current.length || startIndex === -1) return\n const positions = positionsRef.current\n const newPositions: IPosition[] = []\n let firstUpdatedIndex = -1\n const limit = Math.min(positions.length - 1, endIndex)\n const t1 = performance.now()\n for (let i = startIndex; i <= limit; i++) {\n const realHeight = getItemHeight(i)\n if (realHeight !== positions[i].height) {\n if (firstUpdatedIndex === -1) firstUpdatedIndex = i\n newPositions[i] = {\n height: realHeight,\n // 先随便赋个值,后面再统一更新\n offset: 0\n }\n }\n }\n if (firstUpdatedIndex !== -1) {\n // 有更新的节点\n positions.forEach((p, i) => {\n if (!newPositions[i]) newPositions[i] = p\n })\n // 从 `firstUpdatedIndex` 开始,更新后面的 `offset`\n const length = positions.length\n for (let i = firstUpdatedIndex; i < length; i++) {\n newPositions[i].offset = newPositions[i].height + (newPositions[i - 1]?.offset || 0)\n }\n const t2 = performance.now()\n console.log('更新缓存耗时: ', t2 - t1)\n setPositions(newPositions)\n }\n }, [endIndex, getItemHeight, startIndex])\n\n return {\n startIndex,\n positions,\n totalHeight: positions[positions.length - 1]?.offset || 0,\n visibleData,\n offset: (positions[startIndex]?.offset || 0) - (positions[startIndex]?.height || 0),\n }\n}\n","import React, { useCallback, useRef, useState, useEffect } from 'react'\nimport useReactiveHeightVirtualList from '../hooks/useReactiveHeightVirtualList'\n\ninterface IProps {\n data: T[];\n estimatedItemHeight: number\n itemRender: (item: T) => JSX.Element\n}\n\nfunction ReactiveHeight ({ data, estimatedItemHeight, itemRender }: IProps) {\n const [scrollTop, setScrollTop] = useState(0)\n const [clientHeight, setClientHeight] = useState(0)\n const itemRefs = useRef>([])\n\n const { totalHeight, visibleData, offset, updatePositions } = useReactiveHeightVirtualList({\n data,\n estimatedItemHeight,\n scrollTop,\n clientHeight,\n itemRefs,\n })\n\n const containerRef = useRef(null)\n\n const handleScroll = useCallback(() => {\n if (containerRef.current) {\n setScrollTop(containerRef.current.scrollTop)\n }\n }, [])\n\n const containerRefCallback = useCallback((node: HTMLDivElement) => {\n if (node) {\n containerRef.current = node\n setClientHeight(node.clientHeight)\n } else {\n containerRef.current = null\n }\n }, [])\n\n useEffect(() => {\n if (visibleData) {\n itemRefs.current = []\n }\n }, [visibleData])\n\n return (\n \n \n \n {visibleData.map((data, index) => (\n {\n itemRefs.current[index] = node\n if (visibleData.length === itemRefs.current.filter(Boolean).length) {\n updatePositions()\n }\n }}\n >{itemRender(data)}\n ))}\n \n \n )\n}\n\nexport default ReactiveHeight\n","import { useState, useEffect, useMemo, useCallback } from 'react'\nimport binarySearch from '../utils/binary-search'\n\ninterface IParams {\n data: T[]\n estimatedItemHeight: number\n scrollTop: number\n clientHeight: number\n itemRefs: React.MutableRefObject<(HTMLDivElement | null)[]>\n}\n\ninterface IPosition {\n height: number\n offset: number\n}\n\nexport default function useReactiveHeightVirtualList ({\n data,\n estimatedItemHeight,\n scrollTop,\n clientHeight,\n itemRefs,\n}: IParams) {\n const [positions, setPositions] = useState([])\n\n // 以 `estimatedItemHeight` 初始化 `positions` 数组\n useEffect(() => {\n const initPositions: IPosition[] = []\n const length = data.length\n for (let i = 0; i < length; i++) {\n initPositions[i] = {\n height: estimatedItemHeight,\n offset: estimatedItemHeight + (initPositions[i - 1]?.offset || 0)\n }\n }\n setPositions(initPositions)\n }, [data.length, estimatedItemHeight])\n\n // 二分查找 `startIndex`\n const t1 = performance.now()\n const startIndex = binarySearch(positions.slice(0, Math.ceil(scrollTop / estimatedItemHeight) + 1).map((p) => p.offset), scrollTop)\n const t2 = performance.now()\n console.log('查找 startIndex 耗时: ', t2 - t1)\n const endIndex = Math.ceil(clientHeight / estimatedItemHeight) + startIndex + 1\n const visibleData = useMemo(() => data.slice(startIndex, endIndex), [data, endIndex, startIndex])\n\n // 根据渲染的列表项,获取实际高度并更新 `positions` 数组\n const updatePositions = useCallback(() => {\n if (!itemRefs.current.length) return\n if (!positions.length || startIndex === -1) return\n const newPositions: IPosition[] = []\n let firstUpdatedIndex = -1\n const t1 = performance.now()\n itemRefs.current.forEach((node, index) => {\n if (node) {\n const i = index + startIndex\n const realHeight = node.getBoundingClientRect().height\n if (realHeight !== positions[i].height) {\n if (firstUpdatedIndex === -1) firstUpdatedIndex = i\n newPositions[i] = {\n height: realHeight,\n // 先随便赋个值,后面再统一更新\n offset: 0\n }\n }\n }\n })\n if (firstUpdatedIndex !== -1) {\n // 有更新的节点\n positions.forEach((p, i) => {\n if (!newPositions[i]) newPositions[i] = p\n })\n // 从 `firstUpdatedIndex` 开始,更新后面的 `offset`\n const length = positions.length\n for (let i = firstUpdatedIndex; i < length; i++) {\n newPositions[i].offset = newPositions[i].height + (newPositions[i - 1]?.offset || 0)\n }\n const t2 = performance.now()\n console.log('更新缓存耗时: ', t2 - t1)\n setPositions(newPositions)\n }\n }, [itemRefs, positions, startIndex])\n\n return {\n totalHeight: positions[positions.length - 1]?.offset || 0,\n visibleData,\n offset: (positions[startIndex]?.offset || 0) - (positions[startIndex]?.height || 0),\n updatePositions,\n }\n}\n","// eslint-disabled\n\n// <= n 的最大的 2^x\nfunction power2le (n: number) {\n let sum = 1\n let tmp = 2\n while (tmp <= n) {\n sum = tmp\n tmp = sum << 1\n }\n return sum\n}\nclass BinaryIndexedTree {\n tree!: any[]\n bitMask!: number\n constructor(nums: number[]) {\n this.__init(nums)\n }\n // 初始化树状数组,\n // O(n)\n __init (nums: number[]) {\n this.tree = Array(nums.length + 1).fill(0)\n for (let i = 0; i < nums.length; i++) {\n this.tree[i + 1] = nums[i]\n }\n for (let i = 1; i < this.tree.length; i++) {\n let j = i + (i & -i)\n if (j < this.tree.length) {\n this.tree[j] += this.tree[i]\n }\n }\n this.bitMask = power2le(nums.length - 1)\n }\n // 更改第 i 项的, 1<=i 0) {\n sum += this.tree[n]\n n -= n & -n\n }\n return sum\n }\n // 计算 i ~ j 项的和,\n // 也可用来获取某个位置的实际值,不过建议使用 getValue 方法,效率更高\n // 2*O(log n)\n rangeSum (i: number, j: number | undefined) {\n return this.prefixSum(j) - this.prefixSum(i - 1)\n }\n // 获取第 i 项的实际值, 1<=i 0) {\n let z = i - (i & -i)\n i--\n while (i !== z) {\n sum -= this.tree[i]\n i -= (i & -i)\n }\n }\n return sum\n }\n // 找到一个n,其前n项和为 target\n // 要求数组非负,否则只能前n项迭代计算\n // 由于存在 0 的情况,满足条件的 n 有多个,返回其中任意一个\n // O(logn)\n find (target: number) {\n let idx = 0\n let len = this.tree.length\n let bitMask = this.bitMask\n while (bitMask !== 0 && (idx < len)) {\n let tIdx = idx + bitMask\n if (target === this.tree[tIdx]) {\n return tIdx\n } else if (target > this.tree[tIdx]) {\n idx = tIdx\n target -= this.tree[tIdx]\n }\n bitMask >>= 1\n }\n if (target !== 0) {\n return -1\n } else {\n return idx\n }\n }\n // 找到最大的一个n,其前n项和为 target\n // 要求数组非负,否则只能前n项迭代计算\n // O(logn)\n findG (target: number) {\n let idx = 0\n let len = this.tree.length\n let bitMask = this.bitMask\n while (bitMask !== 0 && (idx < len)) {\n let tIdx = idx + bitMask\n if (target >= this.tree[tIdx]) {\n idx = tIdx\n target -= this.tree[tIdx]\n }\n bitMask >>= 1\n }\n if (target !== 0) {\n return -1\n } else {\n return idx\n }\n }\n // 找到最小的一个n,其前n项和大于等于 target\n // O(logn)\n findGe (target: number) {\n let idx = 0\n let len = this.tree.length\n let bitMask = this.bitMask\n while (bitMask !== 0 && (idx < len)) {\n let tIdx = idx + bitMask\n if (target === this.tree[tIdx]) {\n return tIdx\n } else if (target > this.tree[tIdx]) {\n idx = tIdx\n target -= this.tree[tIdx]\n }\n bitMask >>= 1\n }\n return target === 0 ? idx : (\n idx + 1 < this.tree.length ? idx + 1 : -1\n )\n }\n}\nexport default BinaryIndexedTree","import React, { useCallback, useRef, useState, useEffect } from 'react'\nimport useBinaryIndexedTreeVirtualList from '../hooks/useBinaryIndexedTreeVirtualList'\n\ninterface IProps {\n data: T[];\n estimatedItemHeight: number\n itemRender: (item: T) => JSX.Element\n}\n\nfunction OptimizedReactiveHeight ({ data, estimatedItemHeight, itemRender }: IProps) {\n const [scrollTop, setScrollTop] = useState(0)\n const [clientHeight, setClientHeight] = useState(0)\n const itemRefs = useRef>([])\n\n const { totalHeight, visibleData, offset, updatePositions } = useBinaryIndexedTreeVirtualList({\n data,\n estimatedItemHeight,\n scrollTop,\n clientHeight,\n itemRefs,\n })\n\n const containerRef = useRef(null)\n\n const handleScroll = useCallback(() => {\n if (containerRef.current) {\n setScrollTop(containerRef.current.scrollTop)\n }\n }, [])\n\n const containerRefCallback = useCallback((node: HTMLDivElement) => {\n if (node) {\n containerRef.current = node\n setClientHeight(node.clientHeight)\n } else {\n containerRef.current = null\n }\n }, [])\n\n useEffect(() => {\n if (visibleData) {\n itemRefs.current = []\n }\n }, [visibleData])\n\n return (\n \n \n \n {visibleData.map((data, index) => (\n {\n itemRefs.current[index] = node\n if (visibleData.length === itemRefs.current.filter(Boolean).length) {\n updatePositions()\n }\n }}\n >{itemRender(data)}\n ))}\n \n \n )\n}\n\nexport default OptimizedReactiveHeight\n","import { useEffect, useMemo, useCallback, useRef } from 'react'\nimport BinaryIndexedTree from '../utils/BinaryIndexedTree'\n\ninterface IParams {\n data: T[]\n estimatedItemHeight: number\n scrollTop: number\n clientHeight: number\n itemRefs: React.MutableRefObject<(HTMLDivElement | null)[]>\n}\n\nexport default function useBinaryIndexedTreeVirtualList ({\n data,\n estimatedItemHeight,\n scrollTop,\n clientHeight,\n itemRefs,\n}: IParams) {\n const treeRef = useRef()\n\n // 初始化树状数组\n useEffect(() => {\n const initPositions: number[] = []\n const length = data.length\n for (let i = 0; i < length; i++) {\n initPositions[i] = estimatedItemHeight\n }\n treeRef.current = new BinaryIndexedTree(initPositions)\n }, [data.length, estimatedItemHeight])\n\n // 查找 `startIndex`\n const t1 = performance.now()\n const startIndex = (treeRef.current?.findGe(scrollTop) || 1) - 1\n const t2 = performance.now()\n console.log('查找 startIndex 耗时: ', t2 - t1)\n const endIndex = Math.ceil(clientHeight / estimatedItemHeight) + startIndex + 1\n const visibleData = useMemo(() => data.slice(startIndex, endIndex), [data, endIndex, startIndex])\n\n // 根据渲染的列表项,获取实际高度并更新树状数组\n const updatePositions = useCallback(() => {\n if (!itemRefs.current.length) return\n if (!data.length || startIndex === -1) return\n const t1 = performance.now()\n itemRefs.current.forEach((node, index) => {\n if (node) {\n const i = index + startIndex\n const realHeight = node.getBoundingClientRect().height\n const currentHeight = treeRef.current?.getValue(i + 1) || 0\n if (realHeight !== currentHeight) {\n treeRef.current?.update(i + 1, realHeight - currentHeight)\n }\n }\n })\n const t2 = performance.now()\n console.log('更新缓存耗时: ', t2 - t1)\n }, [data.length, itemRefs, startIndex])\n\n return {\n totalHeight: treeRef.current?.prefixSum(data.length) || 0,\n visibleData,\n offset: treeRef.current?.prefixSum(startIndex) || 0,\n updatePositions,\n }\n}\n","import React from 'react'\n\ninterface IProps {\n data: T[];\n itemHeight: number;\n itemRender: (item: T) => JSX.Element\n}\n\nfunction NormalList ({ data, itemHeight, itemRender }: IProps) {\n\n return (\n
\n
\n {data.map((d) => (\n
{itemRender(d)}
\n ))}\n
\n
\n )\n}\n\nexport default NormalList\n","export default function dataGen (amount = 200000) {\n const data = []\n for (let i = 0; i < amount; i++) {\n data.push({\n id: Math.random().toString(36).substr(2),\n index: i,\n value: i + 1,\n })\n }\n return data\n}\n","import React, { useCallback } from 'react'\nimport { Switch, Route, useHistory, useLocation, Redirect } from 'react-router-dom'\nimport Counter from './components/Counter'\nimport FixedHeight from './components/FixedHeight'\nimport PropHeight from './components/PropHeight'\nimport ReactiveHeight from './components/ReactiveHeight'\nimport OptimizedReactiveHeight from './components/OptimizedReactiveHeight'\nimport NormalList from './components/NormalList'\nimport dataGen from './utils/data-generator'\n\nconst data = dataGen()\n\nconst normalListData = dataGen(50000)\n\nconst radioButtons = [\n {\n path: '/fixed-height',\n text: '定高',\n },\n {\n path: '/prop-height',\n text: '不定高(二分),'\n },\n {\n path: '/reactive-height',\n text: '自适应(二分)',\n },\n {\n path: '/optimized-reactive-height',\n text: '自适应(树状数组)',\n },\n {\n path: '/normal-list',\n text: '普通列表',\n },\n {\n path: '/fixed-height-with-counter',\n text: '带计数器',\n },\n]\n\nexport default function App () {\n const history = useHistory()\n const location = useLocation()\n\n const itemRender = useCallback((item) => {\n return (\n
{item.value}
\n )\n }, [])\n\n const itemRenderWithCounter = useCallback((item) => {\n return (\n
{item.value} |
\n )\n }, [])\n\n const reactiveHeightItemRender = useCallback((item) => {\n return (\n
{item.value}
\n )\n }, [])\n\n const getItemHeight = useCallback((index: number) => {\n return 50 + (index % 5) * 10\n }, [])\n\n return (\n <>\n
\n {\n radioButtons.map((radio) => {\n return (\n \n )\n })\n }\n
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n )\n}\n","// This optional code is used to register a service worker.\n// register() is not called by default.\n\n// This lets the app load faster on subsequent visits in production, and gives\n// it offline capabilities. However, it also means that developers (and users)\n// will only see deployed updates on subsequent visits to a page, after all the\n// existing tabs open on the page have been closed, since previously cached\n// resources are updated in the background.\n\n// To learn more about the benefits of this model and instructions on how to\n// opt-in, read https://bit.ly/CRA-PWA\n\nconst isLocalhost = Boolean(\n window.location.hostname === 'localhost' ||\n // [::1] is the IPv6 localhost address.\n window.location.hostname === '[::1]' ||\n // 127.0.0.0/8 are considered localhost for IPv4.\n window.location.hostname.match(\n /^127(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/\n )\n);\n\ntype Config = {\n onSuccess?: (registration: ServiceWorkerRegistration) => void;\n onUpdate?: (registration: ServiceWorkerRegistration) => void;\n};\n\nexport function register(config?: Config) {\n if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {\n // The URL constructor is available in all browsers that support SW.\n const publicUrl = new URL(\n process.env.PUBLIC_URL,\n window.location.href\n );\n if (publicUrl.origin !== window.location.origin) {\n // Our service worker won't work if PUBLIC_URL is on a different origin\n // from what our page is served on. This might happen if a CDN is used to\n // serve assets; see https://github.com/facebook/create-react-app/issues/2374\n return;\n }\n\n window.addEventListener('load', () => {\n const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;\n\n if (isLocalhost) {\n // This is running on localhost. Let's check if a service worker still exists or not.\n checkValidServiceWorker(swUrl, config);\n\n // Add some additional logging to localhost, pointing developers to the\n // service worker/PWA documentation.\n navigator.serviceWorker.ready.then(() => {\n console.log(\n 'This web app is being served cache-first by a service ' +\n 'worker. To learn more, visit https://bit.ly/CRA-PWA'\n );\n });\n } else {\n // Is not localhost. Just register service worker\n registerValidSW(swUrl, config);\n }\n });\n }\n}\n\nfunction registerValidSW(swUrl: string, config?: Config) {\n navigator.serviceWorker\n .register(swUrl)\n .then(registration => {\n registration.onupdatefound = () => {\n const installingWorker = registration.installing;\n if (installingWorker == null) {\n return;\n }\n installingWorker.onstatechange = () => {\n if (installingWorker.state === 'installed') {\n if (navigator.serviceWorker.controller) {\n // At this point, the updated precached content has been fetched,\n // but the previous service worker will still serve the older\n // content until all client tabs are closed.\n console.log(\n 'New content is available and will be used when all ' +\n 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'\n );\n\n // Execute callback\n if (config && config.onUpdate) {\n config.onUpdate(registration);\n }\n } else {\n // At this point, everything has been precached.\n // It's the perfect time to display a\n // \"Content is cached for offline use.\" message.\n console.log('Content is cached for offline use.');\n\n // Execute callback\n if (config && config.onSuccess) {\n config.onSuccess(registration);\n }\n }\n }\n };\n };\n })\n .catch(error => {\n console.error('Error during service worker registration:', error);\n });\n}\n\nfunction checkValidServiceWorker(swUrl: string, config?: Config) {\n // Check if the service worker can be found. If it can't reload the page.\n fetch(swUrl, {\n headers: { 'Service-Worker': 'script' }\n })\n .then(response => {\n // Ensure service worker exists, and that we really are getting a JS file.\n const contentType = response.headers.get('content-type');\n if (\n response.status === 404 ||\n (contentType != null && contentType.indexOf('javascript') === -1)\n ) {\n // No service worker found. Probably a different app. Reload the page.\n navigator.serviceWorker.ready.then(registration => {\n registration.unregister().then(() => {\n window.location.reload();\n });\n });\n } else {\n // Service worker found. Proceed as normal.\n registerValidSW(swUrl, config);\n }\n })\n .catch(() => {\n console.log(\n 'No internet connection found. App is running in offline mode.'\n );\n });\n}\n\nexport function unregister() {\n if ('serviceWorker' in navigator) {\n navigator.serviceWorker.ready\n .then(registration => {\n registration.unregister();\n })\n .catch(error => {\n console.error(error.message);\n });\n }\n}\n","import React from 'react';\nimport ReactDOM from 'react-dom';\nimport './index.css';\nimport App from './App';\nimport * as serviceWorker from './serviceWorker';\nimport { BrowserRouter } from 'react-router-dom';\n\nReactDOM.render(\n \n \n \n \n ,\n document.getElementById('root')\n);\n\n// If you want your app to work offline and load faster, you can change\n// unregister() to register() below. Note this comes with some pitfalls.\n// Learn more about service workers: https://bit.ly/CRA-PWA\nserviceWorker.unregister();\n"],"sourceRoot":""} --------------------------------------------------------------------------------