React Hooks详解

1,770 阅读8分钟

背景

React项目,是由N个大大小小的组件组合而成的,React创建组件的方式一种是类组件(class),一种是函数组件 (function),因为类组件性能消耗比较大,而函数组件是个纯函数,没有自身的状态,生命周期一说,因为对于复杂的状态共享等问题,React在16.8引入除render-props和HOC(高阶组件)之外,另一种解决办法React Hooks

例:该怎样实现组件的逻辑复用

const List = (props) => {
    return (
        {
            props.list.map((item,index) => <div key={index}>{item.name}</div>)
        }
    )
}
const Content = ({isLogin,list=['张三','李四','王五']}) => {
    return (
        <>
            {isLogin ? <List list={list} />:<p>请登录...</p>}
        </>
    )
}
  • render-props

    1. 功能:将一个组件内的state作为props传递给子组件,调用者可以动态决定如何渲

    2. 此问题实现方法:

      class Toggle extends Component{
          constructor(props){
              super(props)
              this.state = {
                  status:props.initialStatus,
                  list:[]
              }
          }
          toggleStatus = () =>{
              let {status} = this.state
              this.setState({
                  status:!status
              },()=>{
                  if(this.state.status){
                      this.getList()
                  }
              })
          }
          getList = () => {
              this.setState({
                  list:['Alex','Bob','Cooper','Diann']
              })
          }
          render(){
              const {status,list} = this.state
              return (
                  <div 
                      status={status} 
                      list={list} 
                      toggleStatus={this.toggleStatus}
                  >
                      {this.props.children}
                  </div>
              )
          }
      }
      ​
      function App(){
          return (
              <Toggle initialStatus={false}>
                  {({status,list,toggleStatus}) => (
                      <>
                          <button onClick={toggleStatus}>{status?'退出登录':'登录'}</button>
                          <Content status={status} list={list} />
                      </>
                  )}
              </Toggle>
          )
      }
      
  • hoc

    1. 定义:hoc(high-order-component)译为高阶组件,其本质是一个高阶函数,接受一个组件组为参数,返回一个新的组件,在这个新的组件中的状态共享,通过pops传给原来的组件
    const Toggle = inititalStatus => WrapperComponent => {
        return calss extends Component {
            constructor(props){
                super(props)
                 this.state = {
                    status:props.inititalStatus,
                    list:[]
                }
            }
             toggleStatus = () =>{
                let {status} = this.state
                this.setState({
                    status:!status
                },()=>{
                    if(this.state.status){
                        this.getList()
                    }
                })
            }
            getList = () => {
                this.setState({
                    list:['Alex','Bob','Cooper','Diann']
                })
            }
            render(){
                const {status,list}
                const newProps = {
                    ...this.props,
                    status:status,
                    list:list,
                    toggleStatus:this.toggleStatus
                }0
                return <WrapperComponent ...newProps />
            }
        }
    }
    ​
    //装饰器模式 只能装饰类
    @Toggle(false)
    class App extends Component{
        const {status,list,toggleStatus} = this.props
        return (
            <>
                <button onClick={toggleStatus}>{status?'退出登录':'登录'}</button>
                <Content status={status} list={list} />
            </>
        )
    }
    ​
    ​
    

    不论是render-props 还是hoc都有各自的优缺点,render-props和hoc都容易引起地域回调 ,而且hoc固定的props可能容易被子组件劫持覆盖,hooks因此油然而生从而完美解决了以上问题

Hooks

  • 定义:hooks意为’钩子‘,react hooks鼓励我们组件尽量写成纯函数,如果需要外部功能状态或副作用就用hooks调用,常见的钩子有以下几种

  • useState

    • 保存组件状态,接受状态的初始值作为参数,返回一个数组,其中数组第一项为一个变量,指向状态的当前值,第二项是一个函数,用来更新状态,约定有set为前缀+状态的变量名为函数名
    • //实现一个简单的计数器Class Interval extends Component {
          constructor(props){
              super(props)
              this.state = {
                  count:1
              }
          }
          add = val => {
              this.setState({
                  count: this.state.count += 1
              })
          }
          reduce = val => {
              this.setState({
                  count: this.state.count -= 1
              })
          }
          render() {
              const {count} = this.state
              return (
                  <>
                      <button onClick={reduce(1)}>-</button>
                      <div>{count}</div>
                      <button onClick={add(1)}>+</button>
                  </>
              )
          }
      }
      ​
      //使用hooks
      import React ,{useState}  from 'react'function Interval(){
          const [count, setCount] = useState(0)
          return (
              <>
                  <button onClick={() => setCount(count-=1)}>-</button>
                  <div>{count}</div>
                  <button onClick={() => setCount(count+=1)}>+</button>
              </>
          )
      }
      
  • useEffect

    • 副作用钩子,类似componentDidMount和compoonentDidUpdate结合体,useEffect第一个参数接收一个函数,用来做一些''副''作用(异步请求,修改外部参数等行为),第二个参数是一个数组,如果数组中的值变化才会触发useEffect第一个参数中的函数,如果第二个参数为空数组,则指挥在组件加载进入dom后才会触发副作用,如果不传第二个参数,则会在每次渲染时候都会触发副作用
    • import React,{useState,useEffect}  from 'React'function Interval(){
          const [count,setCount] = useState(0)
          UseEffct(()=>{
             document.title='当前count值为:'+ count 
          })
          return (
              <>
              /*********/
              </>
          )
      }
      ​
      ​
      
  • useContext

    • 跨组件共享状态狗子,在组件间共享状态,接收一个Context对象,并返回该context的当前值, useContext 的参数必须是 context 对象本身
    • import React, {useState,createContext,useContext} from 'react'
      impoft {Button} from 'antd'
      const typeList = ['primary','default','text','ghost','default','dashed']
      const GlobalContext = createContext({type:'primary'})
      const BtnA = () => {
          const {type} = useContext(GlobalContext)
          return <Button type={type}>按钮A</Button>
      }
      const btnB = ()=> {
          return <Button type={type}>按钮B</Button>
      }
      function BtnGroup(){
          const [type,setType] = useState('primary')
          const toggelType = () => {
              let index = Math.floor(Math.random() *6)  
              setType(typeList[index])
          }
          return (
              <>
                  <Button onClick={toggelType}>切换按钮类型</Button>
                  <GlobalContext.Provider value = {{type}})>
                      <BtnA />
                      <BtnB />
                  </GlobalContext.Provider>
              </>
      ​
      }
      ​
      
  • useReducer

    • 状态管理action钩子,hooks提供了useReducer功能,给function组件提供类似redux功能,引入useReducer后,useReducer接受一个reducer(纯函数)和一个初始state值根据reducer中的action来更新state,然后返回一个数组,第一个值是状态(state),第二个值是用来调用发布时间来更新state(dispatch)

    • import React, {useReducer} from 'react'
      ​
      ​
      //老样子,实现一个计数器 
      function Interval(){
          const [count,dispatch] = useReducer((state,action)=> {
              //眼熟不?
              switch(action.type) {
                      case 'increment':
                          return state + action.num
                      case 'decrement':
                          return state - action.num
                  default:
                      return state
              }
          },0)
          return (
              <div>
                  <button onClick={()=>{dispatch({type:'decrement',num:3})}}>减3</button>
                  <p>{count}</p>
                  <button onClick={()=>dispatch({type:'increment',num:2}) }>加2</button>
              </div>
          )
      
      }
      ​
      export default Interval
      
    • useReducer和useContext careateContext混合使用实现一个局部的redux功能

      //Context.js
      import React ,{createContext}from 'react'
      export const myContext = createContext(null)
      
      //App.js
      import React, {  useReducer } from "react";
      import { myContext } from "./Context/myContext";
      import { listReducer } from "./reducer";
      import List from "./components/List";
      import Form from "./components/Form";
      const { Provider } = myContext;
      function App() {
        const [state, dispatch] = useReducer(listReducer, {
          list: [],
          name: "",
        });
        return (
          <Provider value={{ state, dispatch }}>
            <Form />
            <List />
          </Provider>
        );
      }
      
      export default App;
      
      //reducer.js
      export const listReducer = (state, action) => {
        var { list } = state;
        switch (action.type) {
          case "increment":
            if (action.name.length) {
              list.push(action.name);
            }
            return {
              ...state,
              list,
              name: "",
            };
          case "decrement":
            let index = list.findIndex((item) => item === action.name);
            let newList = list.slice()
            newList.splice(index, 1)
            return {
              ...state,
              list: newList
            };
          case "setName":
            return {
              ...state,
              name: action.name,
            };
          default:
            return state;
        }
      };
      
      //List.js
      import React, { useContext } from "react";
      import { myContext } from "./../../Context/myContext";
      function List() {
        const { state, dispatch } = useContext(myContext);
        return (
          <ul>
            {state.list.map((item, index) => {
              return (
                <li key={index}>
                  <span>
                    {item}
                  </span>
                  {state.list.length ? (
                    <button
                      onClick={() => dispatch({ type: "decrement", name: item })}
                    >
                      删除
                    </button>
                  ) : null}
                </li>
              );
            })}
          </ul>
        );
      }
      export default List;
      
      import React, { useContext } from "react";
      import { myContext } from "./../../Context/myContext";
      function Form() {
        const { state, dispatch } = useContext(myContext);
        const handleInputChange = (el) => {
            dispatch({type:'setName',name:el.target.value})
        }
        return (
          <div className="form-box">
          <span>姓名:</span>
            <input value={state.name} placeholder="请输入姓名" onChange={handleInputChange} />
            <button className="add-btn" onClick={() => dispatch({ type: "increment", name: state.name })}>
              新增
            </button>
          </div>
        );
      }
      export default Form;
      
      
  • useCallback

    • 记忆函数钩子,该函数又两个参数,1.useCallback会固定该函数的引用,只要依赖项没有发生改变,则之中返回之前函数的地址,2数组,记录依赖项

    • 场景重现:

      function Parents(props){
          const handleMthdos = () => {
              //////
          }
          rturn(
              <div>
                  <Child handleMthdos={handleMthdos} />
              </div>
          )
      }
      ​
      

      如上 如果Parent组件的props改变了,那么Parents组件就会重新渲染,Child组件也会重新渲染,即使Child没有使用到props,这是不需要重新渲染的,可以使用useCallback稍稍优化一下:

      import React,{useCallback} from 'react'
      function Parents(props) {
          const handleMethods = useCallback(()=> {
              //////
          },[])
      }
      

      第二个参数传入一个数组,数组中的每一项值或者引用发生改变,useCallback就会重新返回一个新的记忆函数提供给Child组件进行渲染,空数组表示无论什么情况下该函数都不回发生改变

  • useMemo

    • 记忆组件钩子,和useCallback功能类似,不过useCallback不会执行第一个参数函数,而是将它返回给你,而useMemo会执行第一个函数并返回执行 的结果
    • 上面的例子也可改成
      function Parents {
          const count = useMemo(()=> {
              //////
              return ****
          },[])
          retuen (
              <div>
                      <Child count={count}  />
              </div>
          )
      }
      
  • useRef

    • 保存引用值钩子,返回一个可变的ref对象,该对象只有一个current属性,初始值为传入的参数
    • 返回的ref对象在组建的整个生命周期内保持不变
    • 与setState不同的是,useRef更新current的值并不会重新渲染
    • 更新useRef一般写在useEffect里面或者事件监听器里面
    • 类似于class组件的this
      
      function App() {
        const [count, setCount] = useState(0);
        const lastCount = useRef(count);
        console.log(lastCount)
        useEffect(() => {
          lastCount.current = count;
        });
        function handleClick() {
          console.log(count);
          setTimeout(() => {
            alert("You clicked on:" + lastCount.current);
          }, 3000);
        }
        return (
          <div>
            <div>you clicked {count} times</div>
            <button
              onClick={() => {
                setCount(count + 1);
              }}
            >
              click me
            </button>
            <button onClick={handleClick}>show alert</button>
          </div>
        );
      }
      
  • useImperativeHandle

    • 透传ref:通过useImperativeHandle用于让父组件获取子组件内的索引
      import React, { useRef, forwardRef } from "react";
      import Child from "./components/Child";
      const ChildInput = forwardRef(Child);
      function App() {
        const inputRef = useRef(null);
        const handleClick = () => {
          inputRef.current.focus();
        };
        return (
          <div className="app-page">
            <div className="app-title">父组件</div>
            <ChildInput ref={inputRef} />
            <button onClick={handleClick}>聚焦</button>
          </div>
        );
      }
      //Child.js
      import React, { useRef, useImperativeHandle } from "react";
      function Form(props, ref) {
        const inputRef = useRef(null);
        useImperativeHandle(ref, () => inputRef.current);
        const handleFocus = () => {
          console.log("从外界聚焦");
        };
        return (
          <div className="child">
            <div className="title">子组件</div>
            <input ref={inputRef} onFocus={handleFocus} />
          </div>
        );
      }
      
  • useLayoutEffect

    • 其函数签名与useEffect相同,但是调用时机不同。useLayoutEffect会在dom更新之后马上同步调用代码,可能会阻塞页面渲染,而useEffect会在整个页面渲染完才会调用代码
    • 为避免阻塞视觉更新,尽量使用useEffect
      useEffect(()=> {
          moveTo(ref.count,300)                      //使用useEffect会出现滑动的动画过程
      })
      useLayoutEffect(()=> {
          moveTo(ref.count,300)             //使用useLayoutEffect会直接出现在移动后的位置
      })
      const moveTo = (dom,delay,x) => {
          domm.style.transform = 'translate('+ x + 'px)'
          dom.style.transition = 'left 3s'
      }
      return (
              <div>
              <div ref={ref}></div>
          </div>
      )
      

总结

  • hooks可以解决的问题

    • 组件之间的逻辑状态难以复用
    • 大型复杂的组件很难拆分
    • class语法的使用不友好
  • hooks优缺点

    • 优点
      • 没有破坏性改动,完全可选
      • 更容易复用代码
      • 代码量少
      • 容易拆分
    • 缺点
      • 状态不同步,函数的运行是独立的,每个函数都有一份独立的闭包作用域
      • 使用useState无法直接更改数组对象
      • useEffect依赖引用类型可能会触发死循环
      • 不能再循环,条件中或者嵌套函数中调用hooks,必须在react函数顶层使用hooks,因为react需要利用调用顺序来正确更新相应的状态,如果顺序打乱了,可能会出现预料不到的后果
  • useEffect会根据第二个参数来决定会不会处理副作用

    • 不传第二个参数,代表不监听任何参数变化,每次渲染dom都会执行useEffect中的函数
    • 如果第二个参数为空数组,则只会在组件初始化或者组件被销毁时触发
  • useMemo和useCallback

    • 接受参数一样,执行机制一样,只有当第二个参数发生变化时,才会执行第一个参数里面的回调,才会重新计算结果,也起到缓存的作用
    • useMemo计算结果是return回来的值,主要用于缓存计算结果的值,useCallback计算结果是函数,主要用于缓存函数
  • ref,useRef,createRef,forwardRef

    • ref可以拿来直接调用,但是不能在函数式组件中使用的,因为函数式组件是没有实例的
    • createRef每次渲染会返回一个新的引用
    • useRef每次都会返回相同的引用
    • fordwardRef会创建一个react组件,这个组件能够将其接受的ref属性转发到组件数下的另一个组件中,如果子组件不想暴露全部方法,就可以使用useImperativeHandle来自定义暴露给父组件的实例值
  • useEffect和useLayoutEffect

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

    • useEffect是浏览器执行渲染完成之后,useLayoutEffect是浏览器把内容渲染到界面之前和componentDidMount等价,会导致视觉阻塞