useReducer

作用:和 useState 的作用类似,用来管理相对复杂的状态数据

案例:编写一个能够加减的函数

  1. 定义一个reducer函数(根据不同的action返回不同的新状态)
  2. 在组件中调用useReducer,并传入reducer函数和状态的初始值
  3. 事件发生时,通过dispatch函数分派一个action对象(通知reducer要返回哪个新状态并渲染UI)
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
import { useReducer } from 'react'

// 1. 定义reducer函数,根据不同的action返回不同的新状态
function reducer(state, action) {
switch (action.type) {
case 'INC':
return state + 1
case 'DEC':
return state - 1
default:
return state
}
}

function App() {
// 2. 使用useReducer分派action
const [state, dispatch] = useReducer(reducer, 0)
return (
<>
{/* 3. 调用dispatch函数传入action对象 触发reducer函数,分派action操作,使用新状态更新视图 */}
<button onClick={() => dispatch({ type: 'DEC' })}>-</button>
{state}
<button onClick={() => dispatch({ type: 'INC' })}>+</button>
</>
)
}

export default App

分派 action 时传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function reducer(state, action) {
switch (action.type) {
// ...
case 'UPDATE':
return state + action.payload
default:
return state
}
}

function App() {
const [state, dispatch] = useReducer(reducer, 0)
return (
<>
{state}
<button onClick={() => dispatch({ type: 'UPDATE', payload: 100 })}>
update to 100
</button>
</>
)
}

export default App

useReducer原理

image.png

渲染性能优化

useMemo

作用:在组件每次重新渲染的时候缓存计算的结果

1
2
3
useMemo(() => {
// 返回计算结果
}, [count]) //第二个参数放置依赖项

案例:计算斐波那契数列,count1需要进行计算并重新渲染组件

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
import { useState } from "react";

function fib(n) {
  console.log('计算函数执行了');
  if (n < 3) {
    return 1
  }
  return fib(n - 2) + fib(n - 1)
}

function App () {
  const [count1, setCount1] = useState(0)
  const [count2, setCount2] = useState(0)
  const result = fib(count1) // 每次count1和count2变化时,组件重新渲染都会执行这个函数
  console.log('组件更新');

  return (
    <div>
      <button onClick={() => setCount1(count1 + 1)}>改变count1:{count1}</button>
      <button onClick={() => setCount2(count2 + 1)}>改变count2:{count2}</button>
      {result}
    </div>
  )
}

export default App;

上面的代码每次组件重新渲染都回重新执行一次计算函数,即使是count2也是如此,是没有意义的但是会消耗性能

1
2
3
4
// const result = fib(count1)
const result = useMemo(() => {
return fib(count1)
}, [count1])

useMemo 会缓存计算结果,而不会重新渲染,只有依赖项变化才会计算

React.memo

作用:允许组件在Props没有改变的情况下跳过渲染

React组件默认的渲染机制:只要父组件重新渲染子组件就会重新渲染

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 } from 'react'

const MemoSon = React.memo(function Son() {
console.log('子组件被重新渲染了')
return <div>this is span</div>
})

function App() {
const [, forceUpdate] = useState()

const [count, setCount] = useState(0)

console.log('父组件重新渲染了')
return (
<>
<MemoSon />
<button onClick={() => forceUpdate(Math.random())}>update</button>

<MemoSon count={count} />
<button onClick={() => setCount(count + 1)}>+{count}</button>
</>
)
}

export default App

子组件通过 memo 进行包裹之后,返回一个新的组件MemoSon, 只有传给MemoSon的props参数发生变化时才会重新渲染

props 的比较机制

对于props的比较,进行的是浅比较,底层使用 Object.is 进行比较,针对于对象数据类型,只会对比俩次的引用是否相等,如果不相等就会重新渲染,React并不关心对象中的具体属性

prop是简单类型:Object.is(3, 3) => true 没有变化

prop是引用类型(对象 / 数组):Object([], []) => false 有变化,React 只关心引用是否变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { useState } from 'react'

const MemoSon = React.memo(function Son() {
console.log('子组件被重新渲染了')
return <div>this is span</div>
})

function App() {
const [list, setList] = useState([1, 2, 3])
return (
<>
<MemoSon list={list} />
<button onClick={() => setList([1, 2, 3])}>
{JSON.stringify(list)}
</button>
</>
)
}

