npm init -y
npm install --save react react-dom react-router-dom lodash bootstrap is-array reselect redux
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/core @babel/preset-env @babel/preset-react style-loader css-loader postcss-loader @babel/plugin-syntax-class-properties mini-css-extract-plugin
包 | 作用 |
---|---|
react | React 是一个用于构建用户界面的 JavaScript 库 |
react-dom | 使 React 能够与 DOM 交互 |
lodash | 一个 JavaScript 实用程序库,提供了很多有用的工具函数 |
bootstrap | 一种流行的前端框架,用于开发响应式网站和应用程序 |
is-array | 一个小的实用函数,用于检查一个值是否是数组 |
reselect | 用于创建可记忆的、可组合的 selector 函数 |
redux | 一个用于管理应用状态的 JavaScript 库 |
开发依赖包 | 作用 |
---|---|
webpack | 用于打包 JavaScript 文件、CSS、图片等资源的模块打包器 |
webpack-cli | 用于在命令行中运行 webpack |
webpack-dev-server | 用于快速开发应用程序,提供了一个开发服务器和实时重加载功能 |
html-webpack-plugin | 简化了 HTML 文件的创建,以便为你的 webpack 包提供服务 |
babel-loader | 用于让 webpack 知道如何运行 babel |
@babel/core | Babel 的核心包,用于转译 ES6+ 代码 |
@babel/preset-env | Babel 预设,用于转译 ES6+ 代码 |
@babel/preset-react | Babel 预设,用于转译 React 代码 |
style-loader | 将 CSS 插入到 DOM 中 |
css-loader | 解析 CSS 文件 |
postcss-loader | 使用 PostCSS 处理 CSS 文件 |
@babel/plugin-syntax-class-properties | 允许 Babel 解析 class 属性 |
mini-css-extract-plugin | 从 JavaScript 文件中提取 CSS |
// 引入必要的模块
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const bootstrap = path.resolve('node_modules/bootstrap/dist/css/bootstrap.css');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
// 导出一个函数,该函数接受两个参数:development 和 production
module.exports = ({ development, production }) => {
const isEnvDevelopment = development === 'development';
const isEnvProduction = production === 'production';
// 定义一个函数,该函数接受一个参数 cssOptions,并返回一个 loader 数组
const getStyleLoaders = (cssOptions) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && MiniCssExtractPlugin.loader,
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
'postcss-loader',
].filter(Boolean);
return loaders;
};
// 返回一个 webpack 配置对象
return {
mode: isEnvProduction ? 'production' : isEnvDevelopment ? 'development' : 'development',
devtool: isEnvProduction
? shouldUseSourceMap
? 'source-map'
: false
: isEnvDevelopment && 'cheap-module-source-map',
cache: {
type: 'filesystem'
},
entry: {
main: './src/index.js'
},
optimization: {
minimize: isEnvProduction,
minimizer: [
new TerserPlugin({ parallel: true })
],
splitChunks: {
chunks: 'all',
minSize: 0,
minRemainingSize: 0,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
},
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`,
},
moduleIds: isEnvProduction ? 'deterministic' : 'named',
chunkIds: isEnvProduction ? 'deterministic' : 'named'
},
resolve: {
modules: [path.resolve('node_modules')],
extensions: ['.js'],
alias: {
bootstrap
},
fallback: {
crypto: false,
buffer: false,
stream: false
}
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: [
"@babel/preset-react"
]
}
}
],
include: path.resolve('src'),
exclude: /node_modules/
},
{
test: /\.css$/,
use: getStyleLoaders({ importLoaders: 1 })
}
]
},
devServer: {},
plugins: [
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: './public/index.html'
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
)
]
}
}
src\index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
ReactDOM.createRoot(document.getElementById('root')).render(
"root"
);
public\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>react</title>
</head>
<body>
</body>
</html>
package.json
{
"scripts": {
"build": "webpack --env=production",
"dev": "webpack serve --env=development"
},
}
mode
是 webpack 的一个重要配置选项,它用于设置 webpack 的运行模式。
在 webpack 5 中,mode
有三个可能的值:
development: 这是用于开发环境的模式。当 mode
设置为 development
时,webpack 会优化构建速度和调试友好性,而不是生成的文件的大小。具体来说,这会启用以下一些 webpack 内置的插件和设置:
NamedChunksPlugin
: 用于生成更易读的 chunk 名字。NamedModulesPlugin
: 用于生成更易读的模块名字。production: 这是用于生产环境的模式。当 mode
设置为 production
时,webpack 会尽可能地优化构建的输出结果,包括压缩代码、删除未使用的代码、优化模块等。具体来说,这会启用以下一些 webpack 内置的插件和设置:
FlagDependencyUsagePlugin
: 用于标记模块的依赖关系,以便 tree shaking 可以更好地移除未使用的代码。FlagIncludedChunksPlugin
: 用于标记包含其他模块的 chunk,以减少总体大小。ModuleConcatenationPlugin
: 用于优化模块,使浏览器能够更快地运行代码。NoEmitOnErrorsPlugin
: 用于在编译出错时跳过输出阶段,以确保输出资源不包含错误。OccurrenceOrderPlugin
: 用于对模块和 chunk 的 id 进行排序,以减小总体大小。SideEffectsFlagPlugin
: 用于标记模块是否有副作用,以便 tree shaking 可以更好地移除未使用的代码。TerserPlugin
: 用于压缩 JavaScript 代码。none: 不启用任何优化。这可以用于测试或者特殊的情况,通常不推荐在实际项目中使用。
通常,在开发时设置 mode
为 development
,并在构建用于生产环境的版本时设置 mode
为 production
。这可以确保你在开发时得到最快的构建速度和最好的调试体验,并在生产环境中得到最小的文件大小和最高的性能。
devtool
配置选项用于控制如何生成 source map。
在 webpack 5 中,devtool
的值可以是以下几种:
false
: 不生成 source map。eval
: 每个模块用 eval
执行,且 source map 转为 DataUrl 添加到 eval
中。source-map
: 生成完整的 source map,且生成单独的 source map 文件。inline-source-map
: 生成完整的 source map,但是将 source map 转为 DataUrl 嵌入到 bundle 文件中。cheap-source-map
: 生成没有列信息(column-mappings)的 source map,且生成单独的 source map 文件。cheap-module-source-map
: 生成包含 loader 的 source maps 的 source map,且生成单独的 source map 文件。eval-source-map
: 每个模块用 eval
执行,且生成完整的 source map。eval-cheap-source-map
: 每个模块用 eval
执行,且生成没有列信息(column-mappings)的 source map。eval-cheap-module-source-map
: 每个模块用 eval
执行,且生成包含 loader 的 source maps 的 source map。inline-cheap-source-map
: 生成没有列信息(column-mappings)的 source map,但是将 source map 转为 DataUrl 嵌入到 bundle 文件中。inline-cheap-module-source-map
: 生成包含 loader 的 source maps 的 source map,但是将 source map 转为 DataUrl 嵌入到 bundle 文件中。cheap-module-eval-source-map
: 每个模块用 eval
执行,且生成包含 loader 的 source maps 的 source map。以下是一些推荐的配置:
eval-source-map
或 eval-cheap-module-source-map
source-map
或 cheap-module-source-map
根据你的需要,你可以选择最适合你的 devtool
配置。例如,如果你不需要列信息,可以使用 cheap-source-map
。如果你想将 source map 嵌入到 bundle 文件中,可以使用 inline-source-map
。
在 webpack 配置中,cache
选项用于配置缓存方式,以提高构建速度。
在你提供的代码中:
cache: {
type: 'filesystem'
},
这段代码表示使用文件系统(filesystem
)缓存。webpack 会将构建生成的中间文件缓存到文件系统中,这样在下次构建时,只需要重新构建更改了的部分,从而大大减少了构建时间。
webpack 提供了两种缓存方式:
memory
: 缓存在内存中。这是默认的缓存方式,但是一旦你关闭了 webpack 进程,缓存就会消失。
filesystem
: 缓存在文件系统中。这意味着即使你关闭了 webpack 进程,缓存也会保留,下次重新启动 webpack 时,会从缓存中读取数据。
因此,通过设置 cache.type
为 filesystem
,你可以在多次构建之间保留缓存,从而提高构建速度。
optimization
是 webpack 的一个配置项,用于控制优化过程的不同方面。这里是 webpack 5 中 optimization
的一些子属性的说明:
minimize: boolean
类型。告诉 webpack 是否需要压缩输出的代码。默认情况下,在生产模式(production mode)中这个值为 true
,而在开发模式(development mode)中这个值为 false
。
minimizer: array
类型。这个数组定义了用于压缩代码的插件。默认情况下,webpack 使用 TerserPlugin
来压缩 JavaScript 代码。
splitChunks: 这个对象控制了代码分割的行为。代码分割可以帮助你将代码分解成多个 bundle,这样可以更好地缓存代码,从而提高应用程序的加载速度。
例如:
splitChunks: {
chunks: 'all',
minSize: 0,
minRemainingSize: 0,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
},
chunks
: 定义了哪些 chunk 会被优化。它可以是以下三个值之一: initial
, async
, all
。minSize
: 生成 chunk 的最小体积(以字节为单位)。默认值是 20000(20KB)。minRemainingSize
: 分割前和分割后的最小剩余大小,以确保剩余的部分没有比 minSize
更小的。maxSize
: 生成 chunk 的最大体积(以字节为单位)。minChunks
: 分割一个模块前,这个模块至少需要在多少个 chunk 中被引用。maxAsyncRequests
: 按需加载的 chunk 最大的并行请求数。maxInitialRequests
: 一个入口点的最大并行请求数。enforceSizeThreshold
: 忽略 maxSize
,对于符合条件的 chunk,总是返回 true
或 false
。cacheGroups
: 一个对象,它的键是缓存组的名称,值是一个对象,该对象定义了缓存组的行为。runtimeChunk: 这个选项允许你将运行时代码(runtime code)提取到一个单独的 chunk 中。这个 chunk 包含了所有的运行时代码,可以在生成多个 chunk 时,用于加载其他的 chunk。
moduleIds 和 chunkIds: 这两个选项用于控制 module
和 chunk
的 id 的生成。它们可以是以下四个值之一: natural
, named
, deterministic
, size
.
natural
: 使用自然数作为 id。named
: 使用模块的路径作为 id。deterministic
: 生成一个短的、数字的 id。size
: 根据模块的大小生成 id。在 webpack 5 中,resolve
对象用于配置模块解析方式。
resolve
对象的属性包括:
["node_modules"]
,意味着 webpack 会在当前目录的 node_modules
目录中查找模块。modules: [path.resolve('node_modules')]
extensions: ['.js']
这意味着当你导入模块时,如果你没有指定扩展名,webpack 会尝试将 .js
添加到模块名后面,然后查找该文件。
alias: {
bootstrap
}
在这个示例中,bootstrap
被映射到了 node_modules/bootstrap/dist/css/bootstrap.css
。
node
选项。fallback: {
crypto: false,
buffer: false,
stream: false
}
在这个示例中,我们设置了 crypto
、buffer
和 stream
的值为 false
,这意味着 webpack 不会为这些模块提供任何替代。这是因为我们知道我们的代码不会在 Node.js 环境中运行,所以我们不需要这些模块。
以上就是 resolve
对象的一些常用属性,它们可以用来定制 webpack 的模块解析逻辑。
在 webpack 5 的配置中,module
是一个对象,它用于定义如何处理项目中的不同类型的模块。
module
对象通常包含以下属性:
rules (必须): 这是一个数组,包含一系列规则。每个规则可以包含以下条件属性:
test
: 一个正则表达式,用于测试文件是否应该被 loader 处理。例如,/\.js$/
会匹配所有 .js
文件。include
: 一个条件,指示应该检查哪些文件。exclude
: 一个条件,指示应该排除哪些文件。use
: 指定应该对匹配文件使用哪些 loader。这可以是一个 loader 字符串,也可以是一个对象,包含 loader
和 options
属性。loader
和 options
: 和 use
类似,但只能用于一个 loader。
示例:
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
这个例子中的 rules
配置表示:对所有 .js
文件(除了 node_modules
目录下的文件)使用 babel-loader
,并为 babel-loader
提供 @babel/preset-env
预设。
noParse: 一个正则表达式,或者一个正则表达式数组,用于排除一些文件,使这些文件不被模块解析器解析。
在 webpack 5 配置中,devServer
是一个对象,用于配置 webpack-dev-server 的行为。
webpack-dev-server 是一个小型的 Node.js Express 服务器,用于提供通过 webpack 构建的资源、进行热模块更新和页面重载等功能。
devServer
对象可以包含以下属性:
contentBase: 类型:string | array。告诉服务器从哪个目录中提供内容。默认情况下,将使用当前工作目录作为提供内容的目录,但是你可以修改为其他目录。
compress: 类型:boolean。是否启用 gzip 压缩。
port: 类型:number。指定要监听请求的端口号。
hot: 类型:boolean。是否启用模块热替换功能。
open: 类型:boolean。是否自动打开浏览器。
historyApiFallback: 类型:boolean | object。当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html
。
proxy: 类型:object。用于将某些 API 请求代理到另一个服务器。
publicPath: 类型:string。此路径下的打包文件可在浏览器中访问。
overlay: 类型:boolean | object。当存在编译器错误或警告时,在浏览器中显示全屏覆盖。
示例:
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9000,
open: true,
hot: true,
historyApiFallback: true,
proxy: {
'/api': 'http://localhost:3000'
},
publicPath: '/',
overlay: true
}
html-webpack-plugin
是一个 webpack 的插件,用于简化 HTML 文件的创建,该文件将包含 webpack 打包生成的所有 bundle。
该插件会在输出目录中生成一个 HTML 文件,并在该文件中引入 webpack 打包生成的所有 JavaScript 和 CSS 文件。
你可以通过配置 html-webpack-plugin
来控制生成的 HTML 文件的内容。
在你的 webpack 配置文件中,你需要先 require
html-webpack-plugin
,然后在 plugins
数组中创建 html-webpack-plugin
的一个实例。
例如:
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// ... 其他配置
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
}
在这个例子中,template
选项表示使用 ./src/index.html
文件作为模板来生成最终的 HTML 文件。
html-webpack-plugin
插件有很多可配置的选项:
title: 类型:string。生成的 HTML 文件的标题。
filename: 类型:string。输出的 HTML 文件的文件名。
template: 类型:string。模板文件的路径。
inject: 类型:boolean | string。注入选项。有四个选项值:true
、false
、'head'
、'body'
。true
或 'body'
表示所有 JavaScript 资源将被放置在 <body>
元素的底部。'head'
表示将 JavaScript 资源放置在 <head>
元素中。false
不会注入 js 文件。
favicon: 类型:string。favicon 路径。
meta: 类型:object。添加到 HTML <head>
中的 meta 标签。
base: 类型:string | object。添加到 HTML <head>
中的 base 标签。
minify: 类型:boolean | object。控制是否最小化输出的 HTML。
hash: 类型:boolean。如果为 true,则将唯一的 webpack 编译哈希值附加到所有包含的脚本和 CSS 文件。这对于清除缓存很有用。
showErrors: 类型:boolean。是否将错误详细信息写入 HTML 页面。
chunks: 类型:array。指定要包含的块。
excludeChunks: 类型:array。指定要排除的块。
import React, { lazy, Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
const Home = lazy(() => import('./components/Home'));
const User = lazy(() => import('./components/User'));
const Loading = () => <div>Loading</div>;
ReactDOM.createRoot(document.getElementById('root')).render(
<Router>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/user">User</Link></li>
</ul>
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/user" element={<User />} />
</Routes>
</Suspense>
</Router>
);
src\components\User.js
import React from 'react';
function Home(){
return <div>Home</div>
}
export default Home;
src\components\Home.js
import React from 'react';
function Home(){
return <div>Home</div>
}
export default Home;
src\index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import PureComponent from './PureComponent';
ReactDOM.createRoot(document.getElementById('root')).render(
<PureComponent/>
);
src\PureComponent.js
import React from 'react';
import { PureComponent, memo } from './utils';
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = { title: '计数器', number: 0 }
}
add = (amount) => {
this.setState({ number: this.state.number + amount });
}
render() {
console.log('App render');
return (
<div>
<Counter number={this.state.number} />
<button onClick={() => this.add(1)}>+1</button>
<button onClick={() => this.add(0)}>+0</button>
<ClassTitle title={this.state.title} />
<FunctionTitle title={this.state.title} />
</div>
)
}
}
class Counter extends PureComponent {
render() {
console.log('Counter render');
return (
<p>{this.props.number}</p>
)
}
}
class ClassTitle extends PureComponent {
render() {
console.log('ClassTitle render');
return (
<p>{this.props.title}</p>
)
}
}
const FunctionTitle = memo(props => {
console.log('FunctionTitle render');
return <p>{props.title}</p>;
});
src\utils.js
import React from 'react';
export class PureComponent extends React.Component{
shouldComponentUpdate(nextProps,nextState){
return !shallowEqual(this.props,nextProps)||!shallowEqual(this.state,nextState)
}
}
export function memo(OldComponent){
return class extends PureComponent{
render(){
return <OldComponent {...this.props}/>
}
}
}
export function shallowEqual(obj1,obj2){
if(obj1 === obj2)
return true;
if(typeof obj1 !== 'object' || obj1 ===null || typeof obj2 !== 'object' || obj2 ===null){
return false;
}
let keys1 = Object.keys(obj1);
let keys2 = Object.keys(obj2);
if(keys1.length !== keys2.length){
return false;
}
for(let key of keys1){
if(!obj2.hasOwnProperty(key) || obj1[key]!== obj2[key]){
return false;
}
}
return true;
}
Immutable.js 是一个 JavaScript 库,它提供了一系列的不可变数据结构。在 JavaScript 中,对象和数组是可变的,这意味着你可以改变对象的属性或数组的元素。但是,这有时会导致不可预见的副作用,特别是在大型应用程序或者多人合作的项目中。
不可变数据结构是一旦创建,就不能再被更改的数据结构。如果你想改变一个不可变对象,你必须创建一个新的对象。
这有几个好处:
Immutable.js 提供了几个不可变数据结构,例如 List
、Map
、Set
、OrderedMap
、Stack
等。这些数据结构类似于 JavaScript 的原生数组和对象,但它们是不可变的。
下面是一些基本示例:
const { Map, List } = require('immutable');
// 创建一个不可变的 map
const map1 = Map({ a: 1, b: 2, c: 3 });
// 创建一个新的 map,而不是修改原始 map
const map2 = map1.set('b', 50);
console.log(map1.get('b')); // 2
console.log(map2.get('b')); // 50
// 创建一个不可变的 list
const list1 = List([1, 2, 3]);
// 创建一个新的 list,而不是修改原始 list
const list2 = list1.push(4);
console.log(list1.size); // 3
console.log(list2.size); // 4
在这个例子中,map1
和 list1
是不可变的,因此当我们想要添加一个新的元素或更改一个现有的元素时,我们不会更改原始数据结构,而是创建一个新的数据结构。
注意,由于 Immutable.js 数据结构是不可变的,所以它们的方法不会更改原始数据结构,而是返回一个新的数据结构。这是一个重要的区别,与 JavaScript 的原生数组和对象的方法不同,这些方法通常会更改原始数据结构。
src\PureComponent.js
import React from 'react';
import { PureComponent, memo } from './utils';
+import { Map } from "immutable";
export default class App extends React.Component {
constructor(props) {
super(props);
+ this.state = {count:Map({ number: 0 })}
}
add = (amount) => {
+ let count = this.state.count.set('number',this.state.count.get('number') + amount);
+ this.setState({count});
}
render() {
console.log('App render');
return (
<div>
+ <Counter number={this.state.count.get('number')}/>
<button onClick={() => this.add(1)}>+1</button>
<button onClick={() => this.add(0)}>+0</button>
<ClassTitle title={this.state.title} />
<FunctionTitle title={this.state.title} />
</div>
)
}
}
class Counter extends PureComponent {
render() {
console.log('Counter render');
return (
<p>{this.props.number}</p>
)
}
}
class ClassTitle extends PureComponent {
render() {
console.log('ClassTitle render');
return (
<p>{this.props.title}</p>
)
}
}
const FunctionTitle = memo(props => {
console.log('FunctionTitle render');
return <p>{props.title}</p>;
});
src\utils.js
import React from 'react';
+import { is } from "immutable";
export class PureComponent extends React.Component{
shouldComponentUpdate(nextProps,nextState){
return !shallowEqual(this.props,nextProps)||!shallowEqual(this.state,nextState)
}
}
export function memo(OldComponent){
return class extends PureComponent{
render(){
return <OldComponent {...this.props}/>
}
}
}
export function shallowEqual(obj1,obj2){
if(obj1 === obj2)
return true;
if(typeof obj1 !== 'object' || obj1 ===null || typeof obj2 !== 'object' || obj2 ===null){
return false;
}
let keys1 = Object.keys(obj1);
let keys2 = Object.keys(obj2);
if(keys1.length !== keys2.length){
return false;
}
for(let key of keys1){
+ if (!obj2.hasOwnProperty(key) || !is(obj1[key],obj2[key])) {
return false;
}
}
return true;
}
reselect
是一个简单的 selector
库,用于构建可记忆的、可组合的 selector 函数。reselect
通常与 Redux
一起使用,但它不是 Redux
专用的,可以在任何应用程序中使用。
在 Redux
应用程序中,selector
函数用于从 Redux
存储中检索特定的数据片段。selector
是一个接受整个 Redux
存储树并返回所需数据的函数。
reselect
提供了一种创建可记忆的 selector
的方法。reselect
的 createSelector
函数会记住参数,只有当参数发生变化时才重新计算结果。这可以提高性能,因为如果参数没有变化,selector
将直接返回上一次的计算结果,而不是重新计算。
下面是一个简单的例子:
// 引入 reselect 库
import { createSelector } from 'reselect';
// 引入 redux 库
import { legacy_createStore as createStore, combineReducers } from 'redux';
// 初始化待办事项的id
let nextTodoId = 0;
// 定义添加待办事项的action
const addTodo = text => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
});
// 定义设置可见性过滤器的action
const setVisibilityFilter = filter => ({
type: 'SET_VISIBILITY_FILTER',
filter
});
// 定义可见性过滤器的常量
const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE'
};
// 解构可见性过滤器的常量
const {
SHOW_ALL
} = VisibilityFilters;
// 定义可见性过滤器的reducer
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter;
default:
return state;
}
}
// 定义待办事项的reducer
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, {
text: action.text,
completed: false
}];
default:
return state;
}
}
// 使用 combineReducers 合并 reducers
const todoApp = combineReducers({
visibilityFilter,
todos
});
// 创建 Redux store
const store = createStore(todoApp);
// 打印初始状态
console.log(store.getState());
// 订阅状态变化
const unsubscribe = store.subscribe(() => console.log(store.getState()));
// 分发一些 actions
store.dispatch(addTodo('Learn about actions'));
store.dispatch(addTodo('Learn about reducers'));
store.dispatch(addTodo('Learn about store'));
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED));
// 停止订阅状态变化
unsubscribe();
// 定义输入选择器
const getVisibilityFilter = state => state.visibilityFilter;
const getTodos = state => state.todos;
// 定义一个可记忆的 selector
const getVisibleTodos = createSelector(
[getVisibilityFilter, getTodos],
(visibilityFilter, todos) => {
switch (visibilityFilter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
default:
throw new Error('Unknown filter: ' + visibilityFilter);
}
}
);
// 定义 mapStateToProps 函数
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state)
};
};
// 打印 mapStateToProps 函数的返回值
console.log(mapStateToProps(store.getState()));
在这个例子中,getVisibilityFilter
和 getTodos
是输入选择器。它们都是简单的函数,接受整个 Redux
存储树并返回所需的数据。
getVisibleTodos
是我们使用 reselect
的 createSelector
函数创建的可记忆 selector
。这个 selector
接受 getVisibilityFilter
和 getTodos
作为输入选择器,并返回一个函数,该函数计算并返回我们需要的数据。
mapStateToProps
函数使用 getVisibleTodos
selector
从 Redux
存储中检索可见的待办事项。每次存储更改时,mapStateToProps
函数都会被调用,但是只有当 getVisibilityFilter
或 getTodos
返回的值发生变化时,getVisibleTodos
selector
才会重新计算结果。
这样可以提高性能,因为如果 visibilityFilter
和 todos
没有变化,getVisibleTodos
selector
将直接返回上一次的计算结果,而不是重新计算。
src\Home.js
import React from 'react';
export default class Home extends React.Component {
state = {
list: []
}
handleClick = () => {
let starTime = new Date().getTime();
this.setState({
list: new Array(30000).fill(0)
}, () => {
const end = new Date().getTime()
console.log((end - starTime) / 1000 + '秒')
})
}
render() {
return (
<ul>
<button onClick={this.handleClick}>点击</button>
{
this.state.list.map((item, index) => (
<li key={index} >{index}</li>
))
}
</ul>
)
}
}
src\Home.js
import React from 'react';
export default class Home extends React.Component {
state = {
list: []
}
+ handleClick = () => {
+ this.timeSlice(550);
+ }
+ timeSlice = (times) => {
+ //requestIdleCallback
+ requestAnimationFrame(() => {
+ let minus = times >= 100 ? 100 : times;
+ times -= minus;
+ this.setState({
+ list: [...this.state.list, ...new Array(minus).fill(0)]
+ }, () => {
+ if (times > 0) {
+ this.timeSlice(times);
+ }
+ });
+ });
+ }
render() {
return (
<ul>
<button onClick={this.handleClick}>点击</button>
{
this.state.list.map((item, index) => (
<li key={index} >{index + 1}</li>
))
}
</ul>
)
}
}
requestAnimationFrame
是一个浏览器提供的API,用于在下一次重绘之前请求浏览器调用指定的函数更新动画。这个API是非常重要的,因为它允许你创建更流畅、更高效的动画。
在浏览器中,屏幕通常每秒刷新60次,也就是每16.7毫秒刷新一次。requestAnimationFrame
允许你在每次重绘之前执行一些代码,这样你可以更精确地控制动画的帧率。
使用方法
使用requestAnimationFrame
通常包含以下步骤:
window.requestAnimationFrame()
将这个函数注册到浏览器。示例
下面是一个使用requestAnimationFrame
的简单示例:
// 定义一个函数,这个函数将在下一次重绘之前被浏览器调用
function updateAnimation(timestamp) {
// 更新动画的状态
// ...
// 如果动画还没有完成,再次注册这个函数
requestAnimationFrame(updateAnimation);
}
// 注册这个函数
requestAnimationFrame(updateAnimation);
在这个示例中,updateAnimation
函数将在每次重绘之前被浏览器调用,你可以在这个函数中更新动画的状态。如果动画还没有完成,可以在函数的末尾再次注册updateAnimation
函数。
注意事项
requestAnimationFrame
被所有现代浏览器支持,但在一些旧的浏览器中可能需要使用前缀,比如webkitRequestAnimationFrame
。requestAnimationFrame
比setTimeout
和setInterval
更高效,因为它是专门为动画设计的。它会尽可能地与浏览器的刷新频率保持同步,从而获得最流畅的动画效果。requestAnimationFrame
回调将不会被触发,直到页面变得可见时才会继续。requestAnimationFrame
会给你的函数传递一个时间戳参数,这个时间戳表示从页面加载到当前的时间。总的来说,requestAnimationFrame
是一个非常重要和强大的API,它允许你创建流畅、高效、低功耗的动画。
requestIdleCallback
是一个浏览器提供的API,允许你在浏览器的空闲时段内执行一些低优先级的任务。这个API是非常有用的,因为它允许你不影响帧率、用户交互等高优先级任务的情况下执行一些后台任务。
浏览器的主线程通常非常繁忙,需要处理用户事件、渲染、布局、JavaScript执行等等。如果你在主线程中执行一些耗时的任务,可能会导致页面卡顿或不响应。requestIdleCallback
可以帮助你更智能地安排这些任务。
使用方法
使用requestIdleCallback
通常包含以下步骤:
window.requestIdleCallback()
将这个函数注册到浏览器。IdleDeadline
对象给你的函数,你可以使用这个对象的timeRemaining
方法来获取浏览器还有多少空闲时间。cancelIdleCallback
取消它。示例
下面是一个使用requestIdleCallback
的简单示例:
// 定义一个函数,这个函数将在浏览器空闲时被调用
function doSomeHeavyWork(deadline) {
while (deadline.timeRemaining() > 0) {
// 执行一些耗时的任务
}
}
// 注册这个函数
var id = requestIdleCallback(doSomeHeavyWork);
// 如果你想取消这个函数,可以这样做
cancelIdleCallback(id);
注意事项
requestIdleCallback
。在使用之前,你需要检查浏览器的兼容性或使用polyfill。requestIdleCallback
适合一些不需要立即执行、可以在空闲时执行的任务,比如统计、日志、本地存储等。requestIdleCallback
。对于动画,应该使用requestAnimationFrame
。总的来说,requestIdleCallback
是一个非常有用的API,可以帮助你更智能地安排一些后台任务,提升页面的性能和响应速度。
在React中,虚拟列表(Virtual List)或窗口化列表(Windowing)是一种优化技术,用于提高大列表或表格的渲染性能。
问题背景
在React中,如果你需要渲染一个包含大量元素的列表,通常会直接将所有的元素一次性渲染到DOM中。然而,这样做可能会导致一些性能问题:
解决方案:虚拟列表
虚拟列表是一种解决这些问题的优化技术。它的基本思想是:只渲染当前可见的元素,而不是一次性渲染所有的元素。
虚拟列表通常包括以下几个步骤:
通过这种方式,虚拟列表可以极大地提高列表的渲染性能,减少内存的占用,并提高滚动的流畅度。
实现
虚拟列表可以手动实现,但通常推荐使用一些成熟的库,比如react-window
或react-virtualized
。
这里是一个使用react-window
的简单示例:
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
);
const VirtualList = () => (
<List
height={150}
itemCount={1000}
itemSize={35}
width={300}
>
{Row}
</List>
);
export default VirtualList;
在这个示例中,我们首先导入react-window
库的FixedSizeList
组件。然后,我们创建一个Row
组件,它接收index
和style
参数,并渲染一个列表项。
接下来,我们创建VirtualList
组件,它使用FixedSizeList
组件并传递一些参数:
height
:列表的高度itemCount
:列表的总项数itemSize
:每一项的高度width
:列表的宽度然后,我们将Row
组件作为子组件传递给FixedSizeList
。
通过这种方式,react-window
会自动计算当前可见的元素,并只渲染这些元素。
总结
虚拟列表是一种非常重要的优化技术,它可以极大地提高大列表的渲染性能,减少内存的占用,并提高滚动的流畅度。react-window
和react-virtualized
是两个非常成熟的库,可以帮助你更容易地实现虚拟列表。
import React from 'react';
import ReactDOM from 'react-dom/client';
import VirtualList from './components/VirtualList';
const data = new Array(30).fill(0);
ReactDOM.createRoot(document.getElementById('root')).render(
<VirtualList
width='50%'
height={500}
itemCount={data.length}
itemSize={50}
renderItem={(data) => {
let { index, item, style } = data;
console.log(data);
return (
<div key={index} style={{ ...style, backgroundColor: index % 2 === 0 ? 'green' : 'orange' }}>
{index+1}
</div>
)
}
}
/>
);
src\components\VirtualList.js
import React from 'react';
export default class Index extends React.Component {
scrollBox = React.createRef()
state = { start: 0 }
handleScroll = () => {
const { itemSize } = this.props;
const { scrollTop } = this.scrollBox.current;
const start = Math.floor(scrollTop / itemSize);
this.setState({ start })
}
render() {
const { height, width, itemCount, itemSize, renderItem } = this.props;
const { start } = this.state;
let end = start + Math.floor(height / itemSize) + 1;
end = end > itemCount ? itemCount : end;
const visibleList = new Array(end - start).fill(0).map((item, index) => ({ index: start + index }));
const style = { position: 'absolute', top: 0, left: 0, width: '100%', height: itemSize };
return (
<div
style={{ overflow: 'auto', willChange: 'transform', height, width }}
ref={this.scrollBox}
onScroll={this.handleScroll}
>
<div style={{ position: 'absolute', width: '100%', height: `${itemCount * itemSize}px` }}>
{
visibleList.map(({ index }) => renderItem({ index, style: { ...style, top: itemSize * index } }))
}
</div>
</div>
)
}
}