React Hooks

238 阅读9分钟

功能介绍

React Hooks对函数型组件进行增强, 比如让函数型组件可以存储状态,又比如 可以拥有处理副作用的能力. 让开发者在不使用类组件的情况下, 实现相同的功能.
在一个组件当中,只要不是把数据转化视图的代码就都是副作用。 比如: 获取dom元素, 为dom元素添加事件,设置定时器, 以及发送ajax请求。
在类组件中我们通常使用生命周期函数中处理副作用, 在函数是组件中,我们就要使用hooks来处理。

类组件的不足 (hooks要解决的问题)
  1. 缺少逻辑复用机制 //TODO 待补充
  2. 类组件通常会变的复杂难以维护

将一组相干的业务逻辑拆分到了多个生命周期函数中 在一个生命周期函数内存在多个不相干的业务逻辑 3. 类成员方法不能保证this指向的正确性

常用的一些钩子函数
1. useState 用于为函数组件引入状态

在我们的认知中,在一个函数中定义的变量,在函数调用完成后就会被释放,所以函数型组件原本是不可保存状态数据的。
有了 useState之后,函数型组件,就可以保存状态了。 useState 内部是使用了闭包来保存数据的。 当状态发生改变时,组件会被重新渲染。

基本使用不再赘述,我们一起来看下使用细节。

  • 接收唯一的参数即状态初始值. 初始值可以是任意数据类型.
  • 返回值为数组. 数组中存储状态值和更改状态值的方法. 方法名称约定以set开头, 后面加上状态名称.
  • 方法可以被调用多次. 用以保存不同状态值.
  • 参数可以是一个函数, 函数返回什么, 初始状态就是什么, 函数只会被调用一次, 用在初始值是动态值的情况.
 function App() {
  const [state, setState] =  useState(() => 100) // 参数可以是一个函数
  return (
    <div className="App">
        <span>{state}</span>
    </div>
  );
}

参数是函数主要是用在初始值是动态值的情况.

function App(props) {
  // 假如调用App组件时传入了props,
  // 如果传了count 我们就用props的值,如果没传,我们就用默认值
  // 那么我们可以这样写
  const propsCount = props.count || 0
  const [count, setCount]  = useState(propsCount) 
  return (
    <div className="App">
        <span>{count}</span>
        <button onClick={()=>setCount(count + 1)}>button</button>
    </div>
  );
}
1.1 useState 的使用细节

虽然按照如上写法功能会实现,但是代码其实是有问题的。
当我们点击按钮去改变状态时,App组件是会被重新渲染的。那么“const propsCount = props.count || 0” 这句代码在每次组件重新渲染时都会执行。这是没有意义的,因为这句代码是在获取状态的初始值,只有在组件初次渲染时才有意义。也就是说这句代码只需要被执行一次。这个时候,我们就需要用到useState传递函数的方式。

function App(props) {
 const [count, setCount]  = useState(() => {
   return props.count || 0 
      // 这样这句代码就只会在初始渲染时执行
     //后续组件重新渲染时就不会执行了
 })  
  return (
    <div className="App">
        <span>{count}</span>
        <button onClick={()=>setCount(count + 1)}>button</button>
    </div>
  );
}
1.2 设置状态值的使用细节
  • 设置状态值方法的参数可以是一个值,也可以是一个函数
  • 设置状态值方法的方法本身是异步的 🌰
// 设置状态值方法的参数是一个函数
function App(props) {
  const [count, setCount]  = useState(() =>  props.count || 0) 
  function handleCount(){
    setCount((count) => {
      return count + 1 // 函数的返回值是什么,count就是什么
    })
  }
  return (
    <div className="App">
      <span>{count}</span>
      <button onClick={handleCount}> button </button>
    </div>
  );
}

🌰 设置状态值方法是异步的,还是上面的handleCount方法

 function handleCount(){
    setCount((count) => {
      return count + 1 // 函数的返回值是什么,count就是什么
    })
    document.title = count 
    //假如setCount是同步的,那么此刻我们拿到的count应该是改过的count,
    //如果是异步的那么这句代码是不会等到setCount执行完才执行的,
    //那么这个时候得到的count就是设置之前的count
  }

切回浏览器看代码执行的结果很明显,证明setCount是异步的。 那么怎么解决呢,很简单我们把代码放在回调函数里即可

 function handleCount(){
    setCount((count) => {
      const newCount = count + 1;
      document.title = newCount 
      return newCount
    })
  }
2. useReducer 是另一种让函数组件保存状态的方式.

使用方法类似redux,这里不再赘述。 直接看代码

