React Hooks

124 阅读7分钟

react在16.8版本后添加hooks(钩子函数),hooks是为了配合静态组件实现类组件的功能,它有着天然的优势:

(1)对静态组件没有破坏性改动,只是在原有代码上增加功能

(2)没有this

(3)函数式编程

(4)通过自定义hooks实现公共代码抽离

接下来就让我们来了解各种hooks函数和用法吧~

1.useState() 让静态组件有自身状态state

useState使静态组件拥有state实现自身状态更新,无需一直this访问内部state了

例:实现计数器

import { useState } from 'react';

const App=()=>{
    const [count,setCount]=useState(0);
    //[初始state,更新state的方法] = useState(state初始值)
    return(
        <>
            <p>count:{count}</p>
            <button onClick={()=>{setCount(count+1)}}>点击加一</button>
        </>
    )
}
export default App;

效果如下:

image.png

2.useEffect()让静态组件拥有自己的生命周期方法

useEffect有四种情况 取决于他的参数个数和是否带参

<1>useEffect(()=>{}); 相当于类组件的componentDidMount+componentDidUpdate

         说明:组件初始化执行一次,任何state改变都会执行一次

<2>useEffect(()=>{},[state]); 相当于类组件的componentDidMount+componentDidUpdate

         说明:组件初始化执行一次,仅指定state改变才执行一次

<3>useEffect(()=>{},[]); 仅相当于类组件的componentDidMount

         说明:组件初始化执行一次,更新state不执行

         说明2:初始化组件的ajax、绘制图形库ECharts

<4>useEffect(()=>{

          //代码

          return ()=>{//相当于类组件的componentWillUNmount
        }

},[])

四种情况就不一一演示了

就演示一个 指定state改变执行触发useEffect的案例吧

import { useState,useEffect } from 'react';
import axios from 'axios';

const App=()=>{
    const [email,setEmail]=useState('');
    const [loading,setLoading]=useState(true);
    //思路:loding为true时获取数据,获取到数据后更新email,将loading刷成false
    useEffect(()=>{
      const asyncEmail = async()=>{
        if(loading){
          const res = await axios.get("https:/randomuser.me/api")
          setEmail(res.data.results[0].email);
          setLoading(false);
        }
      }
      asyncEmail();
    },[loading])
    return(
        <>
            <p>email:{loading?"...loading":email}</p>
            <button onClick={()=>{setLoading(!loading)}}>点击获取随机email</button>
        </>
    )
}
export default App;

3.useReducer:修改state另一种方式,是useState替代方法

import { useReducer } from "react";

//创建初始state
let initialState = {count:0};

//创建reducer函数
const counter = (state,action)=>{
    switch(action.type){
        case 'ADD':
            return {count:state.count+1};
        case 'SUB':
            return {count:state.count-1};
        default:
            throw new Error('发生错误')
    }
}
const App = () => {
    let [state,dispatch] = useReducer(counter,initialState)
                    //调用useReducer功能
                    //1.注册counter这个reducer函数
                    //2.返回一个数组[initialState,f(){}]
    // console.log(state,dispatch);
    return ( 
        <>
            <h3>App组件</h3>
            <p>count:{state.count}</p>
            <button onClick={()=>{dispatch({type:"ADD"})}}>+</button>
            <button onClick={()=>{dispatch({type:"SUB"})}}>-</button>
        </>
     );
}
 
export default App;

多个状态互不影响

import { useReducer } from "react";

let initialState = {
    count1:0,
    count2:0
};
//创建reducer函数
const counter = (state,action)=>{
    switch(action.type){
        case 'ADD1':
            return {...state,count1:state.count1+1};
        case 'SUB1':
            return {...state,count1:state.count1-1};
        case 'ADD2':
            return {...state,count2:state.count2+1};
        case 'SUB2':
            return {...state,count2:state.count2-1};
        default:
            throw new Error('发生错误');
    }
}
const App = () => {
    let [state,dispatch] = useReducer(counter,initialState);
    return ( 
        <>
            <h3>App组件</h3>
            <p>count1:{state.count1}</p>
            <button onClick={()=>{dispatch({type:"ADD1"})}}>+</button>
            <button onClick={()=>{dispatch({type:"SUB1"})}}>-</button>
            <hr/>
            <p>count2:{state.count2}</p>
            <button onClick={()=>{dispatch({type:"ADD2"})}}>+</button>
            <button onClick={()=>{dispatch({type:"SUB2"})}}>-</button>
        </>
     );
}
 
