React学习系列之Hooks那些事

190 阅读6分钟

概述

react的hooks功能发布于v16.8版本,其目的在于解决状态共享的问题,也就是有状态组件的复用问题,在此之前的两种解决方案分别是渲染属性(Render Props)和高阶组价。

hooks使用的规则:

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中。)

useState

import React, { useState } from 'react';

function Example() {
 // 声明一个叫 "count" 的 state 变量
 const [count, setCount] = useState(0);

 return (
   <div>
     <p>You clicked {count} times</p>
     <button onClick={() => setCount(count + 1)}>
       Click me
     </button>
   </div>
 );
}

等价的class写法(方括号是es6中的数组解构的语法)

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

 render() {
   return (
     <div>
       <p>You clicked {this.state.count} times</p>
       <button onClick={() => this.setState({ count: this.state.count + 1 })}>
         Click me
       </button>
     </div>
   );
 }
}

当存在多个useState时,react是按照执行顺序识别对应state的,比如这种情况:

const [fruit, setFruit] = useState('apple');
const [animal, setAnimal] = useState('cat');
const [color, setColor] = useState('green');

所以官方文档也有说明,hooks不能存在条件判断语句中。

useEffect

该hook官方解释作用是可以让你在函数组件中执行副作用操作,而且能够实现一定的性能优化。

关于副作用网上看到一个很简单易懂的解释:

  • 纯函数(Pure function):给一个 function 相同的参数,永远会返回相同的值,并且没有副作用;这个概念拿到 React 中,就是给一个 Pure component 相同的 props, 永远渲染出相同的视图,并且没有其他的副作用;纯组件的好处是,容易监测数据变化、容易测试、提高渲染性能等;
  • 副作用(Side Effect)是指一个 function 做了和本身运算返回值无关的事,比如:修改了全局变量、修改了传入的参数、甚至是 console.log(),所以 ajax 操作,修改 dom 都是算作副作用的;
import React, { useState, useEffect } from 'react';

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

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
    return ()=>{
        console.log('xiezaile...')
    }
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

我理解的该hook的作用就是相当于糅合了声明周期中的挂载、卸载、更新的钩子函数。

首先,第一个参数是个回调,默认(没有第二个参数时)在页面首次载入和页面有更新时调用,return的返回值也是个函数,在页面卸载时调用。

第二个参数我理解的是触发页面更新的条件变量,当它是空数组时,点击按钮count变化,但是第一个参数的回调函数不会执行,即使是props或state变化,第一个参数的回调函数也不会执行。但如果第二个参数是[count],表示count变化时,第一个回调函数也会调用。

由于第一个参数的回调函数内部是异步的,因此用来修改DOM不能达到及时改变的效果

useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
    return ()=>{
        console.log('xiezaile...')
    }
  },[count]);

注:有些副作用需要清除时就带上返回函数,不需要处理时就不需要返回。

useContext

该hook主要用于上下文父子组件(非直系父子)传递数据。

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值(只要是对<Context.Consumer>写法的替代)。

import React, { useState , createContext, useContext } from 'react';
const countContext = createContext(); //创建上下文

function Counter(){ //接收数据的组件
    const count = useContext(CountContext)  //就相当于<countContext.Consumer></countContext.Consumer>得到count 
    return (<h2>{count}</h2>)
}

function Example(){
    const [count,setCount] = useState(0)
    
    return (
        <div>
            <p>this is {count}</p>
            <button onClick={()=>{setCount(count+1)}}>增加</button>
            <countContext.Provider value={count}>  //当数据更新的时候触发渲染
                <Counter></Counter>
            </countContext.Provider>
        </div>
    )
}
export default Example;

useReducer

当state内容较为复杂时,可以作为它的替代方案,跟redux中的reducer处理部分相似。

官网用例:

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();
  }
}
function Counter() {
    const [action,dispatch] = useReducer(reducer,{count: 0});
     return (
        <>
          Count: {state.count}
          <button onClick={() => dispatch({type: 'increment'})}>+</button>
          <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </>
      );
}

useReducer结合useContext实现redux功能

import React, {useReducer,useContext,createContext} from 'react'
const CountContext = createContext()
function reducer(state,action){
    switch(action.type){
        case 'Asc':
            return {count: state.count+1}
        case 'Dsc':
            return {count: state.count-1}
        default:
            new Error()
    }
}
function GrandChild(){
    console.log(useContext(CountContext),'cccccccc')
    const {state,dispatch} = useContext(CountContext)
    return (
        <div>
            {/* <div>{num}</div> */}
            <div>{state.count}</div>
            <button onClick={()=>dispatch({type: 'Asc'})}>adddddddd</button>
            <button onClick={()=>dispatch({type: 'Dsc'})}>descccccc</button>
        </div>
    )
}
function Child(props){
    return (
        <div>
            {props.children}
        </div>
    )
}
export default function App(){
    const [state,dispatch] = useReducer(reducer,0,x=>({count:x})) 
        return (
            <CountContext.Provider value={{state,dispatch}}>
                <Child>
                    <GrandChild></GrandChild>
                </Child>
            </CountContext.Provider>
        )
}

useMemo

官网用例

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

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

传入 useMemo 的函数会在渲染期间执行,不要存在副作用,应把副作用的处理放在useEffect中

再来一个用例

//父组件
function Parent() {
  const [name, setName] = useState('ali')
  const [age,setAge] = useState('22')
  return (
      <>
        <button onClick={() => setName(new Date().getTime())}>name</button>
        <button onClick={() => setAge(new Date().getTime())}>age</button>
        <Button name={name}>{age}</Button>
      </>
  )
}
//子组件
function Button({ name }) {
  function changeName(name) {
    console.log('11')
    return name + '改变name的方法'
  }
  //const otherName =  changeName(name)  //在点击父组件按钮改变name或age时都会调用该函数
  const otherName =  useMemo(()=>changeName(name),[name]) //只在name改变时才调用该函数
  return (
      <>
        <div>{otherName}</div>
      </>
  )
}

useCallback

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
所有依赖本地状态或props来创建函数,需要使用到缓存函数的地方,都是useCallback的应用场景。

useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数。
这里有篇文章关于useMemo和useCallback的异同讲解的通俗易懂。

useRef

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

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

注:当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

useImperativeHandle

不能在函数组件上使用ref属性,因为他们没有实例,但是使用useImperativeHandle,实现函数式组件使用ref,根本上是因为强制性的公开给父组件的实例值。 与 forwardRef 一起使用,官网实例:

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

自定义Hook

有几点需要注意的:

  • 命名必须以use开头,不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。
  • 两个组件中使用相同的hook不会共享state,每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全独立的

这里有一些hooks相关的学习资源:

30分钟精通React Hooks

React Hooks 详解 【近 1W 字】+ 项目实战

技术胖 React Hooks免费视频教程

前端面试专题 —— React Hooks 完全解析

本文为个人学习总结,如有错误欢迎指正和补充。