React Hooks 介绍、原理和API使用场景

作者: 贺鹏飞 分类: React 发布时间: 2021-01-22 22:38

概念

  • React Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性;
  • 以前在编写函数式组件,组件需要自己的 state 的时候,通常我们会转化成 class 组件来做。现在可以在函数组件中使用 Hook 来实现。

Hooks的原理

– 单向链表通过next把hooks串联起来;
– memoizedState存在fiber node上,组件之间不会相互影响;
– useState和useReducer中通过dispatchAction调度更新任务。

React Hooks,它带来了那些便利

  • 代码逻辑聚合,逻辑复用
  • HOC嵌套地狱
  • 代替class

React 中通常使用 类定义 或者 函数定义 创建组件:

在类定义中,我们可以使用到许多 React 特性,例如 state、 各种组件生命周期钩子等,但是在函数定义中,我们却无能为力,因此 React 16.8 版本推出了一个新功能 (React Hooks),通过它,可以更好的在函数定义组件中使用 React 特性。

好处:

  1. 跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;
  2. 类定义更为复杂
  • 不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
  • 时刻需要关注this的指向问题;
  • 代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;
  1. 状态与UI隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。

遵循的规则

  • 避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
  • 只有 函数定义组件 和 hooks 可以调用 hooks,避免在 类组件 或者 普通函数 中调用;
  • 不能在useEffect中使用useState,React 会报错提示;
  • 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存。

类被会替代吗?

Hooks不会替换类,它们只是一个你可以使用的新工具。React 团队表示他们没有计划在React中弃用类,所以如果你想继续使用它们,可以继续用。

我能体会那种总有新东西要学的感觉有多痛苦,不会就感觉咱们总是落后一样。Hooks 可以当作一个很好的新特性来使用。当然没有必要用 Hook 来重构原来的代码, React团队也建议不要这样做。

Hooks API

一:useState

在组件中,我们难免使用state来进行数据的实时响应,这是react框架的一大特性,只需更改state,组件就会重新渲染,试图也会响应更新。userState是一个方法,方法返回值为当前state以及更新state的函数。

不同于react在class可以直接定义state,或者是在constructor中使用this.state来直接定义state值,在hooks中使用state需要useState函数,如下:

import React, { useState, useEffect } from 'react';
function Hooks() {
  const [count, setCount] = useState(0);
  const [age] = useState(16);
  useEffect(() => {
    console.log(count);
  });
  return (
    <div>
      <p>小女子芳年{age}</p>
      <p>计数器目前值为{count}</p>
      <button type="button" onClick={() => { setCount(count + 1); }}>点击+1</button>
      <button type="button" onClick={() => { setCount(count - 1); }}>点击-1</button>
    </div>
  );
}
export default Hooks;

在上面的例子中,我们使用了useState定义了两个state变量,count和age,其中定义count的时候还定义了setCount,就是用来改变count值的函数。在class类中,改变state是使用setState函数,而在hooks中是定义变量的同时定义一个改变变量的函数。

userState是一个方法,方法返回值为当前state以及更新state的函数,所以,在上面的例子中,我们用const [count, setCount] = useState(0);将count和setCount解构出来,而userState方法的参数就是state的初始值。当然count和与之对应的改变函数名称并不一定非得是setCount,名称可以随便起,只要是一块解构出来的即可。

在class组件中,我们可以用setState一次更改多个state值而只渲染一次,同样的,在hooks中,我们调用多个改变state的方法,也只是渲染一次。

二:userEffect

在class组件中,有生命周期的概念,最常用的,我们通常会在componentDidMount这个生命周期中做数据请求,偶尔,我们也会用一些其它的生命周期,像是componentDidUpdata,componentWillReceiveProps等。在hooks中,没有生命周期的概念,但是,有副作用函数useEffect。

默认情况下,useEffect会在第一次和每次更新之后都会执行,useEffect函数接受两个参数,第一个参数是一个函数,每次执行的就是函数中的内容,第二个函数是个数组,数组中可选择性写state中的数据,代表只有当数组中的state发生变化是才执行函数内的语句。如果是个空数组,代表只执行一次,类似于componentDidUpdata。所以,向后端请求可以写成下面这种方式:

// 页面进来只调用一次
useEffect(()=>{
    axios.get('/getYearMonth').then(res=> {
        console.log('getYearMonth',res);
        setValues(oldValues => ({
            ...oldValues,
            fileList:res.data.msg
        }));
    })
},[]);

useEffect函数会在浏览器完成画面渲染之后延迟调用。在一个hooks函数中,可以同时存在多个effect函数,所以,当有需求每次更新都执行useEffect中的代码时,可以用一个useEffect请求数据,用其他的useEffect做另外的事情。只需根据第二个参数即可区别不同作用。

//官方示例性能优化
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

三:userContext

使用userContext,不仅可以实现父子组件传值,还可以跨越多个层级进行传值,例如父组件可以给孙子组件甚至重孙子组件进行直接传值等,redux全局状态管理本质上也是对content的一种应用。

在hooks中使用content,需要使用createContext,useContext,废话不多说,直接示例展示用法

// context.js  新建一个context
import { createContext } from 'react';
const ShowContext = createContext('aaa');
export default ShowContext;
// HooksContext.jsx  父组件,提供context
import React, { useState } from 'react';
import Show from './Show.jsx';
import ShowContext from './context';
function HooksContext() {
  const [count, setCnt] = useState(0);
  const [age, setAge] = useState(16);
  function clear() {
    setCnt(0);
    setAge(16);
  }
  return (
    <div>
      <p>小女子芳年{age}</p>
      <p>你点击了{count}次</p>
      <button
        type="button"
        onClick={() => { setCnt(count + 1); setAge(age + 1); }}
      >
        点击+1
      </button>
      <ShowContext.Provider value={{ count, age, clear }}>
        <Show />
      </ShowContext.Provider>
    </div>
  );
}
export default HooksContext;

// Show.jsx  子组件,使用context
import React, { useContext } from 'react';
import ShowContext from './context';
function Show() {
  const { count, age, clear } = useContext(ShowContext);
  return (
    <div>
      数量:{count}
      年龄:{age}
      <button
        type="button"
        onClick={() => { clear(); }}
      >
        复原
      </button>
    </div>
  );
}
export default Show;

上面是一个完整的使用content实现父子组件传值的过程,如果Show组件下还有子组件,无论多少层,都可以用useContext直接取到HooksContext父组件提供的值,而context.js文件是新建一个context,新建必须要单独列出来,否则子组件无法使用useContext。

content提供了一种树状结构,被Context.Provider所包裹的所有组件,都可以直接取数据。redux就是利用了context的这种特性实现全局状态管理。在下面的几小节中,我们会讲hooks中context搭配useReducer来实现redux的功能。

四:userReducer

userReducer是useState的替代方案,它接收一个形如(state,action) => newState 的reducer,并返回当前的state以及其配套的dispatch方法。
总的来说呢,userReducer可以接受两个参数,第一个参数就是和redux中的reducer一样的纯函数,第二个参数是state的初始值,并返回当前state以及dispatch。

还是以官方示例的计数器为例:

import React, { useReducer } from 'react';
function countReducer(state, action) {
  switch (action.type) {
    case 'add':
      return state + 1;
    case 'minus':
      return state - 1;
    default:
      return state;
  }
}
function HooksEffect() {
  const [count, dispatch] = useReducer(countReducer, 0);
  return (
    <div>
      <p>你点击了{count}次</p>
      <button
        type="button"
        onClick={() => { dispatch({ type: 'add' }); }}
      >
        点击+1
      </button>
      <button
        type="button"
        onClick={() => { dispatch({ type: 'minus' }); }}
      >
        点击-1
      </button>
    </div>
  );
}
export default HooksEffect;

相比起redux还需要connect高阶函数包裹一下才能将dispatch和state注入到props中,hooks中使用reducer更加简洁。

useReducer替代Redux案例

我们会用context和useReducer来实现redux的效果。依然是使用计数器这个功能,先贴代码,后面会详细讲解:

// count.js  定义context和reducer,导出context和包含reducer的context包裹组件。
import React, { createContext, useReducer } from 'react';
function countReducer(state, action) {
  switch (action.type) {
    case 'add':
      return state + 1;
    case 'minus':
      return state - 1;
    default:
      return state;
  }
}
const ADDCOUNT = 'add';
const MINUSCOUNT = 'minus';
export const CountContext = createContext();
export const CountWrap = (props) => {
  const [count, dispatch] = useReducer(countReducer, 0);
  return (
    <CountContext.Provider
      value={{ count, dispatch, ADDCOUNT, MINUSCOUNT }}
    >
      {props.children}
    </CountContext.Provider>
  );
};

// ReducerToRedux.jsx,连接组件,
import React from 'react';
import Button from './Button';
import Show from './Show';
import { CountWrap } from './count';

function ReducerToRedux() {
  return (
    <div>
      <CountWrap>
        <Show />
        <Button />
      </CountWrap>
    </div>
  );
}
export default ReducerToRedux;

// Show.jsx  显示当前数值的组件
import React, { useContext } from 'react';
import { CountContext } from './count';
function ReducerToRedux() {
  const { count } = useContext(CountContext);
  return (
    <div>现在的计数器值为:{count}</div>
  );
}
export default ReducerToRedux;

// Button.jsx  按钮组件,可以实现计数器的增和减
import React, { useContext } from 'react';
import { CountContext } from './count';

function ReducerToRedux() {
  const { dispatch, ADDCOUNT, MINUSCOUNT } = useContext(CountContext);
  return (
    <div>
      <button
        type="button"
        onClick={() => { dispatch({ type: MINUSCOUNT }); }}
      >点我-1</button>
      <button
        type="button"
        onClick={() => { dispatch({ type: ADDCOUNT }); }}
      >点我+1</button>
    </div>
  );
}
export default ReducerToRedux;

通过reducer和context实现计数器的功能,我们共用了四个文件,当然count.js这个文件本应该拆分成三个文件,常量单独定义一个文件,reducer纯函数也应该单独定一个文件,不过代码不多,就暂时合一块了。

在count.js中,我们导出CountContext和CountWrap,其中,CoutWrap就是provider,也就是只要被CountWrap包裹过的组件,就可以使用userContent取到传递数据,而CountContext就是用createContext新建的一个content,使用useContext取传递数据的时候会用到。同时,在这个文件中,我们还将从useReducer解构出的count和dispatch,以及常量增减通过provider传递给包裹组件,使被包裹的组件可以通过useContext取到这些数据。函数countReducer就是和redux中的reducer一样的纯函数,子组件dispatch action,reducer则是接受当前state和action,通过判断action,返回新的state。

在Button组件中,我们通过countContext取到dispatch及常量,改变count这个数值,在Show组件中,只是展示count,在ReducerToRedux文件中,是做一个连接器,用CountWrap包裹Button和Show组件。

可能说的有些啰嗦,看上去有些复杂,其实稍一整理,原理很简单,自己写一遍整理清楚逻辑使用上就很简单了。

五:useMemo

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

useMemo是函数式组件官方提供的性能优化的一个方法,接受两个参数,第一个参数是要执行的函数,第二个参数是state中的值或者父组件传下来的值,代表只有当第二个参数的值发生变化时,才执行函数。其中,第二个参数是数组,可以同时优化多个state或者父组件传下来的参数,首次渲染组件是,如果页面用到要优化的值,函数会执行。

我们还是以计数器以及年龄为例:

import React, { useState, useMemo } from 'react';
function Show({ count, age, clear }) {
  function ageChange(value) {
    console.log(value);
    return value + 2;
  }
  const myAge = useMemo(() => ageChange(age), [age]);
  return (
    <div>
      数量:{count}  我的年龄:{myAge}
      <button
        type="button"
        onClick={() => { clear(); }}
      >复原</button>
    </div>
  );
}
function HooksUseMome() {
  const [count, setCnt] = useState(0);
  const [age, setAge] = useState(16);
  function clear() {
    setCnt(0);
    setAge(16);
  }
  return (
    <div>
      <p>小女子芳年{age}</p>
      <p>你点击了{count}次</p>
      <button
        type="button"
        onClick={() => { setAge(age + 1); }}
      >点击年龄+1</button>
      <button
        type="button"
        onClick={() => { setCnt(count + 1); }}
      >点击计数器+1</button>
      <Show count={count} age={age} clear={clear} />
    </div>
  );
}
export default HooksUseMome;

在上面的例子中,父组件小女子初始年龄为16岁,而到子组件经过ageChange函数,返回我的年龄永远比小女子年龄大两岁。

但是如果没有useMemo,当父组件的计数器count值发生变化时,子组件的ageChange函数也会执行,这不是我们想要的结果,我们只想当小女子的年龄发生变化时,再执行ageChange函数。所以,用useMemo可以实现我们想要的效果。如上面代码所示const myAge = useMemo(() => ageChange(age), [age]);,使用useMemo,第二个参数是age,这样,只有当age发生变化时,才执行其中的函数。

在类组件中,有shouldComponentDidUpdata生命周期,我们可以在其中做监测,当检测到state值没发生变化时,直接不渲染组件,而useMemo和这个生命周期还有些许不同。它是当检测的state发生变化时而执行某些函数,避免额外的开销,节省性能。

六: useRef

在项目开发中,我们比较少用到ref,一般我们不直接操作DOM,都是通过状态来控制DOM,不过在某些情况下,可能还是会用到ref,这一节我们通过对input输入框数据的双向绑定来认识useRef。

import React, { useState, useRef } from 'react';
function HooksUseRef() {
  const [inputValue, setInputValue] = useState();
  const inputRef = useRef(null);
  function inputChangeHandle(e) {
    setInputValue(e.target.value);
  }
  function inputRefChangeHandle() {
    console.log(inputRef.current.value);
  }
  return (
    <div>
      <div>
        <input
          value={inputValue}
          onChange={inputChangeHandle}
          type="text"
        />
        <span>使用state绑定inputValue值</span>
      </div>
      <div>
        <input
          ref={inputRef}
          onChange={inputRefChangeHandle}
          type="text"
        />
        <span>使用Ref绑定inputValue值</span>
      </div>
    </div>
  );
}
export default HooksUseRef;

在上面的案例中,我们如果要取input的值,如果是state双向绑定,可以直接取inputValue,如果是用ref,则可以通过inputRef.current.value取到值
通过const inputRef = useRef(null);,我们获取到的是一个对象,而current属性就是其中的dom元素。

七: useCallBack

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

简而言之,useCallBack是用来缓存函数的,在class类中,我们通常在constructor中使用this.fn = this.fn.bind(this)来绑定this,是每次调用的fn都是之前的fn,而不用开辟新的函数。而useCallback同样有此功能,useCallBack和useMemo的不同点在于useMemo相当于缓存state,而useCallBack相当于缓存函数,官方给的解释是这样的useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。.

我们下面还是用计数器和年龄做例子:

import React, { useState, useEffect, useCallback } from 'react';
function Show({ countCallBack, ageCallBack }) {
  const [count, setCount] = useState(() => { countCallBack(); });
  const [age, setAge] = useState(() => { ageCallBack(); });
  useEffect(() => {
    setCount(countCallBack());
  }, [countCallBack]);
  useEffect(() => {
    setAge(ageCallBack());
  }, [ageCallBack]);
  return (
    <div>
      数量:{count}  年龄:{age}
    </div>
  );
}
function HooksCallBack() {
  const [count, setCnt] = useState(0);
  const [age, setAge] = useState(16);
  const countCallBack = useCallback(() => {
    return count;
  }, [count]);
  const ageCallBack = useCallback(() => {
    return age;
  }, []);
  return (
    <div>
      <p>小女子芳年{age}</p>
      <p>你点击了{count}次</p>
      <button
        type="button"
        onClick={() => { setAge(age + 1); }}
      >点击年龄+1</button>
      <button
        type="button"
        onClick={() => { setCnt(count + 1); }}
      >点击计数器+1</button>
      <Show countCallBack={countCallBack} ageCallBack={ageCallBack} />
    </div>
  );
}
export default HooksCallBack;

在上面的例子中,只有点击计数器按钮,子组件才会跟着更新,点击年龄按钮子组件则不跟着更新。使用useCallback如果没有依赖,则只会执行一次,只有依赖改变,才会返回新的函数,我们可以根据这个规则实现bind的效果。

八: useImperativeHandle

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

useImperativeHandle(ref, createHandle, [deps])
  • 通过useImperativeHandle可以只暴露特定的操作
    • 通过useImperativeHandle的Hook, 将父组件传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起
    • 所以在父组件中, 调用inputRef.current时, 实际上是返回的对象
import React, { useRef, forwardRef, useImperativeHandle } from 'react'
const JMInput = forwardRef((props, ref) => {
  const inputRef = useRef()
  // 作用: 减少父组件获取的DOM元素属性,只暴露给父组件需要用到的DOM方法
  // 参数1: 父组件传递的ref属性
  // 参数2: 返回一个对象,父组件通过ref.current调用对象中方法
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus()
    },
  }))
  return <input type="text" ref={inputRef} />
})

export default function ImperativeHandleDemo() {
  // useImperativeHandle 主要作用:用于减少父组件中通过forward+useRef获取子组件DOM元素暴露的属性过多
  // 为什么使用: 因为使用forward+useRef获取子函数式组件DOM时,获取到的dom属性暴露的太多了
  // 解决: 使用uesImperativeHandle解决,在子函数式组件中定义父组件需要进行DOM操作,减少获取DOM暴露的属性过多
  const inputRef = useRef()
  return (
    <div>
      <button onClick={() => inputRef.current.focus()}>聚焦</button>
      <JMInput ref={inputRef} />
    </div>
  )
}

useImperativeHandle使用简单总结:

  • 作用: 减少暴露给父组件获取的DOM元素属性, 只暴露给父组件需要用到的DOM方法
  • 参数1: 父组件传递的ref属性
  • 参数2: 返回一个对象, 以供给父组件中通过ref.current调用该对象中的方法

九:useLayoutEffect

布局副作用

  • useEffect在浏览器渲染完成后执行
  • useLayoutEffect在浏览器渲染前执行

特点:

  • useLayoutEffect总是比useEffect先执行
  • useLayoutEffect里面的任务最好影响了Layout(布局)
import React, {useState, useRef, useLayoutEffect, useEffect} from "react";
import ReactDOM from "react-dom";
function App() {
  const [n, setN] = useState(0)
  const time = useRef(null)
  const onClick = ()=>{
    setN(i=>i+1) 
    time.current = performance.now()
  }
  useLayoutEffect(()=>{ // 改成 useEffect 试试
    if(time.current){
      console.log(performance.now() - time.current)
    }
  })
  return (
    <div className="App">
      <h1>n: {n}</h1>
      <button onClick={onClick}>Click</button>
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

十:useDebugValue

useDebugValue 用于在 React 开发者工具(如果已安装,在浏览器控制台 React 选项查看)中显示 自定义 Hook 的标签。

useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。

import React, { useState, useDebugValue } from 'react';
import ReactDOM from 'react-dom';
// 自定义 Hook
function useMyCount(num) {
  const [ count, setCount ] = useState(0);
  // 延迟格式化
  useDebugValue(count > num ? '溢出' : '不足', status => {
    return status === '溢出' ? 1 : 0;
  });
  const myCount = () => {
    setCount(count + 2);
  }
  return [ count, myCount ];
}
function App() {
  const [ count, seCount ] = useMyCount(10);
  return (
    <div>
      {count}
      <button onClick={() => seCount()}>setCount</button>
    </div>
  )
}
ReactDOM.render(<App />, root);

我们不推荐你向每个自定义 Hook 使用 useDebugValue,只有自定义 Hook 被复用时才最有意义。

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注