官网:https://react.docschina.org/

一、React简介

1. React入门

概述:React用于构建用户界面的轻量级的JS库,只关注 MVC 中的V(视图)。

特点:

  • 单向数据流
  • 组件化
  • 虚拟 DOM

2. React项目创建

2.1 官方脚手架

npx create-react-app 项目名

npm6+

npm init react-app 项目名

2.2 vite

npm create vite@last 项目名 -- --template react

2.3 redux

新项目:

npx create-react-app 项目名 --template redux

npx create-react-app 项目名 --template redux-typescript

老项目:

npm i redux react-redux

2.4 redux-thunk

npm i redux-thunk

2.5 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!--%PUBLIC_URL%表示public文件夹的路径-->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!--用于开启理想视口,用于移动端页面的适配-->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!--用于配置浏览器地址栏的颜色(仅支持安卓手机浏览器)-->
<meta name="theme-color" content="#000000" />
<!--描述网页信息的-->
<meta
name="description"
content="Web site created using create-react-app"
/>
<!--用于指定网页添加到手机主屏幕后的图标(仅仅支持ios)-->
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />

<!--应用加壳时候的配置文件 -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

<title>React App</title>
</head>
<body>
<!-- 浏览器不支持JS的运行的时候展现 -->
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

2.6 路由

npm install react-router-dom

3. JSX语法

  • 可以直接将标签赋值给变量
  • 如果要在标签之间插入JS变量或者JS表达式,必须使用{}
  • 需要替换部分的关键字,如标签上的 class 写为 className , for 写为 htmlFor
  • 事件绑定要使用小驼峰的写法,如onClick
  • 所有的标签必须闭合,即 必须写为
  • 标签中的注释需要写在 {} 中
  • 当变量是 Number,String 时可以直接显示
  • 当变量是Null,Undefined,Boolean,就会显示空
  • 如果是Object则不能直接渲染
  • 如果是数组,那么会直接展开数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 插入变量
const title = '您好';
const head = <header>{title}</header>;
{head}

// 调用函数
const sum = () => 3;
{sum()}

// 属性绑定
const title = '您好';
<h1 title={title}></h1>

// style 绑定
const style = {
color: 'red',
backgroundColor: 'yellow'
};
<h1 style={style}></h1>

// class 绑定
<p className={n ? 'on' : ''}></p>
<p className={['class1', 'class2', (n ? 'on' : '')].join(' ')}></p>

// 条件渲染
// if
const dom = () => {
if (flag) {
return <p>flag 为 true</p>
} else {
return <p>flag 为 false</p>
}
}

// 三元
flag ? <p>flag 为 true</p> : <p>flag 为 false</p>

// &&
flag && <p>flag 为 true</p>

// 列表渲染
const data = [
'aa', 'bb', 'cc', 'dd'
]
const newData = data.map((item, index) => <li key={index}>{item}</li>)

{newData}

4. 跨域

解决:

方法一:proxy

1
2
3
// 在package.json中追加如下配置

"proxy":"请求的地址" "proxy":"http://localhost:5000"

说明:

  1. 优点:配置简单,前端请求资源时可以不加任何前缀。
  2. 缺点:不能配置多个代理。
  3. 工作方式:上述方式配置代理,当请求了3000不存在的资源时,那么该请求会转发给5000 (优先匹配前端资源)

方法二:创建代理配置文件

1
在src下创建配置文件:src/setupProxy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const proxy = require('http-proxy-middleware')

module.exports = function(app) {
app.use(
proxy('/api1', { //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
target: 'http://localhost:5000', //配置转发目标地址(能返回数据的服务器地址)
changeOrigin: true, //控制服务器接收到的请求头中host字段的值
/*
changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
changeOrigin默认值为false,但我们一般将changeOrigin值设为true
*/
pathRewrite: {'^/api1': ''} //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
}),
proxy('/api2', {
target: 'http://localhost:5001',
changeOrigin: true,
pathRewrite: {'^/api2': ''}
})
)
}

说明:

  1. 优点:可以配置多个代理,可以灵活的控制请求是否走代理。
  2. 缺点:配置繁琐,前端请求资源时必须加前缀。

5. React 和 Vue的区别

相同点:

  • 数据驱动视图
  • 组件化思想
  • 都是用虚拟Dom

不同点:

  • 核心思想不通:
    • Vue为渐进式框架,门槛较低,进行数据劫持或者代理
    • React函数式编程,数据不可变以及单向数据流
  • 组件写法差异:
    • Vue单文件组建格式,即html,css,js写在同一个文件
    • React为html和css都写进js中
  • diff算法不同:
    • react 首先对新集合进行遍历,通过唯一key值来判断新旧集合中是否存在相同的节点,如果没有的话就创建,如果有的话就会将节点在新集合中的位置和老集合中的位置进行比较,如果不同,则进行移动操作,否则就不操作。如果在遍历的过程中,发现在新集合中没有,就会进行删除操作。
    • Vue新旧集合各有头尾的变量,他们的2各变量相互比较,一共有四种比较方式,如果四种都没匹配,则会匹配key值,借助key值找到可复用的节点,在进行相关操作。
  • 响应式原理不同:
    • Vue依赖收集,自动依赖,数据可变。递归监听data中的所有属性,直接修改,当数据改变时,自动找到引用组件重新渲染。
    • React基于状态手动优化,数据不可变,需要setState驱动新的state替换老的state。当数据改变时,以组件为根目录,默认全部重新渲染,所以React中会需要shouldComponentUpdate这个生命周期函数来进行控制。

二、React核心

1. 组件化

概述:组件允许将 UI 部分拆违可独立使用的代码片段

组件划分:

  • class 组件:使用class 去创建组件,可以包含逻辑和状态
  • 函数组件:以函数为组件,目前可以为函数组件添加状态

1.1 class 组件

概述:每一个组件一个文件。该文件后缀可以是 js 也可以是 jsx

执行过程
  • React解析组件标签,找到相应的组件
  • 发现组建时类定义的,随后 new出来的类的实例,并通过该实例调用到原型上的 render方法
  • render 返回的虚拟 DOM 转化为真实的 DOM ,随后呈现在页面中
1.1.1 语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
写法一:在constructor 中添加的状态
class App extends React.Component {
constructor(props) {
super(props) // 继承必须在这里调用 super
this.state = {} // class 组件的data,响应式数据
}
render() { // 每一个class 组件必须设置 render,然后在 render 中返回dom 节点
return {
<div>
<h1>React</h1>
</div>
}
}
}
export default App;

// 写法2:ES7 class 实验性质写法
class App extends React.Component {
state = {}
constructor(props) {
super(props); // 继承必须在这里调用 super
}
render() {}
}
export default App;
1.1.2 修改数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 只能调用 setState 方法去修改 state。

// 语法
setState(updater[ ,callback])
* updater: 可以接受对象类型,也可以接受函数类型;
* callback:可选参数,在 state 修改之后执行的回调函数;

this.setState({
count: 1
}
// 基于之前的值来修改
this.setState({
count: this.state.count + 1
});
// 使用函数
this.setState((preState, props) => ({
count: preState.count + 1
}))

局部数据修改

1
2
3
4
5
6
7
8
// const newUser = Object.assign(this.state.user, { name: 'lily' })
const newUser = {
...this.state.user,
name: 'lily'
}
this.setState({
user: newUser
})

数组数据修改:

React 执行 diff 时是比较引用,直接修改源对象的值,引用值是不发生改变,React 无法检测到,就不会重新渲染,所以需要使用不可变对象(不直接改源对象,而是返回新对象)。即修改数组不能使用 push, splice 等等方法。

1
2
3
this.setState({
arr: [...this.state.arr, 新值]
})
1.1.3 注意:state的更新是异步的
1
2
3
4
5
6
changeC = () => {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
}
// 这里的结果是只增加了 1,原因是: state 的更新可能是异步的,出与性能的考虑,React 会把多个 setState 的调用合并为一个调用;

setState 方法通过一个队列机制实现 state 更新,当执行 setState 的时候,会将更新的 state 合并后放入队列,而不会立即更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
如果不想 setState 被合并,那么需要传递函数来实现:

changeC = () => {
this.setState((preState) => ({
count: preState.count + 1
}))
this.setState((preState) => ({
count: preState.count + 1
}))
this.setState((preState) => ({
count: preState.count + 1
}))
}
1.1.4 事件处理

绑定事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 // 写法1:需要在构造函数中绑定 this
export default class Counter extends Component {
constructor(props) {
//...

// 绑定函数 this 指向
this.changeCount = this.changeCount.bind(this);
}
changeCount() {}

render() {
return <div onClick={this.changeCount}></div>
}
}

// 写法2:在 render 中绑定 this
// 会存在一个问题:即每次视图更新,都会重新绑定
export default class Counter extends Component {
changeCount() {}

render() {
return <div onClick={this.changeCount.bind(this)}></div>
}
}

// 写法3:使用箭头函数
// 会存在一个问题:即每次视图更新,都会重新绑定
export default class Counter extends Component {
changeCount() {}

render() {
return <div onClick={() => this.changeCount()}></div>
}
}

// 写法4 - ES7写法(实验性质)
export default class Counter extends Component {
changeCount = () => {}

render() {
return <div onClick={this.changeCount}></div>
}
}

参数传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 方式1
export default class Counter extends Component {
changeCount(str) {}

render() {
return <div onClick={this.changeCount.bind(this, 'abc')}></div>
}
}

// 方式2
export default class Counter extends Component {
changeCount(str) {}

render() {
return <div onClick={() => this.changeCount('abc')}></div>
}
}

// 方式3 - 给标签自定义属性
<button data-msg="hi" onClick={this.changeCount}></button>

changeCount = (e) => {

}

1.2 函数组件

概述:函数组件接受一个参数,该参数是父组件传递的props,组件内部必须返回 React 元素。

1
2
3
4
function 组件名(props) {

return ()
}

事件处理:

1
2
3
4
5
6
7
8
9
10
11
function App(props) {
function onHandler() {
console.log('事件');
}

return (
<div>
<button onClick={onHandler}>按钮</button>
</div>
)
}

2. 组件通信

2.1 props

概述:父组件传递给子组件的数据,子组件不能修改该数据,在 class组件中通过 this.props的方式获取。

使用:

1
2
3
4
5
6
7
8
9
10
11
 // 子组件
export default class Welcome extends Component {
render() {
return (
<div className='welcome'>{this.props.msg}</div>
)
}
}

// 父组件
<Welcome msg="Hi Props" />

peops约束:因为从 React 15.x 开始移除了 props 约束,所以需要安装第三方模块实现

nom i --save prop-types

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component } from 'react';
import PropTypes from 'prop-types'

export default class Welcome extends Component {
render() {
return (
<div className='welcome'>{this.props.msg}</div>
)
}
}
Welcome.propTypes = {
msg: PropTypes.string
msg: PropTypes.string.isRequired
}

2.2 子传父

1
2
3
4
5
6
// 父组件 
callback = (id) => {}
<Button callback={callback}></Button>

// 子组件
this.props.callback('传递给父组件数据')

2.3 context

概述:提供了一个无需为没曾组建手动添加 props ,就能在组件树中进行数据传递的方法。

使用:

  1. 新建 context文件
1
2
3
4
5
import React from 'react'
const MyContext = React.createContext([]) // 创建一个

export default MyContext

  1. 新建Provider 组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { Component } from 'react'
import MyContext from './MyContext'

export default class Provider extends Component {
state = {
msg: 'hello',
list: [],
}
changeMsg = (msg) => {
this.setState({
msg,
})
}
addList = (todo) => {
this.setState({
list: [...this.state.list, todo],
})
}
render() {
const value = {
msg: this.state.msg,
list: this.state.list,
changeMsg: this.changeMsg,
addList: this.addList,
}
return (
<MyContext.Provider value={value}>
{this.props.children}
</MyContext.Provider>
)
}
}

  1. class 组件使用 value
1
2
3
4
5
6
7
<MyContext.Consumer>
{
(value) => {
return <p>{value.msg}</p>
}
}
</MyContext.Consumer>
  1. 函数组件使用

概述: 允许在函数组件中使用 context 对象,在 context 值发生改变时重新渲染。

问题:context 的值发生改变,无论子组件是否引用 value, 都会导致 子组件重新渲染。

1
2
3
4
5
6
7
8
9
import {useContext} from 'react'

function App() {
const context = useContext(MyContext); // 需要接受context作为参数

return (
<div>{context.msg}</div>
)
}

2.4 useReducer

概述: useReducer 是 useState 的替代方案,能够解决 useState 状态更新逻辑散落在 UI 中,不能独立复用,不方便测试。

使用:

  1. 新建FnProvider组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { useState, useReducer } from 'react'
import MyContext from './MyContext'
import todoReducer from './todoReducer'

export default function FnProvider(props) {
const [msg, changeMsg] = useState('')
// const [todoList, setTodoList] = useState([])
const [todoList, dispatch] = useReducer(todoReducer, [])
return (
<div>
<h2>FnProvider</h2>
<MyContext.Provider
value={{
msg,
changeMsg,
todoList,
dispatch,
}}
>
{props.children}
</MyContext.Provider>
</div>
)
}

  1. 新建reducer.js文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, action.todo]
case 'DEL':
return state.filter((item) => item.id !== action.id)
case 'EDIT':
const index = state.findIndex((item) => item.id === action.payload.id) // 返回下标
const newTodoList = [...state]
newTodoList.splice(index, 1, action.payload.todo)
return newTodoList
default:
return state
}
}

  1. 调用
1
2
3
4
5
6
7
import FnProvider from './store/FnProvider'

<FnProvider>
<HashRouter>
<App />
</HashRouter>
</FnProvider>
1
2
3
4
5
6
7
8
9
10
import React, { useContext, useState } from 'react'

const context = useContext(MyContext)

onClick={() =>
context.dispatch({
type: 'DEL',
id: item.id,
})
}

2.5 redux

概述:redux 是 JS 的状态容器,可以使用一个叫 action 对象的事件来管理和更新应用的状态。不依赖于任何的框架,可以在 JS 中独立使用。

2.5.1 环境搭建

新项目:

npx create-react-app 项目名 --template redux

npx create-react-app 项目名 --template redux-typescript

老项目:

npm i redux react-redux

2.5.2 项目引入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux'
import store from './store/index'
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
reportWebVitals();
2.5.3 store 创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createStore, combineReducers } from 'redux'
import reducer from './reducer/reducer'
import todoReduce from './reducer/todoReducer'

const mainReducer = combineReducers({
reducer: reducer,
todo: todoReduce,
})
const store = createStore(mainReducer)

// 检测 state 的变化
store.subscribe(() => {
console.log('数据发生改变', store.getState())
})

export default store

2.5.4 reducer

概述:是一个纯函数,会根据不同的 action 对象返回一个新的 state

纯函数:

  • 不修改传入的参数
  • 不执行有副作用的操作,比如API,计时器等等
  • 不调用非纯函数,如new Date()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const initData = [
{
id: Date.now(),
flag: false,
text: '练习篮球一年半',
},
]

const addActionType = 'ADD'
export const addTodo = (payload) => {
return {
type: addActionType,
payload,
}
}

const editActionType = 'EDIT'
export const editTodo = (payload) => {
return {
type: editActionType,
payload,
}
}

const DelType = 'DEL'
export const actionDel = (payload) => ({
type: DelType,
payload,
})

export default function todoReduce(state = initData, action) {
const { type, payload } = action
switch (type) {
case addActionType:
return [...state, payload]
case DelType:
return state.filter((item) => item.id !== payload.id)
case editActionType:
const index = state.findIndex((item) => item.id === payload.id) // 返回下标
const newTodoList = [...state]
newTodoList.splice(index, 1, payload)
return newTodoList
default:
return state
}
}

2.5.5 combineReducers

概述:用于将多个 reducer 合并为一个 reducer。

1
2
3
4
5
6
7
8
import {combineReducers} from 'redux'

const mainReducer = combineReducers({
home,
user,
todo
})
store = createStore(mainReducer)
2.5.6 useSelector

概述:用于组件中获取 state 的hook

1
2
3
4
5
6
import {useSelector} from 'react-redux

const App =() => {
const todoList = useSelector((state) => state.todo)
}

2.5.7 useDispatch

概述:返回 dispatch 函数的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {useDispatch} from 'react-redux

const App =() => {
const dispatch = useDispatch()

const onSubmitTodo = () => {
dispatch(
addTodo({
text,
id: Date.now(),
flag: false,
})
)
setText('')
}
}
2.5.8 redux-thunk

概述: 因为在 reducer 中只能同步操作,那么要想异步处理 redux 就只能在组件中先进行异步操作,然后成功后再 dispatch。但是会让redux 的逻辑散落在UI中,不易于后期管理。

接用该插件可以实现 dispatch 一个函数,然后在这个函数中处理异步。

index.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { createStore, applyMiddleware, compose } from 'redux'
import thunkMiddleware from 'redux-thunk'
import logger from 'redux-logger'
import mainReducer from './reducer'

const middleWares = [
// 三方模块通过该中间件注册
thunkMiddleware,
]

if (process.env.NODE_ENV === 'development') {
middleWares.push(logger)
}

// Redux DevTools 配置
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

// 创建 store
const store = createStore(
mainReducer,
composeEnhancers(applyMiddleware(...middleWares))
)

// 检测redux 的状态变化
store.subscribe(() => {
console.log('数据发生改变', store.getState())
})

export default store

userAction.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { LOGIN } from '../actionTypes/userActionType'
import { loginApi } from '../../http/Api/user'

// 同步 类似于 Vuex-mutation
export const loginAction = (payload) => ({
type: LOGIN,
payload,
})

export const loginActionAsync = (payload) => (dispatch) =>
new Promise((resolve, reject) => {
loginApi(payload).then((res) => {
if (res.data.code == '200') {
dispatch(loginAction(res.data.data))
resolve()
} else {
console.log('登陆失败')
reject()
}
})
})

