组件触发重新渲染的条件:
1、组件自身的 state 变化。
2、父组件重新渲染了。
3、父组件传递过来的 props 发生了变化
import { FC, useState, ReactElement } from 'react';
export const Example1: FC = (): ReactElement => {
const [count, setConut] = useState<number>(0)
// useState 方法被调用后,返回一个数组,这个数组有两个元素,这两个元素分别被赋给 count 和 setConut
// setConut 是一个函数的引用,我们调用 setConut 函数达到状态更新,更新的结果就是 setConut 函数的参数
return (
<>
<p>你点击了 {count} 次</p>
<button onClick={() => setConut(count + 1)}>addCount</button>
</>
)
}
export const Example2: FC = (): ReactElement => {
const [count, setCount] = useState<number>(0)
const countAction = (preCount: number, b: number): number => { //
return preCount + b
}
return (
<>
<p>你点击了 {count}</p>
<button onClick={() => setCount(countAction(count, 2))}>click+2</button>
{/* setCount 的参数是我们上面定义的函数 countAction,我们利用函数返回一个新的 count 的值*/}
{/* 这样我们就可以在更新 count 的值的时候另外写一个逻辑去更新 count 的值 */}
</>
)
}
useState更新不及时问题:
const [currentIndex, setCurrentIndex] = useState(0)
const handleLeft = () => {
setCurrentIndex(currentIndex+ 1)
console.log(currentIndex)
}
因为他执行的是异步更新,目的是减少render的次数,解决方法:
const currentRef = useRef(0)
const handleLeft = () => {
currentRef.current +=1
console.log(currentRef.current)
}
或者使用useEffect实现更新,将 state 写到 Effect 的依赖数组项中
function Example(){
const [count, setCount] = useState(0) // useState 值的改变会让组件重新刷新
const [data, setData] = useState([]);
// useEffect 在 Example 组件第一次渲染完毕后执行,它与类中 componentDidMount 生命周期钩子类似,而且在组件更新的时候(也就类似于 componentDidUpdate 生命周期钩子执行的时候)执行,但它相比生命周期函数发生稍晚一点
useEffect(()=> {
document.title = `you clicked ${count} times` // 他就相当于函数作用域之外的东西、或者这个函数调用了另一个函数这都是函数的副作用
console.log('count', count);
setData([{id: 1, name: '张三'}])
}) //
useEffect(()=>{
console.log('我只渲染一次')
},[]) // 因为 [] 始终保持不变,所以他只会在组件渲染完毕执行一次,相当于 componentDidMount,而组件更新(componentDIdUpdata)的时候就不执行
useEffect(()=> {
console.log('在count的状态更新的时和组件实例挂载完毕时我会渲染')
return ()=>{ // 清理函数,当组件卸载的时候对副作用进行清理,例如这个函数打开数据库,那么这个函数就是执行关闭数据库、清空回调函数、对 window 添加了事件监听 return 移除对 window 的事件监听等等,路由的跳转/页面的刷新/页面的关闭都会卸载组件
}
},[count]) // 最少会渲染一次,之后在 count 值发生变化的时候才会去渲染
return <>
{console.log('我渲染了2次')} // 因为第一次渲染完毕去执行 useEffect 的时候执行了 setData 改变的 data 的状态,所以 Example 组件重新更新了,所以渲染了 2 次
<p>you clicked ${count} times</p>
<Button onClick={()=>{setState( count +1 )}}></Button>
{data.map((item)=>{
return <p key= {item.id}> {item.name} </p>
})}
</>
}
useEffect 副作用:他会影响到你的作用域之外的变量、数据库的链接、屏幕的刷新等等
// useReducer 就是自定义的 useState, 执行了比较复杂的状态更新
// 就是三个函数的作用,init 函数(设置初值)、dispatch 函数(更新 state)与 reducer 函数(返回新的 state),
// reducer 可以实现 redux 功能
function reducer( state, action){ // 输入旧的 state,返回新的 state
switch(action.type){
case: 'reset' :
return { count: action.payload }
case: 'increament' :
return { count: state.count + 1 }
case: 'decreament' :
return { count: state - 1 }
default:
return state
}
}
function init({initalstate}) { // 通过 init 初始化 state
return { count: initalstate.count + 1 }
}
// useReducer 返回了只有两个元素的数组,解构赋值,一个赋值给 state,一个赋值给 dispatch
function Counter(initialcount){
// useReducer 有三个参数,init 是一个函数,它会最初被执行,输入初始 state 返回新的 state,替换初值 initialcount,reducer 也是一个函数,定义任务如何处理
const [state, dispatch] = useReducer(reducer, initialcount, init)
return (
<>
Count: { state.count }
<Button onClick={ ()=>{ dispatch(type:'reset', payload:initialcount.count )} }>Reset</Button> // 通过 dispatch 更新 state,dispatch 就是任务的分配
<Button onClick={ ()=>{ dispatch(type: 'increment')} }>increment</Button>
<Button onClick={ ()=>{ dispatch(type: 'decrement')} }>decrement</Button>
</>
)
}
const App ()=>{
<Counter initialcount={{count: 1}} />
}
旧版 context API 有一个重大的设计缺陷(传递截断)
所谓的“传递截断”就是指在如果在使用旧版 context API 来跨层级去传递 props 过程中,假如承载了数据源的父组件和最终接收数据的子组件之间的某个组件通过 shouldComponentUpdate 来跳过自己的更新的话(shouldComponentUpdate 函数里面 return false),那么子组件也会被动跳过更新,因而无法拿到最新的 prop 值。
新版 Context API
只要某个子组件订阅了 context 对象实例所承载的数据,只要这些数据发生了改变,不管该子组件的上层组件做了什么,这个子组件都会得到更新。
新版 Context 存在的问题
Context 也是将底部状态控制交到了顶部组件,当顶部组件状态更新的时候,一定会触发所有子组件的重新渲染,也会带来损耗,我们可以通过一些手段来避免,但是项目较大时,我们需要花太多的精力去做这件事。
context 用法
import React from 'react'
const myContext = React.createContext() // 创建一个上下文容器
// useCotext 是父组件给子组件传值的时候用的,原本 react 父子组件间传值使用 props 一级一级的传递,而 context 可以生成一个数据,把它提供给这个组件所包含的所有的子组件
const Com1 = () =>{
const [count, setCount] = React.useContext(myContext) // count 与 setCount 是从父组件拿到的功能,能正常使用
return (
<>
Count:{ count }
<Button onClick = ()=>{ setCount(count + 1) } > count+1 </Button>
</>
)
}
const App = ()=>{
const [count, setCount] = useState(0)
return (
<myContext.Provider value={{count, setCount}}> //value 中是要共享的值
<div>
<Com1 /> // 这个子组件内部就可以 useContext 拿到所共享的值
</div>
</myContext.Provider>
)
}
最后推荐一篇不错的文章:hooks 的缺点
1、useCallback 就是让组件内所定义的函数是一个固定的引用,而不是每次组件发生重新渲染的时候都生成一个新的函数
2、useCallback 的第二个参数是一个数组,里面的依赖项发生变化函数的引用才会变化
3、它并不是使用越多越好,只有真正需要用到的场景才能去使用,因为调用react.useCallback 也是一笔资源的开销,性能反而会变差,这个说法仁者见仁智者见智,个人感觉,它的好处比坏处大得多
唉,写不动了
这有一篇好文章推荐:你不知道的 useCallback
useCallback 使用场景:
1、子组件中的函数只要是父组件传进来的,这个函数都用 useCallback 包一下,能优化性能。
2、函数的节流、防抖
防抖:useCallback 缓存防抖函数,不然防抖不起作用
import { FC, memo, ReactElement, useCallback, useState } from 'react';
import _ from 'lodash'
const InputDebounce: FC = (): ReactElement => {
const [name, setName] = useState('')
const getValue = (e: any) => {
const { value } = e.target
setName(value)
searchDebounce(value)
}
const search = (val: string) => {
console.log(val)
}
// 如果防抖函数不用 debounce 包一下,那么当调用 setName 的时候组件会发生重新渲染,相当于每次 debounce 都是新的函数,防抖没起作用,所以需要 useCallback 缓存一下
const searchDebounce = useCallback(_.debounce(search, 300), [])
return (
<>
<input type="text" onChange={getValue} />
</>
)
}
export default memo(InputDebounce)
1、useMemo 的写法和 useCallBack 一样,它接受两个参数,函数和数组依赖,只有依赖项变化的时候他才会重新调用函数计算,如果不使用它,那么组件每次 render 的时候,都会去调用函数计算,如果是一个很耗时的计算那么页面就会出现卡顿状态
2、这个函数是在渲染的时候去执行的,尽量不要在里面做一些渲染的时候不会做的事情,例如 setState
3、我们在函数中用到的值都应该写到依赖数组中去,不然拿到的值永远都是最开始的值,不会变化更新
4、它与 useCallbak 的区别是,useCallback 缓存的是一个函数,而 useMemo 缓存的是一个变量(值)。
import React, { FC, ReactElement, useState } from 'react'
import ClickCount from './count'
const TodoList: FC = (): ReactElement => {
const [count, setCount] = useState(0)
const [score, setScore] = useState(0)
const countClick = () => {
setCount(count + 1)
}
const scoreClick = () => {
setScore(score + 2)
}
return (
<div>
<button onClick={countClick}>次数点击</button>
<br />
<ClickCount count={count} />
<br />
<button onClick={scoreClick}>分数点击</button>
<p>你得到的分数{score}</p>
</div>
)
}
export default TodoList
ClickCount 组件
import { FC, memo, ReactElement, useMemo } from 'react';
interface IProps {
count: number
}
const ClickCount: FC<IProps> = ({ count }): ReactElement => {
const ClickCount = useMemo(() => {
return count * 1000 // 假装这是在执行复杂计算
}, [count]) // 当 count 改变的时候返回依赖 count 处理过的新值 ClickCount
console.log('子组件渲染了')
return (
<>
<p>你点击了 {ClickCount} 次</p> // 依赖 count 处理过的新值
</>
)
}
export default memo(ClickCount) // 使用 memo:在 props 不变的情况下,本子组件不随父组件的重新渲染而重新渲染
作用:
1、保存变量。
2、获取 JSX 中 DOM 元素,利用拿到 Input 元素操作 value 的值。
import { FC, memo, ReactElement, useRef, useState } from 'react';
const Interval: FC = (): ReactElement => {
const [count, setCount] = useState<number>(0)
// let timer: any = null
// 因为定时器中有执行 setState 的操作,引发子组件更新,更新导致每次 timer 都是新定义的一个值
// 所以他在 start 中的赋值操作不能被保存下来,达不到清除定时器的目的
// 利用 useRef 保存 timer 的赋值,每次组件重新渲染 timer 取被保存下来的值,不重新定义赋值
let timer = useRef<any>()
const start = () => {
if (!timer.current) { // 防止重复开启定时器
timer.current = setInterval(() => {
// setCount(count + 1) // 因为 state 的更新必须在这个函数结束之后才有效,所以不能使用 count + 1,他不能使 count 立即得到更新
setCount(c => c + 1)
}, 500)
}
}
const stop = () => {
if (timer.current) {
clearInterval(timer.current)
timer.current = null
}
}
const reset = () => {
stop()
setCount(0)
}
// 通过 ref 拿到 input DOM
const Show: FC = (): ReactElement => {
const inputEl = useRef<any>(null)
const onButtonClick = () => {
console.log(inputEl.current.value)
inputEl.current.value = ''
}
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>show input content</button>
</>
)
}
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>show input content</button>
</>
)
}
return (
<>
<p>数字 {count}</p>
<button onClick={start}>计数开始</button>
<button onClick={stop}>计数暂停</button>
<button onClick={reset}>重置</button>
</>
)
}
export default memo(Interval)
forwardRef 知识为下面的 useImperativeHandle 做准备
作用:
将父组件的 ref 传递给子组件,那么父组件就可以利用 ref 拿到子组件的 DOM、状态、函数等,达到操作子组件的目的。
useImperativeHandle 有 3 个参数,第一个参数:父组件传递过来的 ref、第二个参数:回调函数,这个回调函数返回一个对象,对象里面包括了开放给父组件的功能,通过父组件的 ParentRef.current 就能访问到这个对象、第三个参数:依赖数组,数组中的值发生变化传递给父组件的对象才会更新
父组件
import React, { FC, ReactElement, useRef, useState } from 'react'
import ActivitiesTab from './count/index'
const TodoList: FC = (): ReactElement => {
const ActivitiesTabRef = useRef<any>()
const [getContent, setGetContent] = useState<any>()
const getChildInputVal = () => {
ActivitiesTabRef.current?.refresh() // 父组件直接通过 ref 拿到子组件开放的函数
setGetContent(Math.random()) // 更改父组件的状态,让父组件重新渲染
console.log(ActivitiesTabRef.current?.content) // 拿到子组件传递给父组件的状态 content
}
const getFoucs = () => {
ActivitiesTabRef.current?.focus() // 让子组件的输入框聚焦
console.log(ActivitiesTabRef.current?.getVal()) // 拿到子组件输入框的 value
}
return (
<div>
<ActivitiesTab ref={ActivitiesTabRef} />
<p>展示子组件的 content:{ActivitiesTabRef.current?.content}</p>
<button onClick={getChildInputVal}>更新</button>
<button onClick={getFoucs}>聚焦</button>
</div>
)
}
export default TodoList
子组件
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'
const ActivitiesTab = (_p: any, ref: any) => { // ref: 子组件通过 forWardRef 接收到从父组件传递过来的 ref
const [content, setContent] = useState<any>('子组件的 content')
const inputRef = useRef<any>() // 子组件自己的 ref
const refresh = (): void => {
console.log(inputRef.current.value)
setContent(Math.random())
}
useImperativeHandle(
ref, () => ({ // 这个函数返回一个对象,对象中有要传递给父组件的信息
refresh, // refresh: refresh 简写
content,
getVal: () => inputRef.current.value,
focus: () => inputRef.current.focus(),
}), [content]
)
return (
<>
<input ref={inputRef} />
</>
);
}
export default forwardRef(ActivitiesTab);
这个组件平时在开发业务时用得较少,它的执行和 useEffect 几乎一致,但是早于 useEffect 执行,官网说,尽量使用 useEffect,当 useEffect 解决不了问题时再考虑使用 useLayoutEffect
子组件
import { forwardRef, useEffect, useLayoutEffect } from 'react'
const ActivitiesTab = () => {
useEffect(() => {
console.log('useEffect')
return () => {
console.log('useEffect 销毁')
}
})
useLayoutEffect(() => { // 当没有依赖数组时,useLayoutEffect 先执行销毁,再执行回调
console.log('useLayoutEffect')
return () => {
console.log('useLayoutEffect 销毁')
};
})
return (
<>
<input />
</>
);
}
export default ActivitiesTab;
父组件
import React, { FC, ReactElement, useState } from 'react'
import ActivitiesTab from './count/index'
const TodoList: FC = (): ReactElement => {
const [getContent, setGetContent] = useState<any>()
const refresh = () => {
setGetContent(Math.random()) // 更改父组件的状态,让父组件重新渲染
}
return (
<div>
<ActivitiesTab />
<button onClick={refresh}>更新</button>
</div>
)
}
export default TodoList