React hook 基本使用

52 阅读24分钟

React hook 基本使用

(1). State Hook让函数组件也可以有state状态, 并进行状态数据的读写操作
(2). 语法: const [xxx, setXxx] = React.useState(initValue)  
(3). useState()说明:
        参数: 第一次初始化指定的值在内部作缓存
        返回值: 包含2个元素的数组, 第1个为内部当前状态值, 第2个为更新状态值的函数
(4). setXxx()2种写法:
        setXxx(newValue): 参数为非函数值, 直接指定新的状态值, 内部用其覆盖原来的状态值
        setXxx(preValue => newValue): 参数为函数, 接收原本的状态值, 返回新的状态值, 内部用其覆盖原来的状态值

1、useState

import React, { useState } from 'react'

/**
 * @description: useState hook的简单使用
 */
const UseStateDemo: React.FC = () => {
  const [count, setCount] = useState<number>(0)

  const handleClick = () => {
    setCount(count + 1)
    // 或者
    // setCount(preCount => preCount + 1)
  }

  return (
    <>
      Count: {count}
      <button onClick={handleClick}>点我+1</button>
    </>
  )
}

export default UseStateDemo

模拟useState源码

const _state: any[] = []
let _index = 0

function render() {
  ReactDOM.createRoot(document.getElementById('container')).render(<TestUseState />)
  _index = 0 // 每更新一次都需要将_index归零,才不会不断重复增加_state
}

type SetStateAction<T> = T | ((prevState: T) => T)
type Dispatch<T> = (value: T) => void

function useState<T>(initialState: T): [T, Dispatch<SetStateAction<T>>] {
  const curIndex = _index // 记录当前操作的索引
  _state[curIndex] = _state[curIndex] === undefined ? initialState : _state[curIndex]

  const setState = (newState: SetStateAction<T>) => {
    if (newState instanceof Function) {
      _state[curIndex] = newState(_state[curIndex])
    } else {
      _state[curIndex] = newState
    }
    // 重新渲染
    render()
  }
  _index += 1 // 下一个操作的索引
  return [_state[curIndex], setState]
}

2、useEffect

(1). Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)
(2). React中的副作用操作:
        发ajax请求数据获取
        设置订阅 / 启动定时器
        手动更改真实DOM
(3). 语法和说明: 
        useEffect(() => { 
          // 在此可以执行任何带副作用操作
          return () => { // 在组件卸载前执行
            // 在此做一些收尾工作, 比如清除定时器/取消订阅等
          }
        }, [stateValue]) // 如果指定的是[], 回调函数只会在第一次render()后执行
        [count] //1、回调函数会在第一次render()后执行 // 2、count的数据发生变化后会调用
    
(4). 可以把 useEffect Hook 看做如下三个函数的组合
        componentDidMount()
        componentDidUpdate()
    	componentWillUnmount() 
import React, { useEffect, useState } from 'react'