userReducer.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { LOGIN, LOGOUT } from '../actionTypes/userActionType'

const INIT_STATE = {
userId: null,
username: null,
authList: [],
token: null,
}

export default function userReducer(state = INIT_STATE, action) {
switch (action.type) {
case LOGIN:
return {
...state,
...action.payload,
}

case LOGOUT:
return {
userId: null,
username: null,
authList: [],
token: null,
}
default:
return state
}
}

reducer/index.js文件

1
2
3
4
5
6
import { combineReducers } from 'redux'
import userReducer from './userReducer'

export default combineReducers({
user: userReducer,
})

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { loginAction, loginActionAsync } from '../../store/actions/userAction'
import { useDispatch } from 'react-redux'

export default function Login() {
const navigate = useNavigate()
const dispatch = useDispatch()
const onFinish = async (value) => {
console.log(value)
dispatch(loginActionAsync(value)).then(() => {
navigate('/')
})


return ()
}

2.6 PubSubJs

1
2
3
4
5
6
7
8
9
10
11
12
13
// 引入
import PubSub from 'pubsub-js'

// 订阅消息
PubSub.subscribe("getSate",(_,data)=>{
console.log(data)
})
PubSub.subscribe("订阅的消息名称",回调函数,第一个参数是消息名称,可以使用_来占位,第二个是传递的数据
})

// 发布消息
PubSub.publish("getSate",{isFrist:false,isLoad:true})
PubSub.publish("订阅的消息名称",传递的数据)

2.7 RTK

3. class 生命周期

过时的生命周期:

名称说明
componentWillMount卸载组件之后
componentWillReceiveProps父组件进行了更新,子组件先执行这个【注意,第一次传递数据的时候,并不执行】
componentWillUpdate组件将要更新之前

原因: 这些生命周期方法经常被误解和滥用;此外,我们预计,在异步渲染中,它们潜在的误用问题可能更大。

现存生命周期:

名称说明
constructor加载的时候调用一次,初始化state
render更新 DOM 树的,每次渲染都会重新调用
componentDidMount组件渲染之后调用一次,一般在这个阶段发起请求、操作 DOM 节点
shouldComponentUpdate组件接收到新的 props 或者 state 的时候调用,该函数内返回 true 则渲染组件,返回 False 就不渲染
componentDidUpdate组件更新后,可以获取最新的 state
componentWillUnmount组件卸载,需要清除计时器、事件绑定 等

4. 插槽

4.1 匿名插槽

概述:写在每个组件之间的内容都可以被 props.children 获取。

1
2
3
4
5
6
7
8
9
10
// 父组件 
<TodoButton>新增</TodoButton>

// 子组件
...
return (
<button>
{this.props.children}
</button>
)

4.2 具名插槽

1
2
3
4
5
// 父组件 
<TodoList right={<TodoButton>删除</TodoButton>}></TodoList>

// 子组件
<div>{this.props.right}</div>

4.3 作用域插槽

概述:允许给子组件传递自定义内容,同时父组件也能拿到子组件的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 父组件 
<TodoList
data={todoList}
right={(item) => <TodoButton onClick={() => this.delTodo(item.id)}>删除</TodoButton>}>
</TodoList>

// 子组件
const lis = data.map(item => {
return (
<div className='todo-item' key={item.id}>
<div className='todo-item-text'>{item.text}</div>
<div>{this.props.right && this.props.right(item)}</div>
</div>
)
})

5. hooks

概述:可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

注意:

  • 只能在函数顶层调用hooks
  • 不能写在循环、条件判断中
  • 只能是在函数组件中或者自定义hook函数中

5.1 useState

概述:该方法用于定义函数组件的 state 的。

语法:

1
2
3
4
5
6
import {useState} from 'react';

function App() {
const [state, setState] = useState(0);
const [msg, setMsg] = useState('');
}

5.2 useEffect

概述:该函数可以看做是函数组件的生命周期,包含 componentDidMountcomponentDidUpdatecomponentWillUnmount,在render 之后执行。

语法:

1
2
3
4
5
6
7
8
import {useEffect} from 'react'

function App() {
useEffect(callback, [dependencies])
}

useLayoutEffect会在渲染的内容更新到DOM上之前进行,会阻塞DOM的更新
如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放在useLayoutEffect

dependencies:该参数是 useEffect的依赖,可以决定 useEffectcallback什么时候执行。

  • 如果该参数不存在,则初始化、组件任意状态值改变都会导致 callback 执行

  • 如果该参数传递一个 [],只有组件初始化和销毁的时候才会触发 callback

  • 该参数传入 [a]。表示在依赖 a 发生变化时才会触发 callback

  • useEffect(() => {
          console.log('Counter');
        }, [count]);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    `callback`:回调函数,一般用于发起请求、添加计时器、事件绑定,在该函数中可以return 一个函数,这个函数作为下一次 callback 执行前的调用,用于清除上一次 callback 产生的副作用。

    ```js
    useEffect(() => {
    console.log('Counter'); // 组件加载的时候执行

    return () => {
    // 组件卸载的时候执行
    console.log('useEffect 回调函数的返回值函数');
    }
    }, []);

    // 没有设置依赖
    useEffect(() => {
    console.log('Counter');
    const timer = setInterval(() => console.log(Date.now()), 1000)

    return () => {
    console.log('useEffect 回调函数的返回值函数');

    // 组件卸载之后需要干掉计时器
    clearInterval(timer)
    }
    });

5.3 useRef

概述:与createRef类似,用于获取 dom 元素或者组件实例

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
import {useRef} from 'react';

function App() {
const myRef = useRef();

useEffect(() => {
console.log(myRef.current); // 需要通过 current 获取节点
}, [])

return (
<div ref={myRef}></div>
)
}

5.n 自定义hooks

1. 封装useTableList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/* 
listReq:api请求
option:obj
{
cur:当前页数
size:一页多少条数据
data:查询参数
flag:是否自动请求
}
*/

import { useState } from 'react'

export function useList(listReq, option) {
const [list, setList] = useState([])

const flag = option?.flag || true // 默认自动请求
const cur = option?.cur || 1
const size = option?.size || 5
const data = option?.data || {}
const getList = () => {
const payload = { ...data, current: cur, pageSize: size }
listReq(payload)
.then((res) => {
console.log(res)
if (res.code == '200') {
console.log(res.data)
// setList(res.)
} else {
console.log('获取数据错误')
}
})
.catch((err) => {
console.log(err)
})
}
flag && getList()

return {
list,
getList,
}
}

2. 封装useLocalStorage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { useState } from 'react'

export function useSessionStorage(key) {
const [state, setState] = useState(() => getStorage())

function getStorage() {
const value = sessionStorage.getItem(key)
if (value) {
try {
return JSON.parse(value)
} catch (error) {
console.log(error)
}
} else {
return undefined
}
}

const updateState = (newState) => {
// 如果为undefined 则删除该键
if (typeof newState == 'undefined') {
sessionStorage.removeItem(key)
setState(undefined)
} else {
sessionStorage.setItem(key, JSON.stringify(newState))
setState(newState)
}
}

return { state, updateState }
}

6. 重要属性

6.1 ref

概述:用于获取DOM节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ... {
constructor() {
super()
this.myRef = React.createRef();
}

componentDidMount() {
// 必须通过 current 获取节点
// 即 this.myRef.current 表示 dom ,相当于 document.getElementById()
this.myRef.current.focus();
}

render() {
return (
<input ref={this.myRef} />
)
}
}

6.2 fragment

默认情况下 react 的render 只支持单标签渲染,如果想要渲染多标签,那么可以使用该组件,可以再不多增加标签嵌套的情况下,渲染节点。

1
2
3
4
5
6
7
8
9
10
11
12
<React.Fragment>
<div></div>
<div></div>
<div></div>
</React.Fragment>

// 简写
<>
<div></div>
<div></div>
<div></div>
</>

6.3 portal

概述:期望渲染的节点能够脱离父组件范围,可以将节点放在任意我期望的位置。

语法:

1
2
3
4
ReactDOM.craetePortal(child, container)

* child:任何有效的节点或者 React 组件;
* container:希望将 child 放在什么位置,应该是一个 DOM 节点;

6.4 createElement

概述:因为 JSX 语法中的元素底层就是调用该方法生成组件的。

1
2
3
4
5
const myDiv = React.createElement('div', null, '标签之间的哦内容');

render() {
return <myDiv />
}

7. 高阶组件(HOC)

概述:以组件为参数,返回新组件的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const WithTable = (Component) => {

// props = {loading: false, titile: ''} = {loading, ...otherProps}
const NewComponent = ({ loading, ...otherProps }) => {
console.log('HOC 的 props:', otherProps);
if (loading) {
return <div>loading...</div>
} else {
return <Component {...otherProps} />
}
}

// 创建一个新组件
// const Loading = () => <div>loading...</div>

return NewComponent;
}
export default WithTable

8. 性能优化

父子组件嵌套:

1
2
3
4
5
6
// 执行顺序
Father render -
ClassA.jsx:14 ClassA constructor
ClassA.jsx:23 ClassA render
ClassA.jsx:17 ClassA componentDidMount
Father useEffect -

子组件数据改变后:

  • 子组件 render;

  • 父组件不变

父组件状态改变:

  • 父组件 render
  • 子组件无论与父组件的状态是否有关,都会render

8.1 class 组件优化

8.1.1 shouldComponentUpdate

概述:针对 class 组件,如果 props 和 state 都没有改变

1
2
3
4
5
6
7
8
9
10
shouldComponentUpdate(nextProps, nextState) {
// 该生命周期必须返回一个布尔值,如果为 true,则render,否则不render
console.log('ClassA shouldComponentUpdate', nextProps, nextState)

if (this.props.num2 != nextProps.num2 || this.state != nextState) {
return true;
} else {
return false;
}
}
8.1.2 PureComponent

概述:React.PureComponent 和 React.Component用法差不多,唯一不同的是 PureComponent 会浅比较 props 和 state 是否相同,从而决定是否重新渲染组件。

8.2 函数组件优化

8.2.1 useMemo

概述:这个函数的返回值会被缓存,在组建第一次渲染的时候执行,以后会在以来发生改变后在此次执行。缓存数据

语法:useMemo(create.deps)

  • create:是一个函数,函数的返回值作为缓存值
  • deps:依赖,是一个数组

作用:一般用于比较复杂的运算或者耗时操作

8.2.2 useCallback

概述:useCallback 和 useMemo 接受参数一样,都是依赖发生改变后执行回调,否则返回缓存值,区别是 useCallback 是返回函数, useMemo 是返回运算结果。缓存函数

8.3 memo 优化

概述:和 PureComponent 类似,用于性能调优,函数组件和类组件都可以使用,区别是 memo 只能判断 props 而不能处理 state 的改变。

语法:memo(组件)

9. 路由

npm install react-router-dom

9.1 Route

概述:用于匹配当前 url 要渲染的组件

1
<Route path="url" element={<组件 />} />

index:表示默认路由,设置该属性后,Route 不能有子组件

9.2 Routes

概述:作为 Route 的上一级组件,可以优化路径匹配

用于连接跳转,最终会被渲染为a 标签

to:链接的路径

{pathname:'/admin',query:'111',state:'111'}

1
// 通过this.props.location.state或this.props.location.query来获取即可

9.4 路由嵌套

1
2
3
4
5
6
7
8
9
10
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
</Route>

在父组件中使用 <Outlet /> 渲染子组件
// Layout
<div>
<Outlet />
</div>

9.5 useNavigate

概述:用于返回一个类似 history 的对象,可用于编程式导航。

1
2
3
4
5
6
7
8
9
10
11
12
import { useNavigate } from "react-router-dom";

const navigate = useNavigate();

// 传递 url 参数
navigate('/home');

// 传递数字来表示前进或者后退多少
navigate(-1)

// 替换历史记录
navigate('/', {replace: true})

9.6 useSearchParams

概述:获取 url 的查询参数

1
2
3
4
5
6
import { useSearchParams } from "react-router-dom";

const [searchParams, setSearchParams] = useSearchParams()
console.log('获取查询参数:', searchParams.get('id'));

// this.props.location.search也可以获取

9.7 useParams

概述:获取动态参数path='/admin/:id'

1
2
3
4
5
6
import { useParams } from "react-router-dom";

const params = useParams();
console.log('获取动态参数:', params);

// this.props.match.params.id也可以获取

9.8 路由模式

1. hasn模式
  • hash 模式是一种把前端路由的路径用井号 # 拼接在真实 url 后面的模式。当井号 # 后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发 onhashchange 事件。不会被包括在 http 请求中。
  • hash变化会触发网页跳转,即浏览器的前进和后退。
  • hash 通过 window.onhashchange 的方式,来监听 hash 的改变,借此实现无刷新跳转的功能
  • hash 可以改变 url ,但是不会触发页面重新加载,不会出现在``http`中
  • 首先,hash本来是拿来做页面定位的,如果拿来做路由的话,原来的锚点功能就不能用了。其次,``hash的传参是基于url`的,如果要传递复杂的数据,会有体积的限制
2. history模式
  • 利用H5的 history中新增的两个API pushState()replaceState() 和一个事件``onpopstate`监听URL变化
  • history每次刷新会重新像后端请求整个网址,也就是重新请求服务器。如果后端没有及时响应,就会报错404!
  • history模式不仅可以在``url`里放参数,还可以将数据存放在一个特定的对象中。
  • 修改历史状态: 包括了 HTML5 History Interface 中新增的 pushState()replaceState() 方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修改时,虽然修改了 url,但浏览器不会立即向后端发送请求。如果要做到改变 url 但又不刷新页面的效果,就需要前端用上这两个 API。
  • 切换历史状态: 包括 forward()back()go() 三个方法,对应浏览器的前进,后退,跳转操作。
3. 两种模式对比
  • history 模式的 pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;
  • pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
  • pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;
  • pushState() 可额外设置 title 属性供后续使用。
  • hash 模式下,仅 hash 符号之前的 url 会被包含在请求中,后端如果没有做到对路由的全覆盖,也不会返回 404 错误;history 模式下,前端的 url 必须和实际向后端发起请求的 url一致,如果没有对用的路由处理,将返回 404 错误。

10. 事件处理

  • React的事件是通过onXxx属性指定事件处理函数
  • React使用的都是自定义的时间,而不是原生的事件
  • React中的事件是通过事件委托方式处理的
  • 通过event.target得到发生事件的Dom元素对象
  • 事件中必须返回的是函数

10.1 受控和非受控组件

受控: 对于受控组件来说,输入的值始终由 React 的 state 驱动。

非受控: 表单元素的值不会更新state。输入数据都是现用现取的。

10.2 class 事件处理

绑定事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 // 写法1:需要在构造函数中绑定 this
export default class Counter extends Component {
constructor(props) {
//...

// 绑定函数 this 指向
this.changeCount = this.changeCount.bind(this);
}
changeCount() {}

render() {
return <div onClick={this.changeCount}></div>
}
}

// 写法2:在 render 中绑定 this
// 会存在一个问题:即每次视图更新,都会重新绑定
export default class Counter extends Component {
changeCount() {}

render() {
return <div onClick={this.changeCount.bind(this)}></div>
}
}

// 写法3:使用箭头函数
// 会存在一个问题:即每次视图更新,都会重新绑定
export default class Counter extends Component {
changeCount() {}

render() {
return <div onClick={() => this.changeCount()}></div>
}
}

// 写法4 - ES7写法(实验性质)
export default class Counter extends Component {
changeCount = () => {}

render() {
return <div onClick={this.changeCount}></div>
}
}

参数传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 方式1
export default class Counter extends Component {
changeCount(str) {}

render() {
return <div onClick={this.changeCount.bind(this, 'abc')}></div>
}
}

// 方式2
export default class Counter extends Component {
changeCount(str) {}

render() {
return <div onClick={() => this.changeCount('abc')}></div>
}
}

// 方式3 - 给标签自定义属性
<button data-msg="hi" onClick={this.changeCount}></button>

changeCount = (e) => {

}

10.3 函数组件事件处理

1
2
3
4
5
6
7
8
9
10
11
function App(props) {
function onHandler() {
console.log('事件');
}

return (
<div>
<button onClick={onHandler}>按钮</button>
</div>
)
}

11. Diff算法

11.1 index

问题:

  • 若对数据进行:逆序添加、逆序删除等破坏顺序操作:

    会产生没有必要的真实DOM更新 界面效果没问题,但效率低。

  • 如果结构中还包含输入类的DOM:会产生错误DOM更新 界面有问题。

  • 注意! 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。

11.2 diff

Diff算法其实就是react生成的新虚拟DOM和以前的旧虚拟DOM的比较规则:

  • 如果旧的虚拟DOM中找到了与新虚拟DOM相同的key:
    • 如果内容没有变化,就直接只用之前旧的真实DOM
    • 如果内容发生了变化,就生成新的真实DOM
  • 如果旧的虚拟DOM中没有找到了与新虚拟DOM相同的key:
    • 根据数据创建新的真实的DOM,随后渲染到页面上

二、React 周边

1. css处理

1.1 局部 css 样式

  1. 要求新建:文件名.module.css

    1. 在该文家中创建css代码

  2. 导入样式:

    1
    2
    3
    import styles from '文件名.module.css';

    <button className={styles.btn_default}></button>

1.2 引入sass

概述:只需要在当前环境中安装以下模块就可以直接使用。

npm i sass

2. antd 引入

npm i antd

配置模块安装:

npm i @craco/craco -D --legacy-peer-deps

npm i babel-plugin-import -D

修改package.json 里的 scripts

1
2
3
"script": {
"start": "craco start"
}

根目录下创建配置文件 craco.config.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
babel: {
plugins: [
["import", {
libraryName: 'antd',
libraryDirectory: 'es',
style: 'css'
}]
]
},
webpack: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
};