摘要
本篇主要是基础拓展篇。
- useReducer
- useMemo
- React.memo
- useCallback
- React.forwardRef
- useImperativeHandle
useReducer
作用:和useState作用类似,用来管理相对复杂的状态数据。
我们同样来实现一个 计数器,count 的自增、自减、重置。
基础用法:
也是有固定的写法的.
-
定义一个reducer函数,根据不同的action返回不同的新状态
-
在组件中调用useReducer,并传入reducer函数和状态的初始值
-
事件发生时,通过dispatch 函数分派一个action对象(通知reducer要返回哪个新状态并渲染UI)
样板代码:
function reducer(state,action){
// 根据不同的action type 返回新的state
switch(action.type){
case 'add':
return state + 1
case 'dec':
return state - 1
case 'set':
return action.payload
default:
return state
}
}
组件中:
function App(){
const [state,dispatch] = useReducer(reducer,0)
return (
<div>
<button onClick={()=>dispatch({type:'dec'})}>-</button>
{state}
<button onClick={()=>dispatch({type:'add'})}>+</button>
{/*传递自定义参数,参入多少,写入多少*/}
<button onClick={()=>dispatch({type:'add',payload:99})}>set</button>
</div>
)
}
useMemo
作用: 在组件每次重新渲染的时候缓存计算的结果。提到缓存,那么这个函数的作用的就属于优化作用。 应用: 一般的计算场景不需要,但是计算密集型,例如斐波那契,n非常大时候,里面的递归调用执行很多次。这个时候就要考虑优化性能。
一个组件,有2个状态:count1 和 count2 。基于count1 做个斐波那契数列求和fn。
正常情况:我们希望,只有相关的count1 发生变化 对应的斐波那契函数fn执行一次即可。但实际情况是只要有状态变量发生变化,这个的组件就会重新渲染,也就是组件函数就会重新执行,也就是位于组件中的fn也还是要执行一次的。所以这种属于一种浪费。
useMemo 如何解决呢?
useMemo(() =>{
//只有count1 发生变化,才执行函数fn
return fn()
},[count1])
完整代码:
import {useState} from 'react'
function fib(n){
console.log('fn执行了')
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)
console.log('组件重新渲染了')
return(
<div className="App">
<button onClick={()=>setCount1(count1 + 1)}>{count1}</button>
<button onClick={()=>setCount2(count2 + 1)}>{count2}</button>
{result}
</div>
)
}
测试发现,count2 变了,fib 也一样的执行。
所以优化:
import {useState} from 'react'
function fib(n){
console.log('fn执行了')
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 = useMemo(()=>{
return fib(count1)
},[count1])
//------------------------改动处
console.log('组件重新渲染了')
return(
<div className="App">
<button onClick={()=>setCount1(count1 + 1)}>{count1}</button>
<button onClick={()=>setCount2(count2 + 1)}>{count2}</button>
{result}
</div>
)
}
扩展
组件渲染完成只执行一次。
useMemo(() =>{
//组件渲染完,执行一次函数fn
return fn()
},[])
React.memo
作用:允许组件在 props
没有改变的情况下跳过渲染。
React组件默认渲染机制:
只要父组件重新渲染,则子组件就会重新渲染。
也就是父组件里一个状态的改变,不仅父组件会更新,其所有的子组件也会无脑更新,而子本身不需要更新渲染的,这样就存在浪费。
所以有了React.memo 这个方法,可以让组件的 props 没有改变的时候,就跳过这种无脑渲染
语法
把子组件用 memo 函数包裹,用一个变量接收,得到一个新的缓存组件,然后页面里使用这个新组件。
import { memo } from 'react'
//或者不导入,直接 React.memo()
const MemoComponent = memo(function ChildComponent(props){
//...
})
然后 页面使用的时候,用 替代
React.memo-props 的比较机制
机制:在使用memo缓存组件之后,React 会对每个prop 使用原生js的object.is 比较新值 和老值,返回true,表示没有变化。
prop 是简单类型:Object.is(3,3) -> true 没有变化。
prop 是引用类型: object([],[]) -> false 有变化,React只关心引用是否变化。
所以对于引用类型,我们需要额外处理,让数组 对象没有发生改变的时候,不进行重新渲染。
import { memo , useMemo , useState} from 'react'
const MemoComponent = memo(function ChildComponent({arr}){
//...
return <span>{arr}</span>
})
function App(){
const [count1,setCount1] = useState(0)
//---------------------核心:
// 利用useMemo 组件渲染过程中缓存一个值。来保证引用的稳定。
const arr = useMemo(()=>{
return [1,2,3]
},[])//依赖给个空数组,这样就只在组件渲染时执行一次。
//---------------------核心
return(
<div className="App">
<button onClick={()=>setCount1(count1 + 1)}>{count1}</button>
<MemoComponent arr={arr}/>
</div>
)
}
传递一个引用类型的prop 比较的是新值和旧值的引用是否相等,当父组件的函数重新执行时,实际上形成的是新的数组引用。利用useMemo来做数组引用的缓存。
useCallback
作用: 在组件多次重新渲染的时候缓存函数。比较常用于封装组件,函数父传子。子把值带出去给父。
参数1:要包裹的需要缓存的函数。
参数2:依赖项,一般给空数组。
当父组件 给 子组件 传递的props 是一个函数的时候,函数属于复杂类型,则父组件里有状态量发生改变触发父组件更新时,那么子组件也会跟着更新。如何避免呢?
import { memo , useState, useCallback} from 'react'
const Input = memo(function Input({onChange}){
console.log('子组件重新渲染了')
return <input type="text" onChange={(e)=> onChange(e.target.value)}>{arr}</input>
})
function App(){
// 触发父组件的重新执行【渲染】
const [count1,setCount1] = useState(0)
//---------------------核心:
const changeFn = useCallback((value)=>{
console.log(value)
},[])//大多数情况下 依赖给个空数组即可,不需要做更新。
//---------------------核心
return(
<div className="App">
{/*点击触发状态量变化,进而组件函数重新执行 渲染,子组件input跟着重新执行*/}
<button onClick={()=>setCount1(count1 + 1)}>{count1}</button>
{/*把函数当做props传给子组件*/}
<Input onChange={changeFn}/>
</div>
)
}
React.forwardRef
在父组件里想拿到组件里某个dom元素,是没有办法完成的,只能通过此方法。[使用ref暴露DOM节点给父组件。]
场景:父组件 通过ref获取到子组件内部的input元素让其聚焦。
import {useRef,forwardRef} from 'react'
// 子组件-这样写,log 的dom 元素直接报错。
// function Input(){
// return <input type="text" />
// }
// 子组件使用forwaRef函数包裹,然后把ref透传到父组件中。
const Input = forwardRef((props,ref){
return <input type="text" ref={ref}/>
})
function App(){
const inputRef = useRef(null)
const logFn = ()=>{
console.log(inputRef)
inputRef.current.focus()//让子组件里input聚焦
}
return(
<div className="App">
<Input ref={inputRef}/>
<button onClick={logFn}>focus</button>
</div>
)
}
useImperativeHandle
通过ref 暴露子组件中的方法给父组件来使用。
案例:父组件 通过ref 调用子组件里 自己的聚焦方法。
基于上个案例,父组件代码完全不用动。
import {useRef,forwardRef,useImperativeHandle} from 'react'
const Input = forwardRef((props,ref){
const inputRef = useRef(null)
// 聚焦方法
const focusFn = ()=>{
inputRef.current.focus()
}
// 要暴露出去的方法
useImperativeHandle(ref,()=>{
return {
focusFn
}
})
return <input type="text" ref={inputRef}/>
})
function App(){
const sonRef = useRef(null)
const fn = ()=>{
sonRef.current.focusFn()//调用子组件里方法
}
return(
<div className="App">
<Input ref={sonRef}/>
<button onClick={fn}>focus</button>
</div>
)
}
vue2 里直接通过ref 就可以调用子组件的方法,vue3里是需要在子组件里通过 defineExpose 方法导出。