import { useReducer } from "react";
export default function App () {
  function reducer(state, action){
    switch(action.type){
      case 'increment':
        return state + 1;
      case 'decrement':
        return state - 1
      default: 
      return state
    }
  }
  const [count, dispatch] = useReducer(reducer, 0);
  return (
    <div>
      <button onClick={()=> dispatch({type:'increment'})}>➕</button>
      <span>{count}</span>
      <button onClick={()=> dispatch({type:'decrement'})}>➖ </button>
    </div>
  )
}

相对于useState的优点:
假如App的某个子组件想要改变count值,这个时候我们就不需要去传递多个修改数据的方法, 比如让数据➕1, ➖1等。我们可以直接把dispatch传递给子组件。 子组件通过调用dispatch方法,就可以触发任意一个action对state进行修改。

3. useContext

在跨组件层级获取数据时简化获取数据的代码.

先来看下createContext的写法

const appContext = createContext()

function Foo(){
  return (
    <appContext.Consumer> 
    {/* 传递一个回调函数,接收一个形参即为context中存储的值 */}
      {
        value =>  {
          return <div> {value}</div>
        }
      }
    </appContext.Consumer>
  )
}

 function App () {
  return (
      <appContext.Provider value={100}>
        <Foo/>
      </appContext.Provider>
  )
}


export default App

对比useContext的写法

import { useContext } from "react";
const appContext = createContext()

function Foo(){
  const value = useContext(appContext)
  return (
    <div>{value}</div>
  )
}

 function App () {
  return (
      <appContext.Provider value={100}>
        <Foo/>
      </appContext.Provider>
  )
}
4. useEffect

让函数型组件拥有处理副作用的能力,类似生命周期函数。

4.1 执行时机

可以把 useEffect 看做 componentDidMount、componentDidUpdate 、componentWillUnmount 这三个函数的组合.

写法执行时机
useEffect(() => {})componentDidMount,componentDidUpdate
useEffect(() => {}, [])componentDidMount
useEffect(() => () => {})componentDidUpdate,componentWillUnMount

接下来一起来验证下吧:

function App () {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log(123) 
    //可以看到组件挂载完成后控制台输出了123,
    // 点击按钮更新组件状态,控制台也会输出123
  })
  return (
      <div>
        <span>{count}</span>
        <button onClick={() => setCount(count + 1)}>点击</button>
      </div>
  )
}

export default App
function App () {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log(123)
     // 组件挂载完成后控制台输出了123,
    // 点击按钮更新组件状态,控制台没有输出123
  },[])
  return (
      <div>
        <span>{count}</span>
        <button onClick={() => setCount(count + 1)}>点击</button>
      </div>
  )
}
import ReactDOM from 'react-dom'
function App () {
  const [count, setCount] = useState(0);
  useEffect(() => {
    return () => {
      console.log('123')
      //点击第一个button更新状态控制台输出
      // 点击第二个button卸载组件控制台输出
    }
  })
  return (
      <div>
        <span>{count}</span>
        <button onClick={() => setCount(count + 1)}>点击</button>
        <button onClick={() => ReactDOM.unmountComponentAtNode(document.getElementById("root"))}> 点击卸载组件 </button>
      </div>
  )
}
4.2 使用方式

只简单介绍useEffect结合异步函数的使用

   ...
  function getData(){
    return new Promise((reslove) => {
      reslove({count: 0})
    })
  }
  useEffect(async () => {
    let result = await getData();
    console.log(result);
  })
  ...

可以看到控制台会有报错,我们来分析下出现报错的原因
原本在useEffect函数中是可以返回一个函数的,返回的这个函数是用来做清理操作的,会在组件卸载之前执行,但是按照上面的代码来写的话,我们就把它变成一个异步函数,异步函数的返回值时一个promise对象,这样就改变了useEffect函数原有返回值的类型,所以会有报错。

正确的写法: 我们要在useEffect里写上一个函数自执行,把这个自执行函数变成一个异步函数,然后在内部才能使用await关键字

    useEffect(() => {
        (async () => {
            await axios.get()
        })()
    })

所以,修改代码如下:

    function getData(){
        return new Promise((reslove) => {
          reslove({count: 0})
        })
    }
    useEffect(() => {
        (async function(){
          const result = await getData();
          console.log(result)
        })()
    })
5. useMemo

useMemo 的行为类似Vue中的计算属性, 可以监测某个值的变化, 根据变化值计算新值. useMemo 会缓存计算结果. 如果监测值没有发生变化, 即使组件重新渲染, 也不会重新计算. 此行为可以有助于避免在每个渲染上进行昂贵的计算.
🌰:

function App () {
  const [count, setCount] = useState(0);
  const result = useMemo(() => {
  //点击第一个button修改bool时,虽然视图发生改变,但监测值count没有发生变化,函数并未执行。
    console.log(1111);
    return count * 2
  },[count])

  const [bool, setBool] = useState(true)
  return (
      <div>  
        <div>
          <span>{bool? '真':'假'}</span>
          <button onClick={() => setBool(!bool)}>点击修改bool  </button>
        </div>
        
        <div>
          <span>{result}</span>
          <button onClick={() => setCount(count + 1)}>点击</button>
        </div>
      </div>
  )
}
6. memo 方法