export default function UseEffectDemo() {

  const [count, setCount] = useState<number>(0)

  const [hotState, setHotState] = useState<boolean>(false)

  //组件第一次渲染后或状态发生变化后执行(相当于监听了组件所有数据,第一次渲染后或当数据发生变化都会执行)
  useEffect(() => {
    //解决生命周期函数 代替了componentDidMount和componentDidUpdate。
    //分别在组件第一次渲染后在浏览器控制台打印出计数器结果和在每次计数器状态发生变化后打印出结
    console.log(`0-组件第一次渲染后或状态发生变化后执行`)
  })

  //只在第一次渲染完成后执行 发送网络请求
  useEffect(() => {
    //解决生命周期函数 代替了componentDidMount
    console.log(`1-只在第一次渲染完成后执行 count:${count}  hotState:${hotState}`)
  }, [])

  //只在第一次渲染完成后执行或count发生改变后执行
  useEffect(() => {
    //解决生命周期函数 代替了componentDidMount和componentDidUpdate
    console.log(`2-只在第一次渲染完成后执行或count发生改变后执行 count:${count}  hotState:${hotState}`)
  }, [count])


  //组件第一次渲染后或状态(count,hotState)发生变化后执行
  useEffect(() => {
    //解决生命周期函数 代替了componentDidMount和componentDidUpdate。
    //分别在组件第一次渲染后在浏览器控制台打印出计数器结果和在每次计数器状态发生变化后打印出结
    console.log(`3-组件第一次渲染后或状态(count,hotState)发生变化后执行 count:${count}  hotState:${hotState}`)
  }, [count, hotState])

  return (
    <div>
      <div>使用React Hooks</div>
      <p>总数:{count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <button onClick={() => setHotState(!hotState)}>改变状态</button>
    </div>
  )
}
//模拟componentWillUnmount
import React, { useEffect } from 'react'
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'

const Index = () => {
  useEffect(() => {
    // componentDidMount
    console.log('useEffect=>老弟你来了!Index页面')
    const timeId = setInterval(() => {
      console.log('setInterval---------', timeId);
    }, 1000)
    return () => {
      //返回一个函数的形式,代替解绑生命周期函数 componentWillUnmount 组件将要被卸载时执行
      console.log('老弟,你走了!Index页面', timeId)
      clearInterval(timeId)
    }
  }, [])
  return <div>加油,程序员</div>
}


const List = () => {
  return (
    <ul>
      <li>你好</li>
      <li>我好</li>
      <li>他好</li>
    </ul>
  )
}


export default function UseEffectDemo() {
  return (
    <BrowserRouter>
      <Link to="/">首页 </Link>
      <Link to="/list/">列表页 </Link>
      <Routes>
        <Route path="/" element={<Index />}></Route>
        <Route path="/list/" element={<List />}></Route>
      </Routes>
    </BrowserRouter>
  )
}

useEffect 与 useLayoutEffect的区别

区别

3、useContext

useContext:解决的是父子及祖孙组件之间值传递的问题

const CarContext = React.createContext()
//父组件
<CarContext.Provider value={{ carName: '宝马x9' }}>
    <ChildContext />
</CarContext.Provider>
// 子组件
const { carName } = useContext(CarContext)

Parent.jsx

import React, { useState } from 'react'
import ChildContext from '../MyContext/Child'
import './index.css'

export const CarContext = React.createContext()

const Parent = () => {
  console.log('Parent---render')

  const [state, setState] = useState({
    carName: '奔驰c36',
    stus: ['小张', '小李', '小王'],
  })

  const addStu = () => {
    const { stus } = state
    stus.unshift('小刘')
    setState((state) => ({ ...state, stus: [...stus] }))
  }

  const changeCar = () => {
    setState((state) => ({ ...state, carName: '迈巴赫' }))
  }

  return (
    <div className="parent">
      <h3>我是Parent组件</h3>
      <div>{state.stus}</div>
      <span>我的车名字是:{state.carName}</span>
      <br />
      <button onClick={changeCar}>点我换车</button>
      <button onClick={addStu}>添加一个小刘</button>
      <CarContext.Provider value={{ carName: state.carName }}>
        <ChildContext />
      </CarContext.Provider>
    </div>
  )
}
export default Parent

Child.jsx

import React, { useContext } from 'react'
import { CarContext } from './index'

const Child = () => {
  console.log('Child--组件')
  const { carName } = useContext(CarContext)
  return (
    <div className="child">
      <h3>我是Child组件</h3>
      <span>我接到的车是:{carName}</span>
    </div>
  )
}
export default Child

ts版

import React, { createContext, useContext, useState } from 'react'

interface IProps {
  carName: string;
  stus: string[];
}

const initState: IProps = {
  carName: '',
  stus: []
}

export const AppContext = createContext<IProps>(initState)

const Child = () => {
  console.log('Child--组件')
  const carInfo: IProps = useContext(AppContext)
  const { carName } = carInfo
  return (
    <div className="child">
      <h3>我是Child组件</h3>
      <span>我接到的车是:{carName}</span>
    </div>
  )
}

const Parent = () => {
  console.log('Parent---render')

  const [state, setState] = useState<IProps>({
    carName: '奔驰c36',
    stus: ['小张', '小李', '小王'],
  })

  const addStu = () => {
    const { stus } = state
    stus.unshift('小刘')
    setState((state) => ({ ...state, stus: [...stus] }))
  }

  const changeCar = () => {
    setState((state) => ({ ...state, carName: '迈巴赫' }))
  }

  return (
    <div className="parent">
      <h3>我是Parent组件</h3>
      <div>{state.stus}</div>
      <span>我的车名字是:{state.carName}</span>
      <br />
      <button onClick={changeCar}>点我换车</button>
      <button onClick={addStu}>添加一个小刘</button>
      <AppContext.Provider value={state}>
        <Child />
      </AppContext.Provider>
    </div>
  )
}
export default Parent

4、useReducer

useReducer 是 useState的代替方案,用于 state 复杂变化 const [state, dispatch] = useReducer(reducer, initialState); state:获取的状态 dispatch:负责派发action通知reducer更新数据
reducer:状态管理者,根据传入的action来进行不同的数据操作 initialState:初始化状态值

返回值是一个数组,[state,dispatch] 参数1:reducer 是一个回调函数,参数state, action 参数2:初始值

// 使用方式  
const [count, dispatch] = useReducer((state, action) => {
    const { type, data } = action
    switch (type) {
      case 'add':
        return state + data
      case 'sub':
        return state - data
      default:
        return state
    }
  }, initValue)
  
  // 调用
  dispatch({ type: 'sub', data: 1 })

使用useReducer

import React, { useReducer, useRef } from 'react'

// state属性类型
interface RState {
  id: number
  name: string
}

// action接口类型
interface RAction {
  type: 'add' | 'delete'
  data: RState
}

//初始值
const initState: RState[] = []

const reducer = (state: RState[], action: RAction): RState[] => {
  const { type, data } = action
  // return的值为新的state
  switch (type) {
    case 'add': {
      return [...state, data]
    }
    case 'delete': {
      const { id } = data
      return state.filter(item => item.id !== id)
    }
    default:
      throw new Error()
  }
}

// useReducer完成用户列表动态渲染添加及删除
const UseReducerDemo: React.FC = () => {
  const [personList, dispatch] = useReducer(reducer, initState)

  const inputRef = useRef<HTMLInputElement>(null)

  // 添加事件
  const handleAddClick = () => {
    // 元素节点不存在或者值为''
    if (!inputRef.current || !inputRef.current.value) return

    // 加工数据
    const person: RState = {
      id: Date.now(),
      name: inputRef.current.value,
    }
    const action: RAction = { type: 'add', data: person }

    // 派发更新
    dispatch(action)

    // 清空输入框
    inputRef.current.value = ''
  }

  // 删除事件
  const handDelete = (person: RState) => {
    const action: RAction = { type: 'delete', data: person }
    // 派发更新
    dispatch(action)
  }

  return (
    <>
      用户名:
      <input type="text" ref={inputRef} /> <button onClick={handleAddClick}>添加</button>
      <hr />
      用户列表:
      <ul>
        {personList.map((person: RState) => (
          <li key={person.id}>
            id:{person.id} ----------- 姓名:{person.name}
            <button onClick={() => handDelete(person)}>删除</button>
          </li>
        ))}
      </ul>
    </>
  )
}
export default UseReducerDemo

(掌握)useReducer useContext实现redux的状态管理和状态共享

useContextuseReducer合作可以完成类似的Redux库的操作,useReducer 可以让代码具有更好的可读性和可维护性,它类似于Redux中的reducer ,reducer 这个函数接收两个参数,一个是状态,一个用来控制业务逻辑的判断参数。 实现状态全局化并能统一管理,统一个事件的派发 案例:todo增删

5、useMemo

useMemo是针对一个函数,是否多次执行
useMemo主要用来解决使用React hooks产生的无用渲染的性能问题
在方法函数,由于不能使用shouldComponentUpdate处理性能问题,react hooks新增了useMemo

useMemo使用
	如果useMemo(fn, arr)第二个参数匹配,并且其值发生改变,才会多次执行执行,否则只执行一次,如果为空数组[],fn只执行一次

useMemo(()=>{
//此处代码每次都会调用 默认监控所有的状态
})

useMemo(()=>{
//此处代码只会调用一次
},[])

const res = useMemo(()=>{
//此处代码会默认调用一次,当自身的age发生变化时,也会触发
return {age}
},[age])

useMemo 我们来看一个反例:

import React, { useState } from 'react'
export default function WithoutMemo() {
  const [count, setCount] = useState(1)
  const [val, setValue] = useState('')

  function expensive() {
    console.log('compute')
    let sum = 0
    for (let i = 0; i < count * 100; i++) {
      sum += i
    }
    return sum
  }

  return (
    <div>
      <h4>
        {count}-{val}-{expensive()}
      </h4>
      <div>
        <button onClick={() => setCount(count + 1)}>+c1</button>
        <input value={val} onChange={(event) => setValue(event.target.value)} />
      </div>
    </div>
  )
}

这里创建了两个state,然后通过expensive函数,执行一次昂贵的计算,拿到count对应的某个值。我们可以看到:无论是修改count还是val,由于组件的重新渲染,都会触发expensive的执行(能够在控制台看到,即使修改val,也会打印);但是这里的昂贵计算只依赖于count的值,在val修改的时候,是没有必要再次计算的。在这种情况下,我们就可以使用useMemo,只在count的值修改时,执行expensive计算:

import React, { useState, useMemo } from 'react'

export default function WithMemo() {
  const [count, setCount] = useState(1)
  const [val, setValue] = useState('')

  //优化后代码
  const expensive = useMemo(() => {
    console.log('compute')
    let sum = 0
    for (let i = 0; i < count * 100; i++) {
      sum += i
    }
    return sum
  }, [count])

  return (
    <div>
      <h4>
        {count}-{expensive}
      </h4>
      {val}
      <div>
        <button onClick={() => setCount(count + 1)}>+c1</button>
        <input value={val} onChange={(event) => setValue(event.target.value)} />
      </div>
    </div>
  )
}

上面我们可以看到,使用useMemo来执行昂贵的计算,然后将计算值返回,并且将count作为依赖值传递进去。这样,就只会在count改变的时候触发expensive执行,在修改val的时候,返回上一次缓存的值。

6、useCallback

讲完了useMemo,接下来是useCallback。useCallback跟useMemo比较类似,但它返回的是缓存的函数。我们看一下最简单的用法:

const fnA = useCallback(fnB, [a])

上面的useCallback会将我们传递给它的函数fnB返回,并且将这个结果缓存;当依赖a变更时,会返回新的函数。既然返回的是函数,我们无法很好的判断返回的函数是否变更,所以我们可以借助ES6新增的数据类型Set来判断,具体如下:

import React, { useState, useCallback } from 'react'

const set = new Set()

export default function Callback() {
  const [count, setCount] = useState(1)
  const [val, setVal] = useState('')

  const callback = useCallback(() => {
    console.log(count)
  }, [count])

  set.add(callback)

  return (
    <div>
      <h4>{count}</h4>
      <h4>{set.size}</h4>
      <div>
        <button onClick={() => setCount(count + 1)}>+</button>
        <input value={val} onChange={(event) => setVal(event.target.value)} />
      </div>
    </div>
  )
}

我们可以看到,每次修改count,set.size就会+1,这说明useCallback依赖变量count,count变更时会返回新的函数;而val变更时,set.size不会变,说明返回的是缓存的旧版本函数。

知道useCallback有什么样的特点,那有什么作用呢?

使用场景是:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。

import React, { useState, useCallback, useEffect } from 'react'

function Parent() {
  const [count, setCount] = useState(1)
  const [val, setVal] = useState('')

  const callback = useCallback(() => {
    return count
  }, [count])

  return (
    <div>
      <h4>{count}</h4>
      <Child callback={callback} />
      <div>
        <button onClick={() => setCount(count + 1)}>+</button>
        <input value={val} onChange={(event) => setVal(event.target.value)} />
      </div>
    </div>
  )
}

function Child({ callback }) {
  const [count, setCount] = useState(() => callback())
  useEffect(() => {
    console.log('useEffect----')
    setCount(callback())
  }, [callback])
  return <div>{count}</div>
}

export default Parent

useMemo和useCallback的区别 及使用场景

回顾

在介绍一下这两个hooks的作用之前,我们先来回顾一下react中的性能优化。在hooks诞生之前,如果组件包含内部state,我们都是基于class的形式来创建组件。当时我们也知道,react中,性能的优化点在于:
	1、调用setState,就会触发组件的重新渲染,无论前后的state是否不同
	2、父组件更新,子组件也会自动的更新
基于上面的两点,我们通常的解决方案是:使用immutable进行比较,在不相等的时候调用setState;
在shouldComponentUpdate中判断前后的props和state,如果没有变化,则返回false来阻止更新。

useMemo和useCallback主要用来解决使用React hooks产生的无用渲染的性能问题,函数型组件没有shouldCompnentUpdate(组件更新前触发),我们就没有办法通过组件前的条件来决定组件是否更新。而且,在函数组件中,react不再区分mount和update两个状态,这意味着函数组件的每一次调用都会执行其内部的所有逻辑,那么会带来较大的性能损耗。

对比

共同作用:
    1.仅仅 依赖数据 发生变化, 才会重新计算结果,否则都返回缓存的值,也就是起到缓存,优化性能的作用。
    2.useMemo 和 useCallback 接收的参数都是一样,第一个参数为回调 第二个参数为要依赖的数据
	3.useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;
区别在于
	useMemo返回的是函数运行的结果,useCallback返回的是函数

自定义hook 监听浏览器大小

import React, { useState, useEffect, useCallback } from 'react'

//监听浏览器窗口大小的hook
const useWinSize = () => {
  // 初始化数据
  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight,
  })

  //useCallback, 目的是为了缓存方法(useMemo是为了缓存变量) [] 只执行一次
  const onResize = useCallback(() => {
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight,
    })
  }, [])

  // 组件挂在完后调用
  useEffect(() => {
    window.addEventListener('resize', onResize)
    return () => {
      window.removeEventListener('resize', onResize)
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  return size
}

//组件中使用
const MyHooks = () => {
  const size = useWinSize()
  return (
    <div>
      size:{size.width}x{size.height}
    </div>
  )
}
export default MyHooks

7、useRef

(1). Ref Hook可以在函数组件中存储/查找组件内的标签或任意其它数据
(2). 语法: const refContainer = useRef()
(3). 作用:保存标签对象,功能与React.createRef()一样
import React, { useState, useEffect, useRef } from 'react'
import ReactDOM from 'react-dom'

const Demo = () => {
  const [count, setCount] = useState(0)
  //声明一个input的element
  const inputEl = useRef(null)

  useEffect(() => {
    let timer = setInterval(() => {
      setCount((count) => count + 1)
    }, 1000)
    return () => {
      clearInterval(timer)
    }
  }, [])

  //加的回调
  function add() {
    //setCount(count+1) //第一种写法
    setCount((count) => count + 1)
  }

  //提示输入的回调
  function show() {
    alert(inputEl.current.value)
  }

  //卸载组件的回调
  function unmount() {
    ReactDOM.unmountComponentAtNode(document.getElementById('root'))
  }

  return (
    <div>
      <input type="text" ref={inputEl} />
      <h2>当前求和为:{count}</h2>
      <button onClick={add}>点我+1</button>
      <button onClick={unmount}>卸载组件</button>
      <button onClick={show}>点我提示数据</button>
    </div>
  )
}

export default Demo

useRef与createRef的区别

  • 在一个组件的正常的生命周期中可以大致分为3个阶段:

  • 从创建组件到挂载到DOM阶段。初始化props以及state, 根据state与props来构建DOM 组件依赖的props以及state状态发生变更,触发更新 销毁阶段 第一个阶段,useRef与createRef没有差别

  • 第二个阶段,createRef每次都会返回个新的引用;而useRef不会随着组件的更新而重新创建

  • 第三个阶段,两者都会销毁

8、forwardRef

将父组件的ref转发给子组件,用于操作子组件的dom

import { useRef, forwardRef } from 'react'

interface IProps {}

const ChildInput = forwardRef<HTMLInputElement, IProps>((props, ref) => {
  console.log(props)
  return (
    <>
      <h3>ChildInput 组件 </h3>
      <input type="text" ref={ref} />
    </>
  )
})

const Father: React.FC = () => {
  const inputEl = useRef<HTMLInputElement>(null)

  // 打印子组件输入框的值
  const printValue = () => {
    console.log(inputEl.current?.value)
  }

  // 改变子组件输入框的值
  const changeValue = () => {
    if (inputEl.current) inputEl.current.value = '哈哈哈'
  }

  return (
    <div>
      <h1>我是父组件</h1>
      <button onClick={printValue}>打印子组件input值</button>
      <button onClick={changeValue}>改变子组件input值</button>
      <hr />
      <ChildInput ref={inputEl} />
    </div>
  )
}

export default Father

9、useImperativeHandle Hook

通过ref和forwardRef,可以在父组件中随意改变元素。
但是我们可能只希望父组件只能对子组件进行有限操作,也就是限制父组件的自由度。

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值,大多数情况下,应当避免使用 ref 这样的命令式代码,useImperativeHandle 应当与 forwardRef 一起使用。

useImperativeHandle(ref, createHandle, [deps])

参数1:ref:定义 current 对象的 ref 参数2:createHandle:一个函数,返回值是一个对象,即这个 ref 的 current对象 参数3: [deps]:即依赖列表,当监听的依赖发生变化,useImperativeHandle 才会重新将子组件的实例属性输出到父组件ref 的 current 属性上,如果为空数组,则不会重新输出。如果不传,每次获取都是重新输出。

import React, { useRef, forwardRef, useState, useImperativeHandle } from 'react'

/**
 * 子组件input事件属性接口
 */
interface ChildInputActionProps {
  onFocus: () => void
  onChangeValue: (value: string) => void
  getInputValue: () => string
}

/**
 * 子组件 pops属性接口
 */
interface ChildInputProps {
  name: string
}

const ChildInput = forwardRef<ChildInputActionProps, ChildInputProps>((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null)
  const [inputValue, setInputValue] = useState<string>('')
  useImperativeHandle(
    ref,
    (): ChildInputActionProps => {
      const exportToFatherRef = {
        // 声明方法用于聚焦input框
        onFocus: () => {
          if (inputRef.current) inputRef.current.focus()
        },
        // 声明方法用于改变input的值
        onChangeValue: (value: string) => {
          setInputValue(value)
        },
        // 获取当前子组件input框的数据
        getInputValue: () => {
          return inputValue
        },
      }
      // 返回值作为暴露给父组件的 ref 对象
      return exportToFatherRef
    },
    // 只有inputValue发生变化 父组件才能获取到最新的数据,否则只能获取到初始数据
    [inputValue],
  )
  return (
    <div>
      <p>props.name:{props.name}</p>
      <input placeholder="请输入内容" ref={inputRef} value={inputValue} onChange={e => setInputValue(e.target.value)} />
    </div>
  )
})

// 父组件
const UseImperativeHandleDemo: React.FC = () => {
  const childInputEl = useRef<ChildInputActionProps>(null)

  // 打印子组件输入框的值
  const handleChildInputFocus = () => {
    if (!childInputEl.current) return
    childInputEl.current.onFocus()
  }

  // 改变子组件输入框的值
  const handleChangeChildInputValue = () => {
    if (!childInputEl.current) return
    childInputEl.current.onChangeValue('哈哈哈哈')
  }

  // 改变子组件输入框的值
  const handlePrintChildInputValue = () => {
    if (!childInputEl.current) return
    alert(childInputEl.current.getInputValue())
  }

  return (
    <div>
      <h1>我是父组件</h1>
      <button onClick={handleChildInputFocus}>子组件input获取焦点</button>
      <button onClick={handlePrintChildInputValue}>打印子组件input值</button>
      <button onClick={handleChangeChildInputValue}>改变子组件input值</button>
      <hr />
      <ChildInput ref={childInputEl} name={'哈哈哈'} />
    </div>
  )
}

export default UseImperativeHandleDemo

性能优化

1、memo的应用

React.memo 为高阶组件。它与React.PureComponent非常相似。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。这与shouldComponentUpdate 方法的返回值相反。

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
}
export default React.memo(MyComponent, areEqual);