export default App;

4.useContext 实现全局数据共享—组件跨层通信

创建countContext.js文件

import React from "react";

export const CounterContext = React.createContext();

创建父组件

import React,{useState} from "react";
import {CounterContext} from './CounterContext'
import Child from './Child';

const App = () => {
    const [count,setCount] = useState(0);
    return ( 
        <>
            <h3>App组件</h3>
            <p>count:{count}</p>
            <button onClick={()=>setCount(count+1)}>+</button>
            <CounterContext.Provider value={count} >
                <Child />
            </CounterContext.Provider>
        </>
     );
}
export default App;

在子组件/后代组件中使用

import { useContext } from "react";
import {CounterContext} from './CounterContext'
const Child = () => {
    let count = useContext(CounterContext);
    return ( 
        <>
            <h3>Child组件</h3> 
            <p>count:{count}</p>
        </>
     );
}
 
export default Child;

5.useRef返回一个ref对象{current:null},current属性存储DOM对象**--功能类似于类组件的this.refs、React.createRef()

特点: <1>ref对象的 current属性可以存储DOM对象也可以存储其他类型数据

        <2> 组件每次渲染的时候,返回同一个ref对象

        <3>对象在组件的整个生命周期内持续存在

       <4>变更 .current 属性不会引发组件重新渲染

案例:点击按钮,input框获得焦点

import {useRef} from 'react';

const App = () => {
  let inputRef = useRef(null);//创建了一个ref对象,初始值为null {current:null}
  console.log('inputRef',inputRef);//null
  let handdleClick=()=>{
      console.log('inputRef',inputRef);//ref属性赋值的DOM对象
      inputRef.current.focus();//让文本框获得焦点
      console.log("获得的文本框的内容",inputRef.current.value);
  }
  return ( 
    <div>
        <h3>App组件</h3>
        <input type="text" ref={inputRef} /> 
                          {/* ref属性的功能:将DOM对象赋值给current属性 {current:input DOM对象} */}
        <button onClick={handdleClick}>点击让文本框获得焦点</button>
    </div>
   );
}
 
export default App;

拓展:ref属性--ref对象和ref属性是不同的

  • ref属性的值可以是ref对象,也可以是回调函数*

问题:变更.current属性不会引发组件从新渲染

解决:给ref属性赋值为回调函数,根据DOM变化,改变state引发从新渲染

案例:

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

const App = () => {
  let [height,setHeight] = useState(0);
  let inputRef = useRef()
  let myRef = useCallback(node=>{
    console.log('node',node);
    inputRef.current=node;//将当前ref属性所在DOM赋值给ref对象的current属性
    setHeight(node.getBoundingClientRect().height);
    inputRef.current.innerHTML="我是小甜儿";//修改h1的内容

  },[])
  return ( 
    <div>
        <h3>App组件</h3>
        <h1 ref={myRef}>哈哈哈哈</h1>
        <p>h1的高度是:{Math.ceil(height)}px</p>
    </div>
   );
}

export default App;
/*
getBoundingClientReact() :
Element.getBoundingClientRect() 方法返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置
width-- 元素的实际宽度+padding+border-width
height-- 元素的实际高度+padding+border-height
相对于视口:
top -- 元素顶部到视口顶部的距离
left -- 元素左侧到视口左侧的距离
bottom -- 元素底部到视口顶部的距离 (相当于height+top)
right -- 元素右侧到视口左侧的距离 (相当于width+left)

除了 width 和 height 以外的属性是相对于视图窗口的左上角来计算的。
*/

6. forwardRef():将ref转发给子组件

说明:将ref属性的值转发给子组件,可以在父组件中操作子组件的DOM对象

App组件:

import React,{useRef} from 'react';
import Child from './Child';

const App = () => {
  const inputRef = useRef(null);
  const getFocus = ()=>{
    inputRef.current.focus();
    // console.log(inputRef.current.value)
    
  }
  return ( 
    <div>
        <h3>App组件</h3>
        <button onClick={getFocus}>点击让子组件的文本框获得焦点</button>
        <hr />
        <Child  ref={inputRef}/>
    </div>
   );
}
 
export default App;

Child组件:

import React,{forwardRef} from "react";

//forwardRef 可以将ref传给子组件(让子组件接收父组件穿过来的ref)
// 就相当于 用子组件的ref属性给父组件的ref属性赋了个值 
// 在这个过程中涉及的接收问题 forwardRef就给了子组件一个变量能接收父组件传过来的ref对象
// forwardRef 也就相当于是高阶组件
const Child = forwardRef((props,parentRef) => {//ref随便起(就是用来接收父组件穿过来的ref对象的)
    return ( 
        <div>
            <h3>Child组件</h3>
            <input type="text" ref={parentRef} />
        </div>
     );
})
 
export default Child;

存在问题:  使用forward+useRef获取子函数式组件DOM时,获取到的dom属性暴露的太多了

解决: 使用uesImperativeHandle解决,在子函数式组件中定义父组件需要进行DOM操作,减少获取DOM暴露的属性过多

7.useImperativeHandle可以只暴露DOM对象特定的操作

useImperativeHandle(ref,createHandle)

说明:将父组件传入的ref和useImperativeHandle的第二个参数返回的对象绑定到一起,这样在父组件中调用inputRef,current时,实际上是返回的对象

App组件:

import React,{useRef,useState} from 'react';
import Child from './Child';

const App = () => {
  const inputRef = useRef(null);
  // useImperativeHandle 主要作用:用于减少父组件中通过forward+useRef获取子组件DOM元素暴露的属性过多
  let getFocus = ()=>{
    inputRef.current.focus();
    //这里的focus不再是子组件DOM对象的focus
    //而是 子组件创建的focus事件(这个事件里子组件通过自己的ref对象,操作自己的DOM获得焦点)
    console.log(inputRef.current.value());
  }
  return ( 
    <div>
        <h3>App组件</h3>
        <button onClick={getFocus}>点击让子组件的文本框获得焦点</button>
        <hr />
        <Child  ref={inputRef} />
    </div>
   );
}
 
export default App;

Child组件:

import React,{useRef,forwardRef,useImperativeHandle} from "react";

const Child = forwardRef((props,parentRef) => {//ref随便起(就是用来接收父组件穿过来的ref对象的)
    const inputChildRef = useRef();
    useImperativeHandle(parentRef,()=>({//这个函数赋值给了父组件的ref属性(parentRef)
        focus:()=>{//父组件调用的focus事件 
            inputChildRef.current.focus();//通过子组件自己的ref来操作DOM
        },
        value:(y)=>{
            let childValue = inputChildRef.current.value;
            return childValue;
        },
        
    }))
    return ( 
        <div>
            <h3>Child组件</h3>
            <input type="text" ref={inputChildRef} />
        </div>
     );
})
 
export default Child;

8.useLayoutEffect:类似于useEffect

说明:useLayoutEffect 功能和类组件的componentDidMount 完全等价

useLayoutEffect和useEffect不同点:

  • useEffect 是异步执行的,而useLayoutEffect是同步执行的。

  • useEffect 的执行时机是浏览器完成渲染之后,而 useLayoutEffect 的执行时机是浏览器把内容真正渲染到界面之前,和 componentDidMount 等价。

案例:

import React, { useEffect, useLayoutEffect, useState } from 'react';
import './App.css';

function App() {
  const [state, setState] = useState("hello world")
  // useEffect(() => {  //先看到 hello world  while后刷成world hello的
  //   let i = 0;
  //   while(i <= 1000000000) {
  //     i++;
  //   };
  //   setState("world hello");
  // }, [])
  
  useLayoutEffect(() => { //直接看到的就是word hello 因为是在浏览器把内容真正渲染到界面之前执行的
    let i = 0;
    while (i <= 1000000000) {
      i++;
    };
    setState("world hello");
  }, []);
  /*
    因为 useEffect 是渲染完之后异步执行的,所以会导致 hello world 先被渲染到了屏幕上,
    再变成 world hello,就会出现闪烁现象。而 useLayoutEffect 是渲染之前同步执行的,
    所以会等它执行完再渲染上去,就避免了闪烁现象。
    也就是说我们最好把操作 dom 的相关操作放到 useLayouteEffect 中去,避免导致闪烁。
  */
  return (
    <>
      <div>{state}</div>
    </>
  );
}
export default App;

9.useDebugValue 可用于在 React dev 开发者工具中显示自定义 hook 的标签。了解就行

import React, { useState, useDebugValue } from 'react';
// 自定义 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>
  )
}
export default App;

image.png

大于10的时候开发者工具会显示自定义hook的Mycount值为1

image.png