export default App

虽然两次的 list 状态都是 [1,2,3] , 但是因为组件 App 俩次渲染生成了不同的对象引用 list,所以传给 MemoSon组件的 props 视为不同,子组件就会发生重新渲染

自定义比较函数

不想通过引用来比较,而是完全比较数组的成员是否完全一致,则可以通过自定义比较函数来实现

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
import React, { useState } from 'react'

// 自定义比较函数
function arePropsEqual(oldProps, newProps) {
console.log(oldProps, newProps)
return (
oldProps.list.length === newProps.list.length &&
oldProps.list.every((oldItem, index) => {
const newItem = newProps.list[index]
console.log(newItem, oldItem)
return oldItem === newItem
})
)
}

const MemoSon = React.memo(function Son() {
console.log('子组件被重新渲染了')
return <div>this is span</div>
}, arePropsEqual)

function App() {
console.log('父组件重新渲染了')
const [list, setList] = useState([1, 2, 3])
return (
<>
<MemoSon list={list} />
<button onClick={() => setList([1, 2, 3])}>
内容一样{JSON.stringify(list)}
</button>
<button onClick={() => setList([4, 5, 6])}>
内容不一样{JSON.stringify(list)}
</button>
</>
)
}

export default App

useCallback

作用:在组件多次重新渲染的时候缓存函数

当给子组件传递一个引用类型prop(例如函数)的时候,即使我们使用了memo 函数依旧无法阻止子组件的渲染,其实传递prop的时候,往往传递一个回调函数更为常见,比如实现子传父,此时如果想要避免子组件渲染,可以使用 useCallback缓存回调函数

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { memo, useState } from 'react'

const MemoSon = memo(function Son() {
console.log('Son组件渲染了')
return <div>this is son</div>
})

function App() {
const [, forceUpate] = useState()
console.log('父组件重新渲染了')
const onGetSonMessage = (message) => {
console.log(message)
}

return (
<div>
<MemoSon onGetSonMessage={onGetSonMessage} />
<button onClick={() => forceUpate(Math.random())}>update</button>
</div>
)
}

父组件每次渲染,传回去的onGetSonMessage函数都是新的引用,所以子组件也会渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function App() {
// ...
const onGetSonMessage = useCallback((message) => {
console.log(message)
}, [])

return (
<div>
<MemoSon onGetSonMessage={onGetSonMessage} />
<button onClick={() => forceUpate(Math.random())}>update</button>
</div>
)
}

export default App

useCallback 缓存之后的函数可以在组件渲染时保持引用稳定,也就是返回同一个引用

React.forwardRef

作用:允许组件使用ref将一个DOM节点暴露给父组件

案例:通过 ref 获取到子组件内部的 input 元素让其聚焦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { forwardRef, useRef } from 'react'

const MyInput = forwardRef(function Input(props, ref) {
return <input {...props} type="text" ref={ref} />
}, [])

function App() {
const ref = useRef(null)

const focusHandle = () => {
ref.current.focus()
}

return (
<div>
<MyInput ref={ref} placeholder="请输入" />
<button onClick={focusHandle}>focus</button>
</div>
)
}

export default App

useInperativeHandle

作用:通过ref暴露子组件中的方法

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
import { forwardRef, useImperativeHandle, useRef } from 'react'

const MyInput = forwardRef(function Input(props, ref) {
// 实现内部的聚焦逻辑
const inputRef = useRef(null)
const focus = () => inputRef.current.focus()

// 暴露子组件内部的聚焦方法
useImperativeHandle(ref, () => {
return {
focus,
}
})

return <input {...props} ref={inputRef} type="text" />
})

function App() {
const ref = useRef(null)

const focusHandle = () => ref.current.focus()

return (
<div>
<MyInput ref={ref} />
<button onClick={focusHandle}>focus</button>
</div>
)
}

export default App

用 ref 将组件内部的方法暴露给父组件

Class API

类组件基础结构

类组件 是老版本的用法,现在已经不再推荐

类组件就是通过 JS 中的类来组织组件的代码

  1. 通过类属性 state 定义状态数据
  2. 通过 setState 方法来修改状态数据
  3. 通过 render 来写UI模版(JSX语法一致)
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
import { Component } from 'react'