问题

需求:

​ 父组件中引用子组件,且不传递任何数据给子组件

发现问题:

​ 每次IndexFC组件中的count发生变化都会导致子组件被重新渲染。

结论:

​ 父组件没有传递数据给子组组件,父组件的数据发生变化都会导致子组件重新渲染。

import React, { useEffect, useState } from 'react'

//首页组件
const IndexFC: React.FC = () => {

  useEffect(() => {
    console.log('Index--------------');
  })

  const [counter, setCounter] = useState<number>(0)

  const addCounter = () => {
    setCounter(counter => counter + 1)
  }

  return (
    <>
      <h1>购买数量为: {counter}</h1>
      <button onClick={addCounter}>数量加1</button>
      <hr />
      <ChildFC />
    </>
  )
}

//子组件
const ChildFC: React.FC = () => {

  useEffect(() => {
    console.log('ChildFC--------------');
  })

  return (
    <p>
      我是ChildFC组件
    </p>
  )
}

export default IndexFC

优化

解决方法: 使用React.memo()将子组件包裹

import React, { memo, useEffect, useState } from 'react'

//首页组件
const IndexFC: React.FC = () => {

  useEffect(() => {
    console.log('Index--------------');
  })

  const [counter, setCounter] = useState<number>(0)

  const addCounter = () => {
    setCounter(counter => counter + 1)
  }

  return (
    <>
      <h1>购买数量为: {counter}</h1>
      <button onClick={addCounter}>数量加1</button>
      <hr />
      <ChildFC />
    </>
  )
}

//子组件
const Child: React.FC = () => {

  useEffect(() => {
    console.log('ChildFC--------------');
  })

  return (
    <p>
      我是ChildFC组件
    </p>
  )
}

// React.memo()
//优化后的子组件
const ChildFC = memo(Child)

export default IndexFC

结论

​ 父组件中引用子组件,且不传递任何数据给子组件,要想父组件发生变化子组件不会被重新渲染. ​ 子组件采用React.memo(子组件)包裹

2、使用useCallback

问题

需求:

​ 父组件中引用子组件,并且向子组件传递数据和方法,且子组件使用React.memo()包裹。

情形分析

情形1: 父组件给子组件传递数据(简单数据),父组件数据发送改变,且传递给子组件数据未发生变化。 结果: 子组件不会重新渲染

情形2: 父组件给子组件传递数据([数组、对象]使用useState初始化后),父组件数据发送改变,且传递给子组件数据未发生变化。 结果: 子组件不会重新渲染

情形3: 父组件给子组件传递数据(函数数据),父组件数据发送改变,且传递给子组件数据未发生变化。 结果: 子组件会重新渲染。可以使用Set测试,Set具有去重功能

发现问题:

​ 在IndexFC组件自身中修改count,每次count发生变化都会导致自身组件重新被渲染。若给子组件传递数据为引用类型(函数,数组,对象),会导致子组件每次都会被重新渲染。

为什么会出现这种情况呢? 因为JavaScript必须在每次的render中为函数定义分配内存

const changeName = (newName: string) => {
    setName(newName)
}
	第一次IndexFC组件渲染,就会为就会为调用changeName函数并且分配内存。
	每次count发生变化都会导致自身组件重新被渲染,重新渲染就会将之前为changeName函数分配内存进行垃圾回收(释放内存空间),然后又会重新调用changeName函数,又为changeName函数分配内存,因为函数为引用类型,两次引用的地址都不相同。
	子组件使用memo(子组件)进行包裹后,会对props中的数据进行对比(基本类型值对比,引用类型地址值对比),发现changeName的地址值与上次changeName的地址值不一样,对比失败,子组件就会被再次渲染

我们想要的:只有当父组件传递给子组件的数据发生变化,才会重新渲染子组件。

结论:

​ 父组件传递数据给子组件,父组件的数据发生变化且传递给子组件数据未发生变化。(子组件均已使用memo包裹) ​ 若传递是非函数数据:子组件不会被重新渲染。 ​ 若传递是函数数据:子组件会重新渲染。

import React, { memo, useEffect, useState } from 'react'

interface IGoodsProps {
  goodId: number;
  goodsName: string
}

/**
 * 发送网络查询商品列表
 * @returns 
 */
const getGoodsList: () => IGoodsProps[] = (): IGoodsProps[] => {
  const producs: IGoodsProps[] = [
    {
      goodId: 10000,
      goodsName: '小米1'
    },
    {
      goodId: 100002,
      goodsName: '小米2'
    },
    {
      goodId: 10003,
      goodsName: '小米3'
    },
    {
      goodId: 100004,
      goodsName: '小米4'
    }, {
      goodId: 100005,
      goodsName: '小米5'
    }
  ]
  return producs
}


const set = new Set()

//首页组件
const IndexFC: React.FC = () => {

  useEffect(() => {
    console.log('Index--------------');
  })

  const [count, setCount] = useState<number>(0);
  const [name, setName] = useState<string>('Child组件');

  const [goodsList, settGoodsList] = useState<IGoodsProps[]>(getGoodsList());


  const changeName = (newName: string) => {
    setName(newName)
  }

  set.add(changeName)

  return (
    <>
      <p>set.size {set.size}</p>
      <button onClick={() => { setCount(count + 1) }}>加1</button>
      <p>count:{count}</p>
      {/* <ChildFC name={name} /> */}
      {/* <ChildFC goodsList={goodsList} /> */}
      <ChildFC changeName={changeName} />
    </>
  )
}

//子组件
//情形1:父组件给子组件传递数据(简单数据),父组件数据发送改变,且传递给子组件数据未发生变化,且子组件使用memo包裹
//结论:子组件不会重新渲染
/* 
const ChildA: React.FC<{ name: string }> = ({ name }) => {

  useEffect(() => {
    console.log('ChildFC------A--------');
  })

  return (
    <>
      <div>我是一个子组件,父级传过来的数据:{name}</div>
    </>
  )
}
 */


//情形2:父组件给子组件传递数据(数组数据),父组件数据发送改变,且传递给子组件数据未发生变化,且子组件使用memo包裹
//结论:子组件不会重新渲染
/* 
const ChildB: React.FC<{ goodsList: IGoodsProps[] }> = ({ goodsList }) => {

  useEffect(() => {
    console.log('ChildFC------B--------');
  })

  return (
    <>
      <h5>我是一个子组件,父级传过来的数据:</h5>
      {
        goodsList.map(good =>
          <div key={good.goodId}>
            商品id:{good.goodId}
            商品名称:{good.goodsName}
          </div>
        )
      }
    </>
  )
}
 */


//情形3:父组件给子组件传递数据(函数数据),父组件数据发送改变,且传递给子组件数据未发生变化,且子组件使用memo包裹
//结论:子组件会重新渲染
const ChildC: React.FC<{ changeName: (params: string) => void }> = ({ changeName }) => {

  useEffect(() => {
    console.count('ChildFC------C--------');
  })

  return (
    <>
      {/* <div>我是一个子组件,父级传过来的数据:{name}</div> */}
      <button onClick={changeName.bind(null, '新的子组件name')}>改变name</button>
    </>
  )
}

//使用memo优化子组件
const ChildFC = memo(ChildC)

export default IndexFC

优化

useCallback

1、参数:
	useCallback有2个参数,第一个是参数是callback函数,第二个是依赖项数组。
		callback(函数),要做的事放在这个函数里面
		desp:要做的事需要引入的外部参数或依赖参数(依赖参数:当某些数据发生改变,就要做这件事)。
			 在依赖不变的情况下,多次定义的时候,返回的值是相同的	
2、返回值:返回一个menoized回调函数
3、使用场景:优化子组件渲染次数
import React, { memo, useCallback, useEffect, useState } from 'react'

interface IGoodsProps {
  goodId: number;
  goodsName: string
}

/**
 * 发送网络查询商品列表
 * @returns 
 */
const getGoodsList: () => IGoodsProps[] = (): IGoodsProps[] => {
  const producs: IGoodsProps[] = [
    {
      goodId: 10000,
      goodsName: '小米1'
    },
    {
      goodId: 100002,
      goodsName: '小米2'
    },
    {
      goodId: 10003,
      goodsName: '小米3'
    },
    {
      goodId: 100004,
      goodsName: '小米4'
    }, {
      goodId: 100005,
      goodsName: '小米5'
    }
  ]
  return producs
}


const set = new Set()

//首页组件
const IndexFC: React.FC = () => {

  useEffect(() => {
    console.log('Index--------------');
  })

  const [count, setCount] = useState<number>(0);
  const [name, setName] = useState<string>('Child组件');

  const [goodsList] = useState<IGoodsProps[]>(getGoodsList());

  //修改名字
  //没有任何数据依赖,只初始化一次这个函数,下次不产生新的函数:即只可以修改一次名字
  /*   const changeName = useCallback(
      (newName: string) => setName(newName),
      []
    ) */

  //可以多次换名
  //初始化时返回一个menoized回调函数,当依赖的name发送变化,又会重新返回一个menoized回调函数
  const changeName = useCallback(
    (newName: string) => setName(newName),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [name]
  )

  set.add(changeName)

  return (
    <>
      <p>set.size {set.size} count:{count}  name={name}</p>
      <button onClick={() => { setCount(count + 1) }}>加1</button>
      <ChildFC changeName={changeName} goodsList={goodsList} />
    </>
  )
}

//子组件

//情形3:父组件给子组件传递数据(数组或函数...),父组件数据发送改变,传递给子组件数据未发生变化(子组件使用memo包裹,传递的函数使用useCallback包裹后)
//结论:子组件不会重新渲染
const ChildC: React.FC<{ changeName: (newName: string) => void, goodsList: IGoodsProps[] }> = ({ changeName, goodsList }) => {

  useEffect(() => {
    console.count('ChildFC------C--------');
  })

  return (
    <>
      <h5>我是一个子组件,父级传过来的数据:</h5>
      {
        goodsList.map(good =>
          <div key={good.goodId}>
            商品id:{good.goodId}
            商品名称:{good.goodsName}
          </div>
        )
      }
      <button onClick={changeName.bind(null, '新的子组件name' + Math.random())}>改变name</button>
    </>
  )
}

//使用memo优化子组件
const ChildFC = memo(ChildC)

export default IndexFC

结论

​ 父组件给子组件传递数据(数组或函数...),父组件数据发送改变,传递给子组件数据未发生变化。要想子组件不会被重新渲染: ​ 子组件使用memo包裹,传递的函数使用useCallback包裹后在传递

3、使用useMemo

问题

需求:

​ 在IndexFC组件中展示购物车的总价格,

发现问题:

​ 每次IndexFC组件中的count发生改变(car中数据没有变化)、都会导致getTotolPrice()函数被调用。而每次执行getTotolPrice()需要花费昂贵性能的开销。

为什么?

	第一次IndexFC组件渲染,就会为就会为调用getTotolPrice函数并且分配内存。
	每次count发生变化都会导致自身组件重新被渲染,重新渲染就会将之前为changeName函数分配内存进行垃圾回收(释放内存空间),然后又会重新调用changeName函数并且为其分配内存。

结论:

​ 组件的数据发生变化都会导致组件重新渲染。重新渲染又会调用组件自身定义的函数执行并且为其分配内存。

import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'

//商品属性
interface IGoodsProps {
  goodId: number;
  goodsName: string,
  price: number
}

//购物车商品属性
interface CartGoodsProps extends IGoodsProps {
  count: number
}

//添加购物车函数类型
type addToCarFuncType = (good: IGoodsProps) => void

/**
 * 发送网络查询商品列表
 * @returns 
 */
const getGoodsList: () => IGoodsProps[] = (): IGoodsProps[] => {
  const producs: IGoodsProps[] = [
    {
      goodId: 10000,
      goodsName: '小米1',
      price: 88
    },
    {
      goodId: 100002,
      goodsName: '小米2',
      price: 444
    },
    {
      goodId: 10003,
      goodsName: '小米3',
      price: 888
    },
    {
      goodId: 100004,
      goodsName: '小米4',
      price: 1234
    },
    {
      goodId: 100005,
      goodsName: '小米5',
      price: 3211
    }
  ]
  return producs
}


//首页组件
const IndexFC: React.FC = () => {

  useEffect(() => {
    console.log('Index--------------');
  })

  const [count, setCount] = useState<number>(0);
  const [goodsList] = useState<IGoodsProps[]>(getGoodsList());
  const [cart, setCart] = useState<CartGoodsProps[]>([]);

  //添加商品到购物车
  const addToCart: addToCarFuncType = useCallback(
    (good: IGoodsProps) => {
      const index = cart.findIndex(goodItem => goodItem.goodId === good.goodId)
      if (index > -1)
        cart[index].count++
      else
        cart.push({ ...good, count: 1 })
      setCart([...cart])
    },
    [cart]
  )

  //获取总价

  //每次count发生变化都会重新渲染组件,而每次新渲染组件都会触发该函数执行,而执行该函数需要花费昂贵的代价,造成无用的性能浪费
  /*   const getTotoPrice = () => {
      console.log('----getTotoPrice--------');
      return cart.reduce((pre, good) => {
        return pre + (good.price * good.count)
      }, 0)
    } */

  //使用useCallback,每次count发生变化都会重新渲染组件,虽然每次返回的函数都是记忆中(menoized)的函数,但每次调用该函数执行仍然会产生销昂贵的开销
  /*   const getTotoPrice = useCallback(
      () => {
        console.log('----getTotoPrice--------');
        return cart.reduce((pre, good) => {
          return pre + (good.price * good.count)
        }, 0)
      },
      [cart]
    ) */

  //第一次渲染时和依赖变化时 才会触发触发该函数的执行,否则都是返回缓存的数据
  const getTotoPrice = useMemo(
    () => {
      console.log('----getTotoPrice--------');
      return cart.reduce((pre, good) => {
        return pre + (good.price * good.count)
      }, 0)
    },
    [cart]
  )

  return (
    <div style={{ background: "#ccc" }}>
      <div style={{ textAlign: "center" }}>
        <p>click me count:{count}     购物车总价:{getTotoPrice} </p>
        <button onClick={() => { setCount(count + 1) }}> click me</button>
        <hr />
        < MyCartFC cart={cart} />
      </div>
      <hr />
      <GoodListFC goodsList={goodsList} addToCart={addToCart} />
    </div>
  )
}

//我的购物车组件
const MyCart: React.FC<{ cart: CartGoodsProps[] }> = ({ cart }) => {
  return (
    <div style={{ background: "pink", minHeight: "50px" }}>
      <p>购物车组件    购物车数量:{cart.length}</p>
      {
        cart.map(good =>
          <span key={good.goodId}>
            商品名称:{good.goodsName}
            数量:{good.count} <span> | </span> </span>
        )
      }
    </div>
  )
}

//产品列表组件
const GoodList: React.FC<{ goodsList: IGoodsProps[], addToCart: addToCarFuncType }> = ({ goodsList, addToCart }) => {

  useEffect(() => {
    console.log('GoodList--------------');
  })

  return (
    <div style={{ background: "orange", padding: "10px" }}>
      <h5>产品列表组件</h5>
      <ul>
        {
          goodsList.map(good =>
            <GoodItemFC key={good.goodId} good={good} addToCart={addToCart} />
          )
        }
      </ul>
    </div>
  )
}


//产品详情组件
const GoodItem: React.FC<{ good: IGoodsProps, addToCart: addToCarFuncType }> = ({ good, addToCart }) => {

  useEffect(() => {
    console.count('GoodItem--------------');
  })

  return (
    <>
      <li key={good.goodId} style={{ background: "yellow", padding: "10px", margin: '10px' }}>
        商品id:{good.goodId}
        商品名称:{good.goodsName}
        商品价格:{good.price}
        <button onClick={addToCart.bind(null, good)}>加入购物车</button>
      </li>
    </>
  )
}

const GoodListFC = memo(GoodList)
const GoodItemFC = memo(GoodItem)
const MyCartFC = memo(MyCart)
export default IndexFC

4、性能优化总结

性能优化的两种思路

1. 减少不必要渲染
2. 减少昂贵的计算带来的消耗

1、React.memo

  1. React.memo包裹后的函数组件,只有props发生变化,才会触发函数re-render。如果props中有引用类型数据,该prop可以传入之前使用React.useCallback包裹。因此React.memo + React.useCallback可以打个好配合,减少unnecessary re-render
  2. React.memo也不要滥用。只有当组件内部本身就比较复杂,重新渲染的代价很高时,再去考虑用React.memo包裹。

2、useCallback

  1. 不能滥用React.useCallbackReact.useCallback包裹后的function不会被垃圾回收机制回收,同时直接创建一个新的引用。同时,React.useCallback所制造的闭包将保持对回调函数和依赖项的引用。因此滥用会导致内存负担重。创建React.useCallback本身就是性能消耗。
  2. React.useCallback的目标是:减少不必要重新渲染,而不用是解决组件内部函数多次创建的问题。React.useCallback可以保存一个function的引用。当dependent list不发生变化,该function的引用不会发生改变。这样的function作为参数传入函数组件,不会因为引用不相等,引起组件的不必要渲染。

3、useMemo

  1. React.useMemo的目标是:解决昂贵的计算(比如成千上万次的计算)带来的性能影响。利用缓存的数据(记忆值)缓解复杂计算带来的性能问题。
  2. 不能滥用React.useMemo。如果计算不够复杂,React.useMemo自身带来的消耗没有必要。基于闭包,占用内存。