性能优化, 如果本组件中的数据没有发生变化, 阻止组件更新.
类似类组件中的 PureComponent 和 shouldComponentUpdate 当组件发生重新渲染时,组件的数据有没有发生变化,如果没有就不让组件重新渲染难。
🌰 一般来说,点击button改变App组件的count值,Foo组件也会重新渲染。 代码如下

function Foo(){
  console.log('Foo组件重现渲染了') //控制台会有输出
  return (
    <div>我是Foo组件</div>
  )
}

function App () {
  const [count, setCount] = useState(0);
  return (
      <div>
          <span>{count}</span>
          <button onClick={() => setCount(count + 1)}>点击</button>
          <Foo/>
      </div>
  )
}

引入memo方法后, 点击修改count值可以看到控制台不再有信息打印。也就是说修改了count值,Foo组件未被重新渲染。这样就提高了应用的性能。

const Foo = memo(function Foo(){
  console.log('Foo组件重现渲染了')
  return (
    <div>我是Foo组件</div>
  )
})

function App () {
  const [count, setCount] = useState(0);
  return (
      <div>
          <span>{count}</span>
          <button onClick={() => setCount(count + 1)}>点击</button>
          <Foo/>
      </div>
  )
}

7. useCallback

对函数进行缓存。使组件重新渲染时得到相同的应用实例。 🌰:

const Foo = memo(function Foo(props){
  console.log('Foo组件重现渲染了')
  return (
    <div>
      <div>我是Foo组件</div>
      <button onClick={props.resetCount}>reset count</button>
    </div>
  )
})

function App () {
  const [count, setCount] = useState(0);
  const resetCount = () => {
    setCount(0)
  }
  return (
      <div>
          <span>{count}</span>
          <button onClick={() => setCount(count + 1)}>点击</button>
          <Foo resetCount={resetCount}/>
      </div>
  )
}

问题:
点击按钮改变count,可以看到控制台也在打印信息,也就是Foo组件重现渲染了。那么究竟是为什么呢?

因为点击按钮的时候改变了count值,当改变count值时App组件会被重新渲染,组件被重新渲染时每次都会生成不同的resetCount函数,也就是每次给Foo组件传递的都是不同的resetCount函数,所以Foo组件就被重新渲染了。那么我们要怎么优化下呢?
很简单,如果App组件重新渲染时都能得到一个相同的resetCount函数,这样Foo组件就不会被重新渲染了。

const Foo = memo(function Foo(props){
  console.log('Foo组件重新渲染了')
  return (
    <div>
      <div>我是Foo组件</div>
      <button onClick={props.resetCount}>reset count</button>
    </div>
  )
})

function App () {
  const [count, setCount] = useState(0);
  const resetCount = useCallback(() => {
    setCount(0)
  },[setCount])
  return (
      <div>
          <span>{count}</span>
          <button onClick={() => setCount(count + 1)}>点击</button>
          <Foo resetCount={resetCount}/>
      </div>
  )
}
8. userRef 获取dom元素 & 保存数据(跨组件周期)
8.1 获取dom元素

使用方法不再赘述

8.2 保存数据
  • 使用useState保存数据是组件的状态数据,修改useState里保存的状态数据,组件会重新渲染
  • 使用useRef保存数据,即使组件重新渲染,保存的数据还在。修改useRef保存的数据不会触发组件的重新渲染

那么要怎么使用呢? 🌰

function App () {
  const [count, setCount] = useState(0);
  let timerId = null;
  useEffect(() => {
    timerId = setInterval(() => {
      setCount(count => count + 1)
    },1000)
  },[])
  const stopCount = () => {
    console.log(timerId)
    clearInterval(timerId)
  }
  return (
      <div>
        <span>{count}</span>
        <button onClick={stopCount}>点击</button>
      </div>
  )
}

我们点击按钮想清除timerId,但是点击按钮后发现计时器并未清除,count值还在累加。
因为计时器改变了count值,状态发生变化组件就会重新渲染,那么let timerId = null,这句代码就会重新执行,所以我们拿到的timerId一直都是null。 修改代码如下:

function App () {
  const [count, setCount] = useState(0);
  let timerId = useRef(null);
  useEffect(() => {
    timerId.current = setInterval(() => {
      setCount(count => count + 1)
    },1000)
  },[])
  const stopCount = () => {
    console.log(timerId)
    clearInterval(timerId.current)
  }
  return (
      <div>
        <span>{count}</span>
        <button onClick={stopCount}>点击</button>
      </div>
  )
}

由此可以证明 使用useRef保存数据,即使组件重新渲染,保存的数据还在。