React Hooks 用法详解

React 中提供的 hooks:

  • useState:setState

  • useReducer:setState,同时 useState 也是该方法的封装

  • useRef: ref

  • useImperativeHandle: 给 ref 分配特定的属性

  • useContext: context,需配合 createContext 使用

  • useMemo: 可以对 setState 的优化

  • useCallback: useMemo 的变形,对函数进行优化

  • useEffect: 类似 componentDidMount/Update, componentWillUnmount,当效果为 componentDidMount/Update 时,总是在整个更新周期的最后(页面渲染完成后)才执行

  • useLayoutEffect: 用法与 useEffect 相同,区别在于该方法的回调会在数据更新完成后,页面渲染之前进行,该方法会阻碍页面的渲染

  • useDebugValue:用于在 React 开发者工具中显示自定义 hook 的标签

官网地址:https://react-1251415695.cos-website.ap-chengdu.myqcloud.com/docs/hooks-reference.html#basic-hooks

一、Hooks 初体验

example: 

import React, { useState  } from 'react';

function Example() {
    // 声明一个名为“count”的新状态变量
    const [count, setCount] = useState(0);

    return (
        

You clicked {count} times

); } export default Example;

useState 就是一个 Hook,可以在我们不使用 class 组件的情况下,拥有自身的 state,并且可以通过修改 state 来控制 UI 的展示。

1、useState状态

语法:

const [state, setState] = useState(initialState)
  • 传入唯一的参数: initialState,可以是数字,字符串等,也可以是对象或者数组。
  • 返回的是包含两个元素的数组:第一个元素,state 变量,setState 修改 state值的方法。

与在类中使用 setState 的异同点:

  • 相同点:在一次渲染周期中调用多次 setState,数据只改变一次。
  • 不同点:类中的 setState 是合并,而函数组件中的 setState 是替换。

使用对比

之前想要使用组件内部的状态,必须使用 class 组件,例如:

import React, { Component } from 'react';

export default class Example extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }

    render() {
        return (
            

You clicked {this.state.count} times

); } }

而现在,我们使用函数式组件也可以实现一样的功能了。也就意味着函数式组件内部也可以使用 state 了。

import React, { useState } from 'react';

function Example() {
    // 声明一个名为“count”的新状态变量
    const [count, setCount] = useState(0);

    return (
        

You clicked {count} times

); } export default Example;

优化

创建初始状态是比较昂贵的,所以我们可以在使用 useState API 时,传入一个函数,就可以避免重新创建忽略的初始状态。

普通的方式:

// 直接传入一个值,在每次 render 时都会执行 createRows 函数获取返回值
const [rows, setRows] = useState(createRows(props.count));

优化后的方式(推荐):

// createRows 只会被执行一次
const [rows, setRows] = useState(() => createRows(props.count));

2、useEffect执行副作用操作

  • effect(副作用):指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。
  • 副作用操作可以分两类:需要清除的和不需要清除的
  • 需要清除的,比如开启的定时器,订阅外部数据源等,这些操作如果在组件消亡后不及时清除会导致内存泄漏。

  • 不需要清除的,比如发起网络请求,手动变更 DOM,记录日志等。

  • 原先在函数组件内(这里指在 React 渲染阶段)改变 dom 、发送 ajax 请求以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性
  • useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途,只不过被合并成了一个 API
  • useEffect 接收一个函数,该函数会在组件渲染到屏幕之后才执行,该函数有要求:要么返回一个能清除副作用的函数,要么就不返回任何内容
  • componentDidMountcomponentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。

语法:

1、useEffect(() => { doSomething });

2、useEffect(() => { doSomething },[]);

3、useEffect(() => { doSomething },[count]);
  •  
  • 第一个参数为 effect 函数,该函数将在 componentDidMmount 时触发和 componentDidUpdate 时有条件触发(该添加为useEffect 的第二个数组参数)

  • 第二个参数是可选的,根据条件限制看是否触发
    • 如果不传,如语法1,则每次页面数据有更新(如componentDidUpdate),都会触发 effect。

    • 如果为空数组[],如语法2,则每次初始化的时候只执行一次effect(如componentDidMmount)

    • 如果只需要在指定变量变化时触发 effect,将该变量放入数组。如语法3,count只要变化,就会执行effect,如观察者监听

  • 清除副作用

  • 副作用函数还可以通过返回一个函数来指定如何清除副作用,为防止内存泄漏,清除函数会在组件卸载前执行。如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除。

  • 例1、比如window.addEventListener('resize', handleResize);:监听resize等

  useEffect(() => {
    window.addEventListener('resize', handleResize);
    window.addEventListener('keydown', onKeyDown);
    window.addEventListener('keyup', onKeyUp);
    return (() => {
      window.removeEventListener('resize', handleResize);
      window.removeEventListener('keydown', onKeyDown);
      window.removeEventListener('keyup', onKeyUp);
    })
  }, [globalRef]);
  • 例2、清除定时器

  • function Counter(){
        let [number,setNumber] = useState(0);
        let [text,setText] = useState('');
        useEffect(()=>{
            let $timer = setInterval(()=>{
                setNumber(number=>number+1);
            },1000);
            // useEffect 如果返回一个函数的话,该函数会在组件卸载和更新时调用
            // useEffect 在执行副作用函数之前,会先调用上一次返回的函数
            // 如果要清除副作用,要么返回一个清除副作用的函数
           /*  return ()=>{
                console.log('destroy effect');
                clearInterval($timer);
            } */
        });
        // },[]);//要么在这里传入一个空的依赖项数组,这样就不会去重复执行
        return (
            <>
              setText(event.target.value)}/>
              

    {number}

    ) }

     

3、useContext组件之间传值

语法

const value = useContext(MyContext);

之前在用类声明组件时,父子组件的传值是通过组件属性和props进行的,那现在使用方法(Function)来声明组件,已经没有了constructor构造函数也就没有了props的接收,但是也可以直接收,如下:

组件:



接收:
const SwitchList = ({dataList = null, isReverse = false}: any): React.ReactElement => {
    //TODO
}

React Hooks 也为我们准备了useContext。它可以帮助我们跨越组件层级直接传递变量,实现共享。

一:利用 createContext 创建上下文

import React, { useState , createContext } from 'react';

// 创建一个 CountContext
const CountContext = createContext()

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

You clicked {count} times

{/* 将 context 传递给 子组件,context 值由value props决定*/}
) } export default Example;

二:使用useContext 获取上下文

对于要接收context的后代组件,只需引入 useContext() Hooks 即可。

function Counter(){
  const count = useContext(CountContext)  //一句话就可以得到count
  return (

{count}

) }

强调一点:

useContext 的参数必须是 context 对象本身:

  • 正确: useContext(MyContext)
  • 错误: useContext(MyContext.Consumer)
  • 错误: useContext(MyContext.Provider)

当组件上层最近的 更新时,该 Hook 会触发重渲染,并使用最新传递给 的 context value 值。

4、useReducer处理更为复杂state结构

语法

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

useReducer 接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

我们可以使用 useReducer 来重新写我们开篇计数器的demo:

Example:

import React, { useReducer } from 'react';

const initialState = {count: 0};

function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return {count: state.count + 1};
        case 'decrement':
            return {count: state.count - 1};
        default:
            throw new Error();
    }
}

export default () => {
    
    // 使用 useReducer 函数创建状态 state 以及更新状态的 dispatch 函数
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
        <>
            Count: {state.count}
            
); }

优化:延迟初始化

还可以懒惰地创建初始状态。为此,您可以将init函数作为第三个参数传递。初始状态将设置为 init(initialArg)

它允许您提取用于计算 reducer 外部的初始状态的逻辑。这对于稍后重置状态以响应操作也很方便:

Example.js

import React, { useReducer } from 'react';

function init(initialCount) {
    return {count: initialCount};
}
  
function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return {count: state.count + 1};
        case 'decrement':
            return {count: state.count - 1};
        case 'reset':
            return init(action.payload);
        default:
            throw new Error();
    }
}

export default ({initialCount = 0}) => {
    
    const [state, dispatch] = useReducer(reducer, initialCount, init);
    return (
        <>
            Count: {state.count}
            
); }

与 useState 的区别

  • state 状态值结构比较复杂时,使用 useReducer 更有优势。
  • 使用 useState 获取的 setState 方法更新数据时是异步的;而使用 useReducer 获取的 dispatch 方法更新数据是同步的。

针对第二点区别,我们可以演示一下: 在上面 useState 用法的例子中,我们新增一个 button

useState 中的 Example.js

import React, { useState } from 'react';

function Example() {
    // 声明一个名为“count”的新状态变量
    const [count, setCount] = useState(0);

    return (
        

You clicked {count} times

); } export default Example;

点击 测试能否连加两次 按钮,会发现,点击一次, count 还是只增加了 1,由此可见,useState 确实是 异步 更新数据;

在上面 useReducer 用法的例子中,我们新增一个 button: useReducer 中的 Example.js

import React, { useReducer } from 'react';

const initialState = {count: 0};

function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return {count: state.count + 1};
        case 'decrement':
            return {count: state.count - 1};
        default:
            throw new Error();
    }
}

export default () => {
    
    // 使用 useReducer 函数创建状态 state 以及更新状态的 dispatch 函数
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
        <>
            Count: {state.count}
            
); }

点击 测试能否连加两次 按钮,会发现,点击一次, count 增加了 2,由此可见,每次dispatch 一个 action 就会更新一次数据,useReducer 确实是 同步 更新数据;

对于 useReducer 和 useState的区别主要是以下两点:

  • 当 state 状态值结构比较复杂时,使用 useReducer 更有优势。
  • 使用 useState 获取的 setState 方法更新数据时是异步的;而使用 useReducer 获取的 dispatch 方法更新数据是同步的。

 

5、useMemo性能优化

语法:

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

返回一个memoized值。 传递“创建”函数和依赖项数组。useMemo只会在其中一个依赖项发生更改时重新计算memoized值。此优化有助于避免在每个渲染上进行昂贵的计算。

useMemo在渲染过程中传递的函数会运行。不要做那些在渲染时通常不会做的事情。例如,副作用属于useEffect,而不是useMemo。

用法

useMemo 可以帮助我们优化子组件的渲染,比如这种场景: 在 A 组件中有两个子组件 B 和 C,当 A 组件中传给 B 的 props 发生变化时,A 组件状态会改变,重新渲染。此时 B 和 C 也都会重新渲染。其实这种情况是比较浪费资源的,现在我们就可以使用 useMemo 进行优化,B 组件用到的 props 变化时,只有 B 发生改变,而 C 却不会重新渲染。

例子:

ExampleA.js

import React from 'react';

export default ({ text }) => {
    
    console.log('Example A:', 'render');
    return 
Example A 组件:{ text }
}

ExampleB.js

import React from 'react';

export default ({ text }) => {
    
    console.log('Example B:', 'render');
    return 
Example B 组件:{ text }
}

App.js

import React, { useState } from 'react';
import ExampleA from './ExampleA';
import ExampleB from './ExampleB';

import './App.css';

export default () => {

    const [a, setA] = useState('ExampleA');
    const [b, setB] = useState('ExampleB');

    return (
        

    
) }

此时我们点击上面任意一个按钮,都会看到控制台打印了两条输出, A 和 B 组件都会被重新渲染。

现在我们使用 useMemo 进行优化

App.js

import React, { useState, useMemo } from 'react';
import ExampleA from './ExampleA';
import ExampleB from './ExampleB';

import './App.css';

export default () => {

    const [a, setA] = useState('ExampleA');
    const [b, setB] = useState('ExampleB');

+    const exampleA = useMemo(() => , [a]);
+    const exampleB = useMemo(() => , [b]);

    return (
        
+ {/* + */} + { exampleA } + { exampleB }
    
) }

此时我们点击不同的按钮,控制台都只会打印一条输出,改变 a 或者 b,A 和 B 组件都只有一个会重新渲染。

6、useCallback优化函数式组件性能

语法:

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

返回值 memoizedCallback 是一个 memoized 回调。传递内联回调和一系列依赖项。useCallback将返回一个回忆的memoized版本,该版本仅在其中一个依赖项发生更改时才会更改。当将回调传递给依赖于引用相等性的优化子组件以防止不必要的渲染(例如shouldComponentUpdate)时,这非常有用。

这个 Hook 的 API 不能够一两句解释的清楚,建议看一下这篇文章:useHooks 第一期:聊聊 hooks 中的 useCallback。里面介绍的比较详细。

7、useRef获取dom

语法:

const refContainer = useRef(initialValue);

类组件、React 元素用 React.createRef,如:remindRef: any = React.createRef();通过 this.remindRef.current获取

函数组件使用 useRef,如let globalToolRef: any = useRef(null);通过globalToolRef.current获取

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传递的参数(initialValue)。返回的对象将存留在整个组件的生命周期中。

  • 从本质上讲,useRef就像一个“盒子”,可以在其.current财产中保持一个可变的价值。
  • useRef() Hooks 不仅适用于 DOM 引用。 “ref” 对象是一个通用容器,其 current 属性是可变的,可以保存任何值(可以是元素、对象、基本类型、甚至函数),类似于类上的实例属性。

注意:useRef() 比 ref 属性更有用。与在类中使用 instance(实例) 字段的方式类似,它可以 方便地保留任何可变值。

注意,内容更改时useRef 不会通知您。变异.current属性不会导致重新渲染。如果要在React将引用附加或分离到DOM节点时运行某些代码,则可能需要使用回调引用。

使用

下面这个例子中展示了可以在 useRef() 生成的 refcurrent 中存入元素、字符串

Example.js

import React, { useRef, useState, useEffect } from 'react'; 

export default () => {
    
    // 使用 useRef 创建 inputEl 
    const inputEl = useRef(null);

    const [text, updateText] = useState('');

    // 使用 useRef 创建 textRef 
    const textRef = useRef();

    useEffect(() => {
        // 将 text 值存入 textRef.current 中
        textRef.current = text;
        console.log('textRef.current:', textRef.current);
    });

    const onButtonClick = () => {
        // `current` points to the mounted text input element
        inputEl.current.value = "Hello, useRef";
    };

    return (
        <>
            {/* 保存 input 的 ref 到 inputEl */}
            
            
            

updateText(e.target.value)} /> ); }

