说明

文中很多内容都是来自以下两篇文章,我只是加了点自己的理解,如果有什么不好理解的建议直接查看原文Hook 简介React Hooks 详解【近 1W 字】+ 项目实战

Hook 是什么?

Hook 是一个特殊的函数,它可以让你“钩入” React 的特性。例如,useState 是允许你在 React 函数组件中添加 state 的 Hook。稍后我们将学习其他 Hook。

什么时候我会用 Hook?

如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其转化为 class。现在你可以在现有的函数组件中使用 Hook。

Hook 使用规则

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。
  • 自定义的 Hook 中调用

为什么要使用Hook

使组件管理变得简单使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。 通常一个包含副作用的组件,我们会在componentDidMount 中获取初始化数据,当依赖发生改变之后在componentDidUpdate 中更新数据,当组件要销毁时在componentWillUnmount 中清除。但是紧密联系的3个阶段,我们使用class组件,却要放在3个不同的声明周期函数里面去做,而且componentDidMount和componentDidUpdate往往会有相同的逻辑,但是现在使用hooks,我们可以在同一个地方去完成这些事情,变得更简洁清晰。

使组件间逻辑复用变得简单在Hooks出现之前,组件之间共享状态逻辑,我们使用的是render props 和高阶组件,但是现在我们可以通过自定义hooks来实现。

Hooks使用

useState

  • 惰性初始化 state:

如果initialState的值是要通过一些计算得到,那么我们更希望将这个计算过程放在惰性初始化的过程之中

const [state, setState] = useState(() => {  const initialState = someExpensiveComputation(props);  return initialState;
});复制代码

虽然我们可以像如下这样,来达到相同的效果,但是someExpensiveComputation(props)会在组件每一次render时,都会执行一遍(虽然这个值只在useState初始化时使用)。采用下面的写法,someExpensiveComputation函数只会在初始化渲染中被调用,后续渲染时会被忽略

const initialState = someExpensiveComputation(props); 
const [state, setState] = useState(initialState);复制代码

如果新的 state 需要通过使用先前的 state 计算得出,那么可以将回调函数当做参数传递给 setState。该回调函数将接收先前的 state,并返回一个更新后的值。

setNumber(number=>number+1);复制代码

注:Hook 内部使用 Object.is 来比较新/旧 state 是否相等

useEffect

默认情况下,它在第一次渲染之后和每次更新之后都会执行。 如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

componentDidMounted

useEffect(() => void, []);复制代码

componentWillUnmount

useEffect(() => fn, []);复制代码

componentDidUpdate

useEffect(() => {
  
}, [dev1]);复制代码

由于useEffect第一个参数要么返回void要么返回一个回调函数,所以当我们想在useEffect中使用async/await时,需要如下:

useEffect(() => {  const fetchData = async () => {const result = await axios(      'https://hn.algolia.com/api/v1/search?query=redux',
    );
    setData(result.data);
  };

  fetchData();
}, []);复制代码

useReducer

当组件同时使用多个useState方法时,需要一个一个的声明。状态多了,就一大溜的声明。比如:

const Avatar = ({ user, setUser }) => { const [user, setUser] = useState("崔然"); const [age, setAge] = useState("18"); const [gender, setGender] = useState("女"); const [city, setCity] = useState("北京"); // more ...};复制代码

我们可以通过使用useReducer来解决这个问题。useReducer实际是useState 的一个变种,解决了上述多个状态,需要多次使用 useState 的问题。

useReducer使用

const initialState = {number: 0};function reducer(state, action) {  switch (action.type) {case 'increment':      return {number: state.number + 1};case 'decrement':      return {number: state.number - 1};default:      throw new Error();
  }
}function Counter(){const [state, dispatch] = useReducer(reducer,initialState);    return (<>  Count: {state.number}          <button onClick={() => dispatch({type: 'increment'})}>+</button>  <button onClick={() => dispatch({type: 'decrement'})}>-</button></>)
}复制代码

useReducer内部实现

function useReducer(reducer, initialState) {  const [state, setState] = useState(initialState);  function dispatch(action) {const nextState = reducer(state, action);
    setState(nextState);
  }  return [state, dispatch];
}复制代码

React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch。

useContext

Context主要应用场景在于很多不同层级的组件需要访问同样一些的数据,例如theme,userInfo等等

当我们在使用react,有时候会需要使用全局状态用来解决状态跨层级传递的问题。之前我们可以通过react的context来实现。在Hooks中全局状态还是利用React 提供的Context上下文来实现跨层级数据传递,但是在全局中的状态是比较多的,我们这个时候就使用useReducer来进行状态的管理更新。我们把reducer中的state和dispatch通过Provider的value值传递下去。那么在每一个使用的customer中都可以拿到state和更改state的dispatch方法。useReducer是一个状态管理的实现,而useContext用来解决跨组件跨层级的问题 所以两个可以配合使用

const CounterContext = React.createContext();function SubCounter(){const {state, dispatch} = useContext(CounterContext);return (<><p>{state.number}</p><button onClick={()=>dispatch({type:'ADD'})}>+</button></>)
}function Counter(){const [state, dispatch] = useReducer((reducer), initialState, ()=>({number:initialState}));return (<CounterContext.Provider value={{state, dispatch}}><SubCounter/></CounterContext.Provider>)
}

ReactDOM.render(<Counter  />, document.getElementById('root'));复制代码

当组件上层最近的 <CounterContext.Provider>的value更新时,该Hook会触发重渲染,并使用最新传递给CounterContext Provider的Context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

  • useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
  • useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context
  • context实际上就是发布订阅的原理

useCallback userMemo memo

当我们的父组件因为自身状态改变而重新渲染时,会带动子组件的重新渲染,及时此时子组件的状态并没有发生改变,为了解决子组件的非必要渲染,我们使用memo函数来包装子组件,这样当子组件的props没有发生改变时,及时父组件重新渲染了,子组件也不会重新渲染。

在下面这个demo中,当我们每次点击加1按钮的时候,子组件都会重新渲染,因为父组件重新渲染了。但是实际上子组件的重新渲染是没有必要的。

import React, { useState } from 'react';const Child = (props) => {console.log('子组件?')return(<div>我是一个子组件</div>);
}const Page = (props) => {const [count, setCount] = useState(0);return (<><button onClick={(e) => { setCount(count+1) }}>加1</button><p>count:{count}</p><Child /></>)
}export default Page;复制代码

为了解决上述问题,使用memo包装子组件。

import React, { useState, memo } from 'react';const Child = memo((props) => {console.log('子组件?')return(<div>我是一个子组件</div>);
});复制代码

当我们使用React提供的memo高阶函数包装Child组件,此时父组件重新渲染时,子组件不会跟着重新渲染

当我们的子组件中有引用类型的props时

const Page = (props) => {const [count, setCount] = useState(0);const [name, setName] = useState('Child组件');return (<><button onClick={(e) => { setCount(count+1) }}>加1</button><p>count:{count}</p><Child name={name} onClick={(newName) => setName(newName)}/></>)
}复制代码

这里我们每点击一次加1按钮,子组件都会执行,这是因为每次加1,父组件肯定是要重新render的,父组件重新render,此时即使使用了memo,但是由于Child组件的onClick属性的值是一个内联引用类型的值,而且每次父组件渲染时,这个引用值肯定会发生了变化的,所以子组件肯定也是会更新的。这时我们需要引入useCallback Hooks函数来解决这个问题。

<Child name={name} onClick={useCallback((newName) => setName(newName), [deps])}/>复制代码

该回调函数仅在某个依赖项改变时才会更新。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

同样如果props是一个普通的引用型变量,当父组件重新渲染的时候,子组件也会重新渲染。

<ChildMemo name={{ name}} />复制代码

这个时候就要用到useMemo Hooks来解决这个问题。使用useMemo,返回一个和原本一样的对象,第二个参数是依赖项,当name发生改变的时候,才产生一个新的对象

<Child name={useMemo(()=>({ name }), [name])}/>复制代码

useCallback使用场景

  • 在组件内部,那些会成为其他useEffect依赖项的方法,建议用 useCallback 包裹,或者直接编写在引用它的useEffect中  这种情况通常出现在reset等等这些函数,可能还有多个地方调用这个函数。

  • 如果你的function会作为props传递给子组件,请一定要使用 useCallback 包裹,对于子组件来说,如果每次render都会导致你传递的函数发生变化,可能会对它造成非常大的困扰。同时也不利于react做渲染优化。

useMemo使用场景

  • 有些计算开销很大,我们就需要「记住」它的返回值,避免每次render都去重新计算。
  • 由于值的引用发生变化,导致下游组件重新渲染,我们也需要「记住」这个值。

useRef

useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的ref 对象都是同一个(使用 React.createRef ,每次重新渲染组件都会重新创建 ref)。从概念上讲,你可以认为 refs 就像是一个 class 的实例变量。

useRef不仅仅是用来管理DOM ref的它可以存放任何变量,更改.current属性不会导致重新渲染。

使用场景:

  • 我希望一个变量在这个组件中,即使组建重新渲染,这个值也不会变。
  • 保存子组件提供的某一个值(可能是一个DOM元素,也可能就是一个普通对象)
import React, { useState, useEffect, useRef } from 'react';import ReactDOM from 'react-dom’;

function Child() {
    const inputRef = useRef();
    
    function getFocus() {
        inputRef.current.focus();
    }
    return (
        <>
            <input type="text" ref={inputRef} />
            <button onClick={getFocus}>获得焦点</button>
        </>
    )
}

ReactDOM.render(<Child />, document.getElementById('root'));复制代码
function Timer() {  const intervalRef = useRef();

  useEffect(() => {const id = setInterval(() => {      // ...});
    intervalRef.current = id;return () => {      clearInterval(intervalRef.current);
    };
  });  // ...}复制代码

forwardRef

因为函数组件没有实例,所以函数组件无法像类组件一样可以接收ref属性,为了使函数组件能够像类组件一样接受ref属性,我们需要使用forwardRef包装函数组件,使函数组件能够接受ref属性,包装之后的组件不会把ref属性当做props传入。

function Parent() {return (<> // <Child ref={xxx} /> 这样是不行的<Child /><button>+</button></>)
}复制代码

这时就要使用forwardRef  forwardRef可以将父组件中的ref对象转发到子组件中的dom元素上,子组件接受 props 和 ref 作为参数

function Child(props,ref){  return (<input type="text" ref={ref}/>
  )
}
Child = React.forwardRef(Child);function Parent(){  let [number,setNumber] = useState(0); 
  const inputRef = useRef(); //{current:’'}

  function getFocus(){
    inputRef.current.focus();
  }  return (      <><Child ref={inputRef}/><button onClick={()=>setNumber({number:number+1})}>+</button><button onClick={getFocus}>获得焦点</button>  </>
  )
}复制代码

useImperativeHandle

有时候我们希望在父组件中执行,子组件提供的某些方法,在类组件中我们可以通过ref获取子组件,然后执行子组件(子组件也为类组件)中的方法,但是在函数组件中,我们无法实现这一点。因为函数组件中没有this,我们无法获取到函数子组件中的方法。这时我们可以使用useImperativeHandle配合forwardRef使用。

useImperativeHandle这个Hooks会返回一个对象, 该对象会作为父组件 current属性的值

import React,{useState,useEffect,createRef,useRef,forwardRef,useImperativeHandle} from 'react';function Child(props,parentRef){// 子组件内部自己创建 ref let focusRef = useRef();let inputRef = useRef();
    useImperativeHandle(parentRef,()=>(      // 这个函数会返回一个对象  // 该对象会作为父组件 current 属性的值  // 通过这种方式,父组件可以使用操作子组件中的多个 refreturn {
            focusRef,
            inputRef,name:'计数器',focus(){
                focusRef.current.focus();
            },changeText(text){
                inputRef.current.value = text;
            }
        }
    });return (<><input ref={focusRef}/><input ref={inputRef}/></>)
}
Child = forwardRef(Child);function Parent(){  const parentRef = useRef();//{current:''}
  function getFocus(){
    parentRef.current.focus();// 因为子组件中没有定义这个属性,实现了保护,所以这里的代码无效parentRef.current.addNumber(666);
    parentRef.current.changeText('<script>alert(1)</script>');console.log(parentRef.current.name);
  }  return (      <><ForwardChild ref={parentRef}/><button onClick={getFocus}>获得焦点</button>  </>
  )
}复制代码

useLayoutEffect

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新

function LayoutEffect() {const [color, setColor] = useState('red');
    useLayoutEffect(() => {
        alert(color);
    });
    useEffect(() => {console.log('color', color);
    });return (<div><div id="myDiv">颜色:{color}</div><button onClick={() => setColor('red')}>红</button><button onClick={() => setColor('yellow')}>黄</button><button onClick={() => setColor('blue')}>蓝</button></div>);
}复制代码