class Counter extends Component {
// 状态变量
state = {
count: 0,
}

// 事件回调
clickHandler = () => {
// 修改状态变量 触发UI组件渲染
this.setState({
count: this.state.count + 1,
})
}

// UI模版
render() {
return <button onClick={this.clickHandler}>+{this.state.count}</button>
}
}

function App() {
return (
<div>
<Counter />
</div>
)
}

export default App

生命周期函数

组件从创建到销毁的各个阶段自动执行的函数就是生命周期函数

image.png

  1. componentDidMount:组件挂载完毕自动执行 - 异步数据获取
  2. componentWillUnmount: 组件卸载时自动执行 - 清理副作用

组件通信

父传子

通过 prop 绑定数据

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 { Component } from 'react'

class Son extends Component {
render() {
const { count } = this.props
return <div>this is Son, {count}</div>
}
}

class App extends Component {
// 状态变量
state = {
count: 0,
}

setCount = () => {
this.setState({
count: this.state.count + 1,
})
}

// UI模版
render() {
return (
<>
<Son count={this.state.count} />
<button onClick={this.setCount}>+</button>
</>
)
}
}

export default App

子传父

通过 prop 绑定父组件中的函数,子组件调用

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
import { Component } from 'react'

class Son extends Component {
render() {
const { msg, onGetSonMsg } = this.props
return (
<>
<div>this is Son, {msg}</div>
<button onClick={() => onGetSonMsg('this is son msg')}>
changeMsg
</button>
</>
)
}
}

class App extends Component {
// 状态变量
state = {
msg: 'this is initail app msg',
}

onGetSonMsg = (msg) => {
this.setState({ msg })
}

// UI模版
render() {
return (
<>
<Son msg={this.state.msg} onGetSonMsg={this.onGetSonMsg} />
</>
)
}
}

export default App

zustand

zustand 是状态管理工具,是 Redux 的平替

1
2
# 安装zustand
npm install zustand

store/index.js - 创建store

1
2
3
4
5
6
7
8
9
10
11
12
import { create } from 'zustand'

const useStore = create((set) => {
return {
count: 0,
inc: () => {
set(state => ({ count: state.count + 1 }))
}
}
})

export default useStore

app.js - 绑定组件

1
2
3
4
5
6
7
8
import useStore from './store/useCounterStore.js'

function App() {
const { count, inc } = useStore()
return <button onClick={inc}>{count}</button>
}

export default App

image.png

异步操作

对于异步的支持不需要特殊的操作,直接在函数中编写异步逻辑,最后只需要调用 set方法 传入新状态即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { create } from 'zustand'

const URL = 'http://geek.itheima.net/v1_0/channels'

const useStore = create((set) => {
return {
count: 0,
ins: () => {
return set(state => ({ count: state.count + 1 }))
},
channelList: [],
fetchChannelList: async () => {
const res = await fetch(URL)
const jsonData = await res.json()
set({channelList: jsonData.data.channels})
}
}
})

export default useStore

切片模式

场景:当单个store比较大的时候,可以采用 切片模式 进行模块拆分组合,类似于模块

拆分并组合切片

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 { create } from 'zustand'

// 创建counter相关切片
const createCounterStore = (set) => {
return {
count: 0,
setCount: () => {
set(state => ({ count: state.count + 1 }))
}
}
}

// 创建channel相关切片
const createChannelStore = (set) => {
return {
channelList: [],
fetchGetList: async () => {
const res = await fetch(URL)
const jsonData = await res.json()
set({ channelList: jsonData.data.channels })
}
}
}

// 组合切片
const useStore = create((...a) => ({
...createCounterStore(...a),
...createChannelStore(...a)
}))

组件使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function App() {
const {count, inc, channelList, fetchChannelList } = useStore()
return (
<>
<button onClick={inc}>{count}</button>
<ul>
{channelList.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</>
)
}

export default App

对接DevTools

简单的调试我们可以安装一个 名称为 simple-zustand-devtools 的调试工具

安装调试包

1
npm i simple-zustand-devtools -D

配置调试工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import create from 'zustand'

// 导入核心方法
import { mountStoreDevtool } from 'simple-zustand-devtools'

// 省略部分代码...


// 开发环境开启调试
if (process.env.NODE_ENV === 'development') {
mountStoreDevtool('channelStore', useChannelStore)
}


export default useChannelStore

打开 React调试工具

02.png