点击 在 input 上展示文字 按钮,就可以看到第一个 input 上出现 Hello, useRef;在第二个 input 中输入内容,可以看到控制台打印出对应的内容。

8、useLayoutEffect

语法:

useLayoutEffect(() => { doSomething });

useEffect Hooks 类似,都是执行副作用操作。但是它是在所有 DOM 更新完成后触发。可以用来执行一些与布局相关的副作用,比如获取 DOM 元素宽高,窗口滚动距离等等。

进行副作用操作时尽量优先选择 useEffect,以免阻止视觉更新。与 DOM 无关的副作用操作请使用 useEffect

用法

用法与 useEffect 类似。

Example.js

import React, { useRef, useState, useLayoutEffect } from 'react'; 

export default () => {

    const divRef = useRef(null);

    const [height, setHeight] = useState(100);

    useLayoutEffect(() => {
        // DOM 更新完成后打印出 div 的高度
        console.log('useLayoutEffect: ', divRef.current.clientHeight);
    })
    
    return <>
        
Hello
}

注意:

 

 

  1. useLayoutEffect 相比 useEffect,通过同步执行状态更新可解决一些特性场景下的页面闪烁问题。
  2. useEffect 可以满足百分之99的场景,而且 useLayoutEffect 会阻塞渲染,请谨慎使用。
  3. useEffect 在全部渲染完毕后才会执行
  4. useLayoutEffect 会在 浏览器 layout 之后,painting 之前执行
  5. 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect
  6. 可以使用它来读取 DOM 布局并同步触发重渲染
  7. 在浏览器执行绘制之前 useLayoutEffect 内部的更新计划将被同步刷新
  8. 尽可能使用标准的 useEffect 以避免阻塞视图更新

9、forwardRef

因为函数组件没有实例,所以函数组件无法像类组件一样可以接收 ref 属性

function Parent() {
    return (
        <>
         //  这样是不行的
            
            
        
    )
}
  • forwardRef 可以在父组件中操作子组件的 ref 对象
  • forwardRef 可以将父组件中的 ref 对象转发到子组件中的 dom 元素上
  • 子组件接受 props 和 ref 作为参数
function Child(props,ref){
  return (
    
  )
}
Child = React.forwardRef(Child);
function Parent(){
  let [number,setNumber] = useState(0); 
  // 在使用类组件的时候,创建 ref 返回一个对象,该对象的 current 属性值为空
  // 只有当它被赋给某个元素的 ref 属性时,才会有值
  // 所以父组件(类组件)创建一个 ref 对象,然后传递给子组件(类组件),子组件内部有元素使用了
  // 那么父组件就可以操作子组件中的某个元素
  // 但是函数组件无法接收 ref 属性  这样是不行的
  // 所以就需要用到 forwardRef 进行转发
  const inputRef = useRef();//{current:''}
  function getFocus(){
    inputRef.current.value = 'focus';
    inputRef.current.focus();
  }
  return (
      <>
        
        
        
      
  )
}

 

10、useImperativeHandle

  • useImperativeHandle可以让你在使用 ref 时,自定义暴露给父组件的实例值,不能让父组件想干嘛就干嘛
  • 在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用
  • 父组件可以使用操作子组件中的多个 ref
import React,{useState,useEffect,createRef,useRef,forwardRef,useImperativeHandle} from 'react';

function Child(props,parentRef){
    // 子组件内部自己创建 ref 
    let focusRef = useRef();
    let inputRef = useRef();
    useImperativeHandle(parentRef,()=>(
      // 这个函数会返回一个对象
      // 该对象会作为父组件 current 属性的值
      // 通过这种方式,父组件可以使用操作子组件中的多个 ref
        return {
            focusRef,
            inputRef,
            name:'计数器',
            focus(){
                focusRef.current.focus();
            },
            changeText(text){
                inputRef.current.value = text;
            }
        }
    });
    return (
        <>
            
            
        
    )

}
Child = forwardRef(Child);
function Parent(){
  const parentRef = useRef();//{current:''}
  function getFocus(){
    parentRef.current.focus();
    // 因为子组件中没有定义这个属性,实现了保护,所以这里的代码无效
    parentRef.current.addNumber(666);
    parentRef.current.changeText('');
    console.log(parentRef.current.name);
  }
  return (
      <>
        
        
      
  )
}

官网介绍forwardRef与useImperativeHandle结合使用

React Hooks 用法详解_第1张图片

 

交流

对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!欢迎关注公众号共同学习。

                                                    

 

你可能感兴趣的:(react,react,react,hooks,api,react,hooks,详解,react,hooks,知识点,react,hooks)