首页 > 编程语言 > 30分钟带你全面了解React Hooks
2021
07-01

30分钟带你全面了解React Hooks

概述

1. Hooks 只能在函数组件内使用;

2. Hooks 用于扩充函数组件的功能,使函数组件可以完全代替类组件

React Hooks 都挂在 React 对象上,因此使用时为 React.useState() 的形式,若嫌麻烦,可以提前导入,如下:

import React, { useState } from "react"

React 内置的 Hooks 有很多,这里介绍一些常用到的。全部的请看 Hooks API

用到了 Hook 的函数组件名必须首字母大写,否则会被 ESLint 报错

1. useState

const [state, setState] = useState(initialState)

1.1 概念三连问

调用 useState 有什么作用?

useState 是用于声明一个状态变量的,用于为函数组件引入状态。

我们传递给 useState 的参数是什么?

useState 只接收一个参数,这个参数可以是数字、字符串、对象等任意值,用于初始化声明的状态变量。也可以是一个返回初始值的函数,最好是函数,可在渲染时减少不必要的计算。

useState返回的是什么?

它返回一个长度为2的读写数组,数组的第一项是定义的状态变量本身,第二项是一个用来更新该状态变量的函数,约定是 set 前缀加上状态的变量名。如 setState,setState() 函数接收一个参数,该参数可以是更新后的具体值,也可以是一个返回更新后具体值的函数。若 setState 接收的是一个函数,则会将旧的状态值作为参数传递给接收的函数然后得到一个更新后的具体状态值。

1.2 举个例子

function App(){
  const [n, setN] = useState(0)
  const [m, setM] = useState(() => 0)
  return (
    <div>
      n: {n}
      <button onClick={() => setN(n+1)}>+1</button>
      <br/>
      m: {m}
      <button onClick={() => setM(oldM => oldM+1)}>+1</button>
    </div>
  )
}

1.3 注意事项

  • useState Hook 中返回的 setState 并不会帮我们自动合并对象状态的属性
  • setState 中接收的对象参数如果地址没变的话会被 React 认为没有改变,因此不会引起视图的更新

2. useReducer

useReducer 是 useState 的升级版。在 useState 中返回的写接口中,我们只能传递最终的结果,在 setN 的内部也只是简单的赋值操作。
也就是说,得到结果的计算过程需要我们在函数组件内的回调函数中书写,这无疑增加了函数组件的体积,而且也不符合 Flux 的思想(状态由谁产生的,谁负责进行各种处理,并暴露处理接口出去给别人用)

因此,React 就提供了比 useState 更高级的状态管理 Hook:useReducer,介绍如下:

2.1 使用方法

  • 创建初始状态值 initialState
  • 创建包含所有操作的 reducer(state, action) 函数,每种操作类型均返回新的 state 值
  • 根据 initialState 和 reducer 使用 const [state, dispatch] = useReducer(reducer, initialState) 得到读写 API
  • 调用写接口,传递的参数均挂在 action 对象上

2.2 举个例子

import React, { useReducer } from 'react';
import ReactDOM from 'react-dom';

const initialState = {
  n: 0
}

const reducer = (state, action) => {
  switch(action.type){
    case 'addOne':
      return { n: state.n + 1 }
    case 'addTwo':
      return { n: state.n + 2 }
    case 'addX':
      return { n: state.n + action.x }
    default: {
      throw new Error('unknown type')
    }
  }
}

function App(){
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <div>
      我是 App
      {state.n}
      <button onClick={()=>dispatch({type: 'addOne'})}>+1</button>
      <button onClick={()=>dispatch({type: 'addTwo'})}>+2</button>
      <button onClick={()=>dispatch({type: 'addX', x: 5})}>+5</button>
    </div>
  )
}


ReactDOM.render(<App/>,document.getElementById('root'));

3. useContext

context 是上下文的意思,上下文是局部的全局变量这个局部的范围由开发者自己指定。

3.1 使用方法

useContext 的使用方法分三步走:

  • 使用 const x = createContext(null) 创建上下文,在创建时一般不设置初始值,因此为 null,一般是在指定上下文作用域时初始化。
  • 使用 <x.Provider value={}></x.Provider> 圈定上下文的作用域
  • 在作用域中使用 const value = useContext(x) 使用上下文的数据

3.2 举个例子

import React, { useState, createContext, useContext } from 'react';
import ReactDOM from 'react-dom';

const Context = createContext(null)

function App(){
  const [n, setN] = useState(0)
  return (
    <Context.Provider value={{n, setN}}>
      <div>
        <Baba />
        <Uncle />
      </div>
    </Context.Provider>
  )
}

function Baba(){
  return (
    <div>
      我是爸爸
      <Child />
    </div>
  )
}

function Uncle(){
  const {n, setN} = useContext(Context)
  return (
    <div>
      我是叔叔
      我拿到的 context 数据为 {n}
    </div>
  )
}

function Child(){
  const {n, setN} = useContext(Context)
  return (
    <div>
      我是儿子
      我拿到的 context 数据为 {n}
      <button onClick={() => setN(n+5)}>
        点击改变 context 数据
      </button>
    </div>
  )
}


ReactDOM.render(<App/>,document.getElementById('root'));

4. useEffect

effect 是副作用的意思,对环境的改变就是副作用。副作用好像是函数式编程里的一个概念,这里不做过多解读,也不太懂。
在 React 中,useEffect 就是在每次 render 后执行的操作,相当于 afterRender, 接收的第一个参数是回调函数,第二个参数是回调时机。可用在函数组件中模拟生命周期。

如果同时出现多个 useEffect ,会按出现顺序依次执行

4.1 模拟 componentDidMount

useEffect(()=>{
  console.log('只在第一次 render 后执行')
},[])

4.2 模拟 componentDidMount + componentDidUpdate

useEffect(()=>{
   console.log('每次 render 后都执行,包括第一次 render')
})

4.3 可添加依赖

useEffect(()=>{
    console.log('只在 x 改变后执行,包括第一次 x 从 undefined 变成 initialValue')
},[x])
//如果有两个依赖,则是当两个依赖中的任何一个变化了都会执行

4.4 模拟 componentWillUnmount

useEffect(()=>{
  console.log('每次 render 后都执行,包括第一次 render')
  return ()=>{
    console.log('该组件要被销毁了')
  }
})
//直接 return 一个函数即可,该函数会在组件销毁前执行

5. useLayoutEffect

useEffect 总是在浏览器渲染完视图过后才执行,如果 useEffect 里面的回调函数有对 DOM 视图的操作,则会出现一开始是初始化的视图,后来执行了 useEffect 里的回调后立马改变了视图的某一部分,会出现一个闪烁的状态。
为了避免这种闪烁,可以将副作用的回调函数提前到浏览器渲染视图的前面执行,当还没有将 DOM 挂载到页面显示前执行 Effect 中对 DOM 进行操作的回调函数,则在浏览器渲染到页面后不会出现闪烁的状态。

layout 是视图的意思,useLayoutEffect 就是在视图显示出来前执行的副作用。

useEffect 和 useLayoutEffect 就是执行的时间点不同,useLayoutEffect 是在浏览器渲染前执行,useEffect 是在浏览器渲染后执行。但二者都是在 render 函数执行过程中运行,useEffect 是在 render 完毕后执行,useLayoutEffect 是在 render 完毕前(视图还没渲染到浏览器页面上)执行。

因此 useLayoutEffect 总是在 useEffect 前执行。

一般情况下,如果 Effect 中的回调函数中涉及到 DOM 视图的改变,就应该用 useLayoutEffect,如果没有,则用 useEffect。

6. useRef

useRef Hook 是用来定义一个在组件不断 render 时保持不变的变量。
组件每次 render 后都会返回一个虚拟 DOM,组件内对应的变量都只属于那个时刻的虚拟 DOM。
useRef Hook 就提供了创建贯穿整个虚拟 DOM 更新历史的属于这个组件的局部的全局变量。
为了确保每次 render 后使用 useRef 获得的变量都能是之前的同一个变量,只能使用引用做到,因此,useRef 就将这个局部的全局变量的值存储到了一个对象中,属性名为:current

useRef 的 current 变化时不会自动 render

useRef 可以将创建的 Refs 对象通过 ref 属性的方式引用到 DOM 节点或者 React 实例。这个作用在 React—ref 属性 中有介绍。

同样也可以作为组件的局部的全局变量使用,如下例的记录当前是第几次渲染页面。

function App(){
  const [state, dispatch] = useReducer(reducer, initialState)
  const count = useRef(0)
  useEffect(()=>{
    count.current++;
    console.log(`这是第 ${count.current} 次渲染页面`)
  })
  return (
    <div>
      我是 App
      {state.n}
      <button onClick={()=>dispatch({type: 'addOne'})}>+1</button>
      <button onClick={()=>dispatch({type: 'addTwo'})}>+2</button>
      <button onClick={()=>dispatch({type: 'addX', x: 5})}>+5</button>
    </div>
  )
}

7. forwardRef(不是 Hook)

forwardRef 主要是用来对原生的不支持 ref属性 函数组件进行包装使之可以接收 ref属性 的,具体使用方法可参考 React—ref 属性

forwardRef 接收一个函数组件,返回一个可以接收 ref 属性的函数组件

8. useMemo && useCallback

React 框架是通过不断地 render 来得到不同的虚拟 DOM ,然后进行 DOM Diff 来进行页面 DOM 的选择性更新的,因此,在每次的 render 之后都会短时间内存在新旧两个虚拟 DOM 。

对于组件内包含子组件的情况,当父组件内触发 render 时,就算子组件依赖的 props 没有变化,子组件也会因为父组件的重新 render 再次 render 一遍。这样就产生了不必要的 render 。

为了解决不必要的 render ,React 提供了 React.memo() 接口来对子组件进行封装。如下:

function App(){
  const [n, setN] = useState(0)
  const [m, setM] = useState(0)
  return (
    <div>
      我是父组件
      n: {n}
      <button onClick={()=>setN(n+1)}>n+1</button>
      <button onClick={()=>setM(m+1)}>m+1</button>
      <Child value={m}/>  //这样当子组件依赖的 m 值没有变化时,子组件就不会重新 render
    </div>
  )
}

const Child = React.memo((props)=>{
  useEffect(()=>{
    console.log('子组件 render 了')
  })
  return (
  <div>我是子组件,我收到来自父组件的值为:m {props.value}</div>
  )
})

但是上述方式存在 bug,因为 React.memo 在判断子组件依赖的属性有没有发生改变时仅仅是做的前后值是否相等的比较,如果子组件从父组件处接收的依赖是一个对象的话,比较的就会是对象的地址,而不是对象里面的内容,因此在每次父组件重新 render 后得到的会是不同地址的对象,尽管对象里面的值没有更新,但是子组件发现地址变了也会重新 render。

为了解决这个问题,就又出来了 useMemo() Hook,useMemo 是用于在新旧组件交替时缓存复用一个函数或者一个对象,当某个依赖重新变化时才重新生成。

useMemo Hook 接收一个无参数的返回函数(或对象)的函数。并且 useMemo 必须有个依赖,告诉其在什么时候重新计算。有点类似于 Vue 的计算属性的原理。如下:

function App(){
  const [n, setN] = useState(0)
  const [m, setM] = useState(0)
  const onClickChild = useMemo(()=>{
    return () => {
      console.log(m)
    }
  },[m])  
  return (
    <div>
      我是父组件
      n: {n}
      <button onClick={()=>setN(n+1)}>n+1</button>
      <button onClick={()=>setM(m+1)}>m+1</button>
      <Child value={m} onClick = {onClickChild}/>
    </div>
  )
}

const Child = React.memo((props)=>{
  useEffect(()=>{
    console.log('子组件 render 了')
  })
  return (
    <div>
      我是子组件,我收到来自父组件的值为:m {props.value}
      <br/>
      <button onClick={props.onClick}>click</button>
    </div>
  )
})

useCallback() 是 useMemo 的语法糖,因为 useMemo 是接收一个没有参数的返回函数(或对象)的函数,会有些奇怪,因此提供了 useCallback 来直接接收函数或对象。

const onClickChild = useMemo(() => {
      console.log(m)
  },[m])

9. useInperativeHandle

useInperativeHandel 是和 ref 相关的一个 Hook。

我们知道,ref 属性是会将当前的组件实例或 原生DOM 直接赋值给传入的 Ref 对象的 current 属性上,而且函数组件不能接收 ref 属性,因为函数组件没有实例。但是如果函数组件经过 React.forwardRef() 封装过后 可以接收 ref,一般情况下,这个 ref 是访问的经过函数组件转发过后的 原生DOM,但是,如果在函数组件内不仅仅是想让外来的 ref 指向一个 原生DOM 呢?可不可以让函数组件的 ref 像类组件中的 ref 指向实例一样拥有更多的可控性操作呢?React 就为函数组件提供了一种封装返回的 ref 指向的对象的方法,就是 useInperativeHandle Hook。

9.1 举个例子

function App(){
  const myRef = useRef(null)
  useEffect(()=>{
    console.log(myRef.current.real)
    console.log(myRef.current.getParent())
  }, [])
  return (
    <div>
      我是父组件
      <Child ref={myRef}/>
    </div>
  )
}

const Child = forwardRef((props, ref)=>{
  const childRef = useRef(null)
  useImperativeHandle(ref, ()=>{
    return {
      real: childRef.current,
      getParent(){
        return childRef.current.parentNode
      }
    }
  })
  return (
    <div>
      我是子组件,我有一个子DOM
      <button ref={childRef}>按钮</button>
    </div>
  )
})

10. 自定义 Hook

自定义 Hook 就是自定义一个函数,这个函数必须以 use 开头,并且,该函数里必须用到原生的 Ract Hooks,返回值一般是一个数组或一个对象,用于暴露该 Hooks 的读写接口。

自定义 Hook 通常是将函数组件中多次用到的 hook 整合到一起,尽量在函数组件中不要出现多次 hook 操作。

以上就是30分钟带你全面了解React Hooks的详细内容,更多关于全面了解React Hooks的资料请关注自学编程网其它相关文章!

编程技巧