React---Hook(useRef,useCallback,useMemo,useContext)

47 阅读7分钟

useRef

useRef是用来在组件不同渲染之间共用一些数据的,它的作用和我们在类组件里面为this赋值是一样的。

语法

import {useRef} from "react"
const refObject = useRef(initialValue)
//useRef接收initialValue作为初始值,它的返回值是一个ref对象,这个对象的.current属性就是该数据的最新值。使用useRef的一个最简单的情况就是在函数组件里面获取DOM对象的引用

案例:

import { useRef, useEffect } from 'react'
import ReactDOM from 'react-dom'const AutoFocusInput = () => {
  const inputRef = useRef(null)
​
  useEffect(() => {
    // 组件挂载后自动聚焦
    inputRef.current.focus()
  }, [])
​
  return (
    <input ref={inputRef} type='text' />
  )
}
​
ReactDOM.render(<AutoFocusInput />, document.getElementById('root'))
//在上面代码中inputRef其实就是一个{current: input节点}对象,只不过它可以保证在组件每次渲染的时候拿到的都是同一个对象。

useCallback

1.我们在定义函数组件的时候时常在函数体内定义一些内嵌函数,这些内嵌函数会在组件每次重新渲染的时候被重新定义,如果它们作为props传递给了子组件的话,即使其它props的值没有发生变化,它都会使子组件重新渲染,而无用的组件重渲染可能会产生一些性能问题。

每次重新生成新的内嵌函数还有另外一个问题就是当我们把内嵌函数作为dependency传进useEffect的dependencies数组的话,因为该函数频繁被重新生成,所以useEffect里面的effect就会频繁被调用。为了解决上述问题,React允许我们使用useCallback来记住当前定义的函数,并在下次组件渲染的时候返回之前定义的函数而不是使用新定义的函数。

简而言之useCallback就是把我们在函数组件内部定义的函数保存起来,当组件重新渲染时还是使用之前的,就不会被重新定义一次

2.语法

import {useCallback} from "react"
const memoizedCallback = useCallback(callback, dependencies)
          
//useCallback接收两个参数,第一个参数是需要被记住的函数,第二个参数是这个函数的dependencies,只有dependencies数组里面的元素的值发生变化时useCallback才会返回新定义的函数,否则useCallback都会返回之前定义的函数。

案例:

import React, { useCallback } from 'react'
import useSearch from 'hooks/useSearch'
import ReactDOM from 'react-dom'//items列表可能包含上千个数据,子组件就会被渲染多次 
const HugeList = ({ items, onClick }) => {
  return (
    <div>
      {
        items.map((item, index) => (
          <div key={index} onClick={()=>onClick(index)}>
            {item}
          </div>
        ))
      }
    </div>
  )
}
​
const MemoizedHugeList = React.memo(HugeList)
​
const SearchApp = ({ searchText }) => {
  const handleClick = useCallback(item => {
    console.log(item)
  }, [])
  const items = useSearch(searchText)
  return (<MemoizedHugeList items={items} onClick={handleClick} />)
}
​
ReactDOM.render(<SearchApp />, document.getElementById('root'))
//定义了一个HugeList组件,由于这个组件需要渲染一个大的列表(items),所以每次重渲染都是十分消耗性能的,因此使用了React.memo函数来让该组件只有在onClick函数和items数组发生变化的时候才被渲染,接着我在SearchApp里面使用MemoizedHugeList,由于要避免该组件的重复渲染,所以我使用了useCallback来记住定义的handleClick函数,这样在组件后面渲染的时候,handleClick变量指向的都是同一个函数,所以MemorizedHugeList只有在items发生变化时才会重新渲染

3.任何优化都会有代价,useCallback也是一样的。当我们在函数组件里面调用useCallback函数的时候,React背后要做一系列计算才能保证当dependencies不发生变化的时候,我们拿到的是同一个函数,因此如果我们滥用useCallback的话,并不会带来想象中的性能优化,反而会影响到我们的性能. 当需要复用组件时,会重复生成组件中函数,这时候就可以用useCallback

错误使用案例:

