Date: December 18, 2023
作用: 让 React 管理多个相对关联的状态数据
补充:和useState的作用类似,用来管理相对复杂的状态数据
**特点:**useReducer返回值为一个数组, 可以解构处数值state与修改数值的方法dispatch(修改数值的唯一方法)
案例:
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时如果想要传递参数,需要在action对象中添加一个payload参数,放置状态参数
// 定义reducer
import { useReducer } from 'react'
// 1. 根据不同的action返回不同的新状态
function reducer(state, action) {
?console.log('reducer执行了')
?switch (action.type) {
? ?case 'INC':
? ? ?return state + 1
? ?case 'DEC':
? ? ?return state - 1
? ?case 'UPDATE':
? ? ?return state + action.payload
? ?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>
? ? ?<button onClick={() => dispatch({ type: 'UPDATE', payload: 100 })}>
? ? ? update to 100
? ? ?</button>
? ?</>
)
}
export default App
**作用:**它在每次重新渲染的时候能够缓存计算的结果
语法:
useMemo(() => {
// 根据 count1 返回计算的结果
}, [count1])
说明:使用 useMemo 做缓存之后可以保证只有 count1 依赖项发生变化时才会重新计算
**使用场景:**消耗非常大的计算可以使用 useMemo
使用技巧:
1-指这个空数组只会在组件渲染完毕之后执行一次,即只要这个固定的 [1, 2, 3]
稳定的数组引用
const list = useMemo(() => {
return [1, 2, 3]
}, [])
原本用意:想基于count1的变化计算斐波那契数列之和,但是当我们修改count2状态的时候,斐波那契求和函数也会被执行,显然是一种浪费
案例:
// useMemo
// 作用:在组件渲染时缓存计算的结果
import { useState } from 'react'
function factorialOf(n) {
?console.log('斐波那契函数执行了')
?return n <= 0 ? 1 : n * factorialOf(n - 1)
}
function App() {
?const [count, setCount] = useState(0)
?// 计算斐波那契之和
?**const sum = factorialOf(count)**
?const [num, setNum] = useState(0)
?return (
? ?<>
? ? {sum}
? ? ?<button onClick={() => setCount(count + 1)}>+count:{count}</button>
? ? ?<button onClick={() => setNum(num + 1)}>+num:{num}</button>
? ?</>
)
}
export default App
Res:
思路: 只有count发生变化时才重新进行计算
import { useMemo, useState } from 'react'
function fib (n) {
?console.log('计算函数执行了')
?if (n < 3) return 1
?return fib(n - 2) + fib(n - 1)
}
function App() {
?const [count, setCount] = useState(0)
?// 计算斐波那契之和
?// const sum = fib(count)
?// 通过useMemo缓存计算结果,只有count发生变化时才重新计算
?**const sum = useMemo(() => {
? ?return fib(count)
}, [count])**
?const [num, setNum] = useState(0)
?return (
? ?<>
? ? {sum}
? ? ?<button onClick={() => setCount(count + 1)}>+count:{count}</button>
? ? ?<button onClick={() => setNum(num + 1)}>+num:{num}</button>
? ?</>
)
}
export default App
**作用:**允许组件在props没有改变的情况下跳过重新渲染
**默认机制:**顶层组件发生重新渲染,这个组件树的子级组件都会被重新渲染
Case1:
效果:点击按钮,数字不断增加,同时不断输出子组件内容。很明显,父组件的改变,子组件也会被跟着重新渲染改变。
import { useState } from "react";
function Son() {
console.log('我是子组件,我重新渲染了');
return <div>this is son</div>
}
function App() {
const [count, setCount] = useState(0)
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
</div>
)
}
export default App
Res:
Case2:
// memo
// 作用:允许组件在props没有改变的情况下跳过重新渲染
import { useState } from 'react'
function Son() {
?console.log('子组件被重新渲染了')
?return <div>this is son</div>
}
function App() {
?const [, forceUpdate] = useState()
?console.log('父组件重新渲染了')
?return (
? ?<>
? ? ?<Son />
? ? ?<button onClick={() => forceUpdate(Math.random())}>update</button>
? ?</>
)
}
export default App
Res:
Case3:
使用 useMemo 来缓存计算结果
import { useState ,memo, useMemo } from "react"
// React.memo props比较机制
// 1. 传递一个简单类型的prop props变化时组件重新渲染
// 2. 传递一个引用类型的prop 比较的新值和旧值的引用地址是否相同,相同则不重新渲染,不同则重新渲染
const MemoSon = memo(function Son({list}) {
console.log('子组件重新渲染了')
return <div>this is Son {list}</div>
})
function App() {
const [count, setCount] = useState(0)
const list = useMemo(() => {
return [1,2,3]
}, [])
return (
<div className="App">
<MemoSon count={list}></MemoSon>
<button onClick={() => setCount(count + 1)}>change count</button>
</div>
)
}
export default App;
Res:
**机制:**只有props发生变化时才重新渲染
下面的子组件通过 memo 进行包裹之后,返回一个新的组件MemoSon, 只有传给MemoSon的props参数发生变化时才会重新渲染
Case:
import { memo, useState } from "react";
function Son() {
console.log('我是子组件,我重新渲染了');
return <div>this is son</div>
}
const MemoSon = memo(function Son() {
console.log('我是子组件,我重新渲染了');
return <div>this is son</div>
})
function App() {
const [count, setCount] = useState(0)
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
<MemoSon/>
</div>
)
}
export default App
Res:
Case:
props变化子组件会重新渲染
import React, { useState } from 'react'
const MemoSon = React.memo(function Son() {
?console.log('子组件被重新渲染了')
?return <div>this is span</div>
})
function App() {
?console.log('父组件重新渲染了')
?const [count, setCount] = useState(0)
?return (
? ?<>
? ? ?<MemoSon count={count} />
? ? ?<button onClick={() => setCount(count + 1)}>+{count}</button>
? ?</>
)
}
export default App
Res:
对于props的比较,进行的是‘浅比较’,底层使用 Object.is 进行比较,针对于对象数据类型,只会对比俩次的引用是否相等,如果不相等就会重新渲染,React并不关心对象中的具体属性
举个例子:
// prop是简单类型
Object.is(3, 3) => true //没有变化
// prop是引用类型(对象/数组)
Object([], []) => false // 有变化,React只关心引用是否变化
Case1:
传递一个简单数据类型
import { useState ,memo } from "react"
// React.memo props比较机制
// 1. 传递一个简单类型的prop props变化时组件重新渲染
// 2. 传递一个引用类型的prop 比较的新值和旧值的引用地址是否相同,相同则不重新渲染,不同则重新渲染
const MemoSon = memo(function Son({count}) {
console.log('子组件重新渲染了')
return <div>this is Son {count}</div>
})
function App() {
const [count, setCount] = useState(0)
const num = 100
return (
<div className="App">
<MemoSon count={num}></MemoSon>
<button onClick={() => setCount(count + 1)}>change count</button>
</div>
)
}
export default App;
Res:
无返回
Case2:
传递一个复杂数据类型list。每次当我们点击 button 按钮的时候,都会引发父组件的重新渲染,从而也会引发 const list= [1, 2, 3]
的重新执行,因为其引用地址也会改变
import { useState ,memo } from "react"
// React.memo props比较机制
// 1. 传递一个简单类型的prop props变化时组件重新渲染
// 2. 传递一个引用类型的prop 比较的新值和旧值的引用地址是否相同,相同则不重新渲染,不同则重新渲染
const MemoSon = memo(function Son({list}) {
console.log('子组件重新渲染了')
return <div>this is Son {list}</div>
})
function App() {
const [count, setCount] = useState(0)
const list = [1, 2, 3]
return (
<div className="App">
<MemoSon count={list}></MemoSon>
<button onClick={() => setCount(count + 1)}>change count</button>
</div>
)
}
export default App;
Res:
说明:虽然俩次的list状态都是 [1,2,3]
, 但是因为组件App俩次渲染生成了不同的对象引用list,所以传给MemoSon组件的props视为不同,子组件就会发生重新渲染
如果上一小节的例子,我们不想通过引用来比较,而是完全比较数组的成员是否完全一致,则可以通过自定义比较函数来实现
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
上一小节我们说到,当给子组件传递一个引用类型
prop的时候,即使我们使用了memo
函数依旧无法阻止子组件的渲染,其实传递prop的时候,往往传递一个回调函数更为常见,比如实现子传父,此时如果想要避免子组件渲染,可以使用 useCallback
缓存回调函数
Case:
当我们给子组件传入函数时,由于函数也会引用数据类型,所以即使使用memo进行缓存控制,也会造成 父组件刷新 时,导致子组件同样刷新的问题
import { useState ,memo, useMemo, onChange } from "react"
const Input = memo(function Input({ onChange }) {
console.log(('子组件重新渲染了'))
return <input type="text" onChange={(e) => onChange(e.target.value)}></input>
})
function App() {
// 传给子组件的函数
const changeHandler = (value) => console.log(value);
// 触发父组件重新渲染的函数
const [count, setCount] = useState(0)
return (
<div className="App">
{/* 把函数作为 prop 传递给子组件 */}
<Input onChange={changeHandler}></Input>
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
)
}
export default App;
Res:
当我们不断点击按钮时,数字不断增加的同时,控制台也在不断输出子组件中的语句
**作用:**在组件多次重新渲染的时候缓存函数
语法:
const changeHandler = useCallback((value) => console.log(value), [])
参数:1-需要缓存的函数 2-依赖项
对2的补充说明
[]代表我们只需要将其缓存一次,后面的引用一直保持稳定。如果你想在依赖项变化的时候更新,那就传入一个依赖项即可。
理解:useCallback缓存之后的函数可以在组件渲染时保持引用稳定,也就是返回同一个引用
Case:
// useCallBack
import { memo, useCallback, 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 = useCallback((message) => {
? ?console.log(message)
}, [])
?return (
? ?<div>
? ? ?<MemoSon onGetSonMessage={onGetSonMessage} />
? ? ?<button onClick={() => forceUpate(Math.random())}>update</button>
? ?</div>
)
}
export default App
Res:
**作用:**允许组件使用ref将一个DOM节点暴露给父组件
语法:
const Son = forwardRef((props, ref) => {
return <input type="text" ref={ref}/>
})
参数:
1-props:指给子组件传递的参数 2-ref:通过ref来绑定子组件,从而在父组件上拿到子组件
Case1:
不使用 forwardRef
import { useRef } from "react"
// 子组件
function Son() {
return <input />
}
// 父组件
function App() {
const sonRef = useRef(null)
const showRef = () => {
console.log(sonRef);
}
return (
<>
<Son ref={sonRef} />
<button onClick={showRef}>focus</button>
</>
)
}
export default App
Res:
Case2:
通过 ref 获取
import { forwardRef, useRef } from "react"
// 子组件
// function Son() {
// return <input />
// }
const Son = forwardRef((props, ref) => {
return <input type="text" ref={ref} />
})
// 父组件
function App() {
const sonRef = useRef(null)
const showRef = () => {
console.log(sonRef);
sonRef.current.focus()
}
return (
<>
<Son ref={sonRef} />
<button onClick={showRef}>focus</button>
</>
)
}
export default App
Res:
**作用:**如果我们并不想暴露子组件中的DOM而是想暴露子组件内部的方法
语法:
const Input = forwardRef((props, ref) => {
const inputRef = useRef(null)
// 实现聚焦逻辑函数
const focusHandler = () => {
inputRef.current.focus()
}
// 暴露函数给父组件调用
useImperativeHandle(ref, () => {
return {
focusHandler
}
})
return <input type="text" ref={inputRef}/>
})
Case:
import { forwardRef, useImperativeHandle, useRef } from "react"
// 子组件
const Son = forwardRef((props, ref) => {
const inputRef = useRef(null)
// 实现聚焦逻辑函数
const focusHandler = () => {
inputRef.current.focus()
}
// 暴露函数给父组件调用
useImperativeHandle(ref, () => {
return {
focusHandler
}
})
return <input type="text" ref={inputRef}/>
})
// 父组件
function App() {
const sonRef = useRef(null)
const focusHandler = () => {
console.log(sonRef.current);
sonRef.current.focusHandler()
}
return (
<>
<Son ref={sonRef} />
<button onClick={focusHandler}>focus</button>
</>
)
}
export default App
Res:
**概念:**顾名思义,Class API就是使用ES6支持的原生Class API来编写React组件
特点:
1-通过类属性 state 定义状态数据
2-通过 setState 方法来修改状态数据
3-通过 render 来写UI模版(JSX语法一致)
Case:
通过一个简单的 Counter 自增组件看一下组件的基础编写结构:
// class API
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
Res:
**概念:**组件从创建到销毁的各个阶段自动执行的函数就是生命周期函数
图示:
Case:
基础案例:卸载组件
// Class API 生命周期
import { Component, useState } from "react";
class Son extends Component {
// 生命周期函数
// 挂载时:组件渲染完毕执行一次。用于:发送网络请求
componentDidMount() {
console.log('组件渲染完毕了,请求发送起来')
}
// 卸载时:组件卸载前执行一次。用于:清理副作用
componentWillUnmount() {
console.log('组件卸载了')
}
render() {
return <div>SSSon</div>
}
}
function App() {
const [show, setShow] = useState(true)
return (
<>
{show && <Son />}
<button onClick={() => setShow(false)}>unmount</button>
</>
)
}
export default App
Res:
Case2:
问题:以下Case的son组件在卸载后,其中的 Interval 定时器仍然在继续运行,造成了内存泄漏
// Class API 生命周期
import { Component, useState } from "react";
class Son extends Component {
// 生命周期函数
// 挂载时:组件渲染完毕执行一次。用于:发送网络请求
componentDidMount() {
console.log('组件渲染完毕了,请求发送起来')
// 开启定时器
this.timer = setInterval(() => {
console.log('定时器执行了')
}, 1000)
}
// 卸载时:组件卸载前执行一次。用于:清理副作用
componentWillUnmount() {
console.log('组件卸载了')
}
render() {
return <div>SSSon</div>
}
}
function App() {
const [show, setShow] = useState(true)
return (
<>
{show && <Son />}
<button onClick={() => setShow(false)}>unmount</button>
</>
)
}
export default App
Res:
修复后:
// Class API 生命周期
import { Component, useState } from "react";
class Son extends Component {
// 生命周期函数
// 挂载时:组件渲染完毕执行一次。用于:发送网络请求
componentDidMount() {
console.log('组件渲染完毕了,请求发送起来')
// 开启定时器
this.timer = setInterval(() => {
console.log('定时器执行了')
}, 1000)
}
// 卸载时:组件卸载前执行一次。用于:清理副作用
componentWillUnmount() {
console.log('组件卸载了')
// 清除定时器
**clearInterval(this.timer)**
}
render() {
return <div>SSSon</div>
}
}
function App() {
const [show, setShow] = useState(true)
return (
<>
{show && <Son />}
<button onClick={() => setShow(false)}>unmount</button>
</>
)
}
export default App
Res:
**概念:**类组件和Hooks编写的组件在组件通信的思想上完全一致
分类:
使用总结:
1-思想保持一致,不管API怎么改变,父子通信的思想是一致的
2-类组件依赖于this
父传子
Case:
// class API
import { Component } from 'react'
// 1. 父传子 直接通过props子组件标签身上绑定父组件中的数据即可
class Son extends Component {
render() {
// 使用this.props.msg
return <div>我是子组件 {this.props.msg}</div>
}
}
class Parent extends Component {
state = {
msg: 'this is parent msg'
}
render() {
return (
<div>
<h1>我是父组件</h1>
<Son msg={this.state.msg}/>
</div>
)
}
}
function App() {
return (
<>
<Parent />
</>
)
}
export default App
Res:
子传父
Case:
// class API
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
Res:
参考: