2021-04-01 React Hook进一步理解

758 阅读7分钟

React Hooks是什么,为什么要用它?

Hook是React 16.8新增的特性,其主要目的是写函数组件时用到state、处理副作用等React的特性,可以不再编写class组件

之前使用函数组件大多是是无状态的,只能使用props参数,只能是个UI展示组件,没有逻辑状态注入state等,而使用state只能用class组件,但是使用类组件时,如果有大量的业务逻辑需要放在生命周期函数中,会使得项目越来越复杂且难以维护,class中的this问题需要绑定也是个令人棘手的问题,为了解决这些麻烦,Hook就出现了

Hook主要解决了三类问题:

  • 组件之间复用状态逻辑
  • 复杂组件变得难以理解
  • 难以理解的class

Hook使用规则

Hook本质是一类特殊的函数,但是使用它们会有两个额外的规则:

  • 只能在函数最外层调用Hook。不要再循环、条件判断或者子函数中调用
  • 只能在React的函数组件中调用Hook或者在自定义的Hook中调用。不要再其他Javascript函数中调用

React内置的Hook

基础的Hook

useState
const [state, setState] = useState(initialState)

返回一个state,以及更新state的函数

在初次渲染期间,返回的状态(state)与传入的第一个参数(initialState)相同,当然这个initialState也可以是一个函数,在初次渲染期间,会调用函数计算初始的state,后续不在调用

setState函数用于更新state。接收一个新的state值并将组件的一次重新渲染加入队列;当然也可以接收一个参数为preState=>{...}的函数并返回函数执行的结果

使用useState时,需要注意以下三个点:

1)useState返回的setState方法同类组件的setState一样,也是一个异步方法,需要组件更新之后,state的值才会变成新值

2)useState返回的setState并不具有类组件setState合并多个state的作用,如果state中有多个state,在更新时,其他值一同更新,可以使用ES6中展开运算符来达到合并的效果

setState(prevState => {
    // 也可以使用Object.assign方法
    return {...prevState, ...updatedValues}
})

3)同一个组件中可以使用useState创建多个state

useEffect
useEffect(didUpdate, [依赖项])

该Hook接收一个包含命令式、且可能含有副作用代码的函数

Effect专业术语为副作用。什么是副作用呢?网络请求、DOM操作都是副作用的一种,useEffect就是专门来处理副作用的

在类组件中副作用通常在componentDidMount和componentDidUpdate中进行处理,而useEffect就相当于componentDidMount、componentDidUpdate和componentWillUnmount的集合体

useEffect包含两个参数一个是执行时的回调函数didUpdate和依赖参数数组,并且回调函数还有一个返回函数,在里面会执行诸如清除定时器,取消订阅等的操作。通常,为防止内存泄漏,清除函数会在组件卸载之前执行。另外,如果组件多次渲染(通常如此),则在执行下一个effect之前,上一个effect就已被清除

依赖参数,其本身是一个数组,在数组中放入要依赖的数据,当这些数据有更新时,就只执行回调函数。整个生命周期过程如下:

组件挂载->执行副作用(回调函数)->组件更新->执行清理函数(回调函数里的返回函数)->执行副作用(回调函数)->组件准备卸载->执行清理函数(回调函数里的返回函数)->组件卸载

上述讲的useEffect是componentDidMount、componentDidUpdate和componentWillUnmount的集合体,如果只是单纯的只想要在挂载后、更新后、卸载前其中之一的阶段执行,可以参考一下操作:

componentDidMount:如果只是想在组件挂载后执行,可以把依赖数组置为空数组[],这样在更新时就不会执行该副作用了

componentWillUnmount:如果只想在组件卸载之前执行,同样可以把依赖参数置为空数组[],该副作用的返回函数会在卸载之前执行

componentWillUpdate:只检测更新相对比较麻烦,需要区分更新时还是挂载需要检测依赖数据和初始值是否一致,如果当前的数据和初始数据保持一致就说明是挂载阶段,当然安全起见应和上一次的值进行对比,若当前的依赖数据和上一次的依赖数据完全一样,则说明组件没有更新

useContext
const value = useContext(MyContext)

接收一个context对象(React.createContext的返回值)并返回context的当前值。当前的context值由上层组件中距离当前组件最近的<MyContext.Provider>valueprop决定

当组件上层最近的<MyContext.Provider>更新时,该Hook会触发重新渲染,并使用最新传递给MyContextprovider的contextvalue

调用了useContext的组件总会在context值变化时重新渲染。如果重新渲染组件的开销较大,可以通过使用useMemo来优化

额外的Hook

useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init)

这是useState的替代方案。它接收一个形如(state, action) => newState的reducer,并返回当前的state以及与其配套的dispatch方法,和redux很类似

useCallback
const memoizedCallback = useCallback(
    ()=>{
        doSomething(a,b);
    },
    [a,b]
)

返回一个memoized函数,大白话就是缓存函数

把内联回调函数以及依赖项数组作为参数传入useCallback,它将返回改回调函数的memoized版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如shouldComponentUpdate)的子组件时,它将非常有用

useCallback(fn, deps)相当于useMemo(()=>fn, deps)

useMemo
const memoizedValue = useMemo(() => computedExpensiveValue(a, b), [a, b])

返回一个memoized值,缓存值

把创建函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才会重新计算memoized值。这种优化有助于避免在每次渲染时都进行高额开销的计算

请记住,传入useMemo的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于useEffect的范畴

useRef
const refContainer = useRef(initialValue)

useRef返回一个可变的ref对象,其.current属性被初始化为传入的参数(initialValue)。返回的ref对象在组件的整个生命周期内保持不变

这是一个例子获取上一轮的props或state:

function Counter() {
  const [count, setCount] = useState(0);

  const prevCountRef = useRef();
  useEffect(() => {
    prevCountRef.current = count;
  });
  const prevCount = prevCountRef.current;

  return <h1>Now: {count}, before: {prevCount}</h1>;
}
useImperativeHandle
useImperativeHandle(red, createHandle, [deps])

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

function FancyInput(props, ref){
    const inputRef = useRef();
    useImperativeHandle(ref, ()=>({
        focus: ()=>{
            inputRef.current.focus();
        }
    }));
    return <input ref={inputRef} ... />
}
FancyInput = forwardRef(FancyInput);
useLayoutEffect

其函数签名与useEffect相同,但它会在所有的DOM变更之后同步调用effect。可以使用它来读取DOM布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect内部的更新计划将被同步刷新

尽可能使用标准的useEffect以避免阻塞视觉更新

自定义Hook

在class组件中,要想复用状态逻辑则使用render props高阶组件两种方法,而在Hook中可以把这些状态逻辑提取到一个可重用的函数中,也就是自定义Hook

自定义Hook是一个函数,以“use”开头,函数内部可以调用其他的Hook

例如之前一个例子获取上一轮的props和state,可以使用自定义的Hook如下:

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return <h1>Now: {count}, before: {prevCount}</h1>;
}

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

使用自定义Hook有以下几个注意点:

  1. 自定义Hook必须以“use”开头
  2. 两个组件中使用相同的Hook并不会共享state
  3. 自定义Hook获取state是完全独立的

最后,看了官方文档Ryan Florence介绍React Hook应用,写了例子自定义hook demo:

import Row from "./Row";
import React, { useContext, useEffect, useState } from "react";
import { ThemeContext } from "./context";
import "./index.css";

function Greeting(props) {
  const name = useFormInput("xiaoxu");
  const surname = useFormInput("fangfang");
  const theme = useContext(ThemeContext);
  const width = useWindowWidth();

  useDocumentTitle(name.value + "-" + surname.value);

  return (
    <section className={theme}>
      <Row label="Name">
        <input {...name} />
      </Row>
      <Row label="Surname">
        <input {...surname} />
      </Row>
      <Row label="Width">{width}</Row>
    </section>
  );
}

// 自定义副作用title-Hook
function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title;
  });
}

// 自定义width-Hook
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const handleResize = () => {
      setWidth(window.innerWidth);
    };
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  });
  return width;
}

// 自定义input-Hook
function useFormInput(name) {
  const [value, setValue] = useState(name);
  function handleChange(e) {
    setValue(e.target.value);
  }
  return {
    value,
    onChange: handleChange
  };
}

export default Greeting;

效果如下:

自定义hooks.png