import React, { useCallback } from 'react'
import ReactDOM from 'react-dom'
const Mybtn = () => {
  const handleClick = useCallback(() => {
    console.log('666')
  }, [])
  return (
    <button onClick={handleClick}>btn</button>
  )
}
ReactDOM.render(<Mybtn />, document.getElementById('root'))
​
​
//上面例子使用的useCallback没有起到任何优化代码性能的作用,反而由于hook内部机制的运行,它消耗的计算资源其实比没有优化之前还多,相当于:
import React, { useCallback } from 'react'
import ReactDOM from 'react-dom'
const Mybtn = () => {
  const inlineClick = () => {
    console.log('666')
  }
  const handleClick = useCallback(inlineClick, [])
​
  return (
    <button onClick={handleClick}>btn</button>
  )
}
​
ReactDOM.render(<Mybtn />, document.getElementById('root'))
​

useMemo

1.useMemo和useCallback的作用十分类似,只不过它允许你记住任何类型的变量(不只是函数)

语法

import {useMemo} from "react"
const memoizedValue = useMemo(() => valueNeededToBeMemoized, dependencies)
//useMemo接收一个函数,该函数的返回值就是需要被记住的变量,当useMemo的第二个参数dependencies数组里面的元素的值没有发生变化的时候,memoizedValue使用的就是上一次的值。

案例:

import React, { useMemo } from 'react'
import ReactDOM from 'react-dom'const RenderPrimes = ({ iterations, multiplier }) => {
  const primes = React.useMemo(() => calculatePrimes(iterations, multiplier), [
    iterations,
    multiplier
  ])
​
  return (
    <div>
      Primes! {primes}
    </div>
  )
}
​
ReactDOM.render(<RenderPrimes />, document.getElementById('root'))
//例子中calculatePrimes是用来计算素数的,因此每次调用它都需要消耗大量的计算资源。为了提高组件渲染的性能,我们可以使用useMemo来记住计算的结果,当iterations和multiplier保持不变的时候,我们就不需要重新执行calculatePrimes函数来重新计算了,直接使用上一次的结果即可。

useContext

1.我们知道React中组件之间传递参数的方式是props,假如我们在父级组件中定义了某些状态,而这些状态需要在该组件深层次嵌套的子组件中被使用的话就需要将这些状态以props的形式层层传递,这就造成了props drilling的问题。为了解决这个问题,React允许我们使用Context来在父级组件和底下任意层次的子组件之间传递状态。在函数组件中我们可以使用useContext Hook来使用Context。

语法:

const value = useContext(MyContext)
//useContext接收一个context对象为参数,该context对象是由React.createContext函数生成的。useContext的返回值是当前context的值,这个值是由最邻近的<MyContext.Provider>来决定的。一旦在某个组件里面使用了useContext这就相当于该组件订阅了这个context的变化,当最近的<MyContext.Provider>的context值发生变化时,使用到该context的子组件就会被触发重渲染,且它们会拿到context的最新值。

案例:

import React, { useContext, useState } from 'react'
import ReactDOM from 'react-dom'//定义context
const NumberContext = React.createContext()
​
const NumberDisplay = () => {
  const [currentNumber, setCurrentNumber] = useContext(NumberContext)
​
  const handleCurrentNumberChange = () => {
    setCurrentNumber(Math.floor(Math.random() * 100))
  }
​
  return (
    <>
      <div>Current number is: {currentNumber}</div>
      <button onClick={handleCurrentNumberChange}>Change current number</button>
    </>
  )
}
​
const ParentComponent = () => {
  const [currentNumber, setCurrentNumber] = useState(100)
​
  return (
    <NumberContext.Provider value={[currentNumber, setCurrentNumber]}>
      <NumberDisplay />
    </NumberContext.Provider>
  )
}
​
ReactDOM.render(<ParentComponent />, document.getElementById('root'))
​

2.使用时避免无用渲染

如果一个函数组件使用了useContext(SomeContext)的话它就订阅了这个SomeContext的变化,这样当SomeContext.Provider的value发生变化的时候,这个组件就会被重新渲染。

这里有一个问题就是,我们可能会把很多不同的数据放在同一个context里面,而不同的子组件可能只关心这个context的某一部分数据,当context里面的任意值发生变化的时候,无论这些组件用不用到这些数据它们都会被重新渲染,这可能会造成一些性能问题.

错误使用案例:

import React, { useContext, useState } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'const AppContext = React.createContext()
​
const ChildrenComponent = () => {
  const [appContext] = useContext(AppContext)
  const theme = appContext.theme
​
  return (
    <div>
      <ExpensiveTree theme={theme} />
    </div>
  )
}
​
const App = () => {
  const [appContext, setAppContext] = useState({ theme: { color: 'red' }, configuration: { showTips: false }})
​
  return (
    <AppContext.Provider value={[appContext, setAppContext]}>
      <ChildrenComponent />
    </AppContext.Provider>
  )
}
​
ReactDOM.render(<App />, document.getElementById('root'))
//ChildrenComponent只使用到了appContext的.theme属性,可是当appContext其它属性例如configuration被更新时,ChildrenComponent也会被重新渲染,而ChildrenComponent调用了一个十分耗费性能的ExpensiveTree组件,所以这些无用的渲染会影响到我们页面的性能

3.解决上面这个问题的方法有下面三种:

3.1拆分Context

这个方法是最被推荐的做法,和useState一样,我们可以将不需要同时改变的context拆分成不同的context,让它们的职责更加分明,这样子组件只会订阅那些它们需要订阅的context从而避免无用的重渲染。

import React, { useContext, useState } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'const ThemeContext = React.createContext()
const ConfigurationContext = React.createContext()
​
const ChildrenComponent = () => {
  const [themeContext] = useContext(ThemeContext)
​
  return (
    <div>
      <ExpensiveTree theme={themeContext} />
    </div>
  )
}
​
const App = () => {
  const [themeContext, setThemeContext] = useState({ color: 'red' })
  const [configurationContext, setConfigurationContext] = useState({ showTips: false })
​
  return (
    <ThemeContext.Provider value={[themeContext, setThemeContext]}>
      <ConfigurationContext.Provider value={[configurationContext, setConfigurationContext]}>
        <ChildrenComponent />
      </ConfigurationContext.Provider>
    </ThemeContext.Provider>
  )
}
​
ReactDOM.render(<App />, document.getElementById('root'))

3.2拆分组件,使用memo来优化消耗性能的组件

如果出于某些原因你不能拆分context,仍然可以通过将消耗性能的组件和父组件的其他部分分离开来,并且使用memo函数来优化消耗性能的组件

import React, { useContext, useState } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'const AppContext = React.createContext()
​
const ExpensiveComponentWrapper = React.memo(({ theme }) => {
  return (
    <ExpensiveTree theme={theme} />
  )
})
​
const ChildrenComponent = () => {
  const [appContext] = useContext(AppContext)
  const theme = appContext.theme
​
  return (
    <div>
      <ExpensiveComponentWrapper theme={theme} />
    </div>
  )
}
​
const App = () => {
  const [appContext, setAppContext] = useState({ theme: { color: 'red' }, configuration: { showTips: false }})
​
  return (
    <AppContext.Provider value={[appContext, setAppContext]}>
      <ChildrenComponent />
    </AppContext.Provider>
  )
}
​
ReactDOM.render(<App />, document.getElementById('root'))
​

3.3 不拆分组件,也可以使用useMemo来优化

import React, { useContext, useState, useMemo } from 'react'
import ExpensiveTree from 'somewhere/ExpensiveTree'
import ReactDOM from 'react-dom'const AppContext = React.createContext()
​
const ChildrenComponent = () => {
  const [appContext] = useContext(AppContext)
  const theme = appContext.theme
​
  return useMemo(() => (
      <div>
        <ExpensiveTree theme={theme} />
      </div>
    ),
    [theme]
  )
}
​
const App = () => {
  const [appContext, setAppContext] = useState({ theme: { color: 'red' }, configuration: { showTips: false }})
​
  return (
    <AppContext.Provider value={[appContext, setAppContext]}>
      <ChildrenComponent />
    </AppContext.Provider>
  )
}
​
ReactDOM.render(<App />, document.getElementById('root'))
​