最易懂-react18入门系列六

110 阅读6分钟

摘要

本篇主要是基础拓展篇。

  • useReducer
  • useMemo
  • React.memo
  • useCallback
  • React.forwardRef
  • useImperativeHandle

useReducer

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

我们同样来实现一个 计数器,count 的自增、自减、重置。

基础用法:

也是有固定的写法的.

  1. 定义一个reducer函数,根据不同的action返回不同的新状态

  2. 在组件中调用useReducer,并传入reducer函数和状态的初始值

  3. 事件发生时,通过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 方法导出。