一文看懂React hook

185 阅读5分钟

v2-f888deb7a91526a1a0ba5e9a75ecc32d_1440w.png

前言

React hook是React16.8版本之后推出的一套全新API,它可以让我们在不编写class组件的情况下适用state以及其他的React特性。

至于为什么要推出这套API呢,那我们就要看看在编写class组件会有哪些问题

  • 复杂组件难以维护

    class语法,控制的精细度最多只能到组件,在开始编写组件时,可能业务逻辑,组件状态相对较少,但随着业务的增加,代码的增多,组件中某个生命周期的方法可能会存在某些方法的逻辑耦合,组件变得越来越复杂、难以维护。

  • this的指向问题

    我们在写class组件的时候,需要操作各种this的指向,使得自己晕头转向,冗余代码也会变多(React内心OS表示这个锅我可不背^_^),其实这并不是React的问题,而是js本身this的问题,如果不使用bind或者箭头函数写法的话,this就是指向undefined(class语法默认是严格模式,而严格模式中的this指向默认是undefined)

React hook就是为了解决这一系列问题,下面就让我们来梳理认识一下React hook

状态hook 【useState】

const [state, setState] = useState(initialState)
  • state是我们设置状态的名称,如果新的state需要老的state的值进行处理,那么setState就需要传递一个回调函数作为参数,而这个回调函数的参数就是state,这样才可以正常执行。
  • setState是我们改变状态的方法,可以在事件或者其他方法中调用它,它会把新的state直接替换老的state。而不是像class组件中this.setState将他们合并在处理。
  • initialState是我们需要传入的唯一默认值.

code.png

副作用操作hook 【useEffect】

useEffect(() => {
    // 执行相关操作,当前相当于componentDidMount
    return () => {
        // 执行相关操作,当前相当于componentWillUnment
    }
}, 
// 根据依赖项执行更新,当前相当于componentWillUnment
[]
)

useEffect将componentDidMount,componentDidUpdate,componentWillUnment这三个生命周期融合在其中

  • 如果不传入array参数,可以理解为默认执行componentDidMount,componentDidUpdate两个生命周期,在组件中任意状态的改变都会执行回调方法
  • 如果array参数为[],那么我们可以理解为只执行componentDidMount生命周期
  • 如果我们在组件销毁的时候想要清除组件中的定时器或者事件监听呢,这时我们就需要在回调函数中执行return操作。
    接着使用我们上述例子,如果我想在进入页面就开始倒计时,而不是使用点击事件才开始倒计时,这时就需要使用useEffect

code1.png

共享状态hook 【useContext】

const context = useContext(MyContext)
  • Context对象即React.createContext的返回值。
  • useContext(MyContext)相当于class组件中的static contextType = MyContext或者<MyContext.Consumer> useContext应用于相对数据简单,但组件层级较深的场景,比如我们看到的一些的网站一键换肤功能就可以使用useContext来实现

code2.png 我们首先在根组件设置light,dark两种主题颜色,然后通过<MyContext.Provider value={theme}>来传递

code3.png 这是子组件

code4.png 这样我们就可以在孙组件或层级更多的组件中获取theme从而来进行换肤操作

状态管理hook 【useReducer】

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

useReducer接受一个reducer函数作为参数,reducer接收state和action两个参数,initialArg为初始数据。当我们有多个state需要一起更新是,就需要考虑使用useReducer了。

下面我们将useState中的例子加以改进,变成了这样

code5.png 在这里是可以正常运行,但是如果随着互相依赖的状态变多,改变state的逻辑变得越来越复杂,useEffect的第二个数组参数就会更复杂,降低可读性的同时,useEffect重新执行实际变的更加难以预料,我们用useReducer来改进下

code6.png 现在组件只需要发出action,而无需知道如何更新状态。也就是将What to doHow to do解耦。彻底解耦的标志就是:useReducer总是返回相同的dispatch函数(发出action的渠道),不管reducer(状态更新的逻辑)如何变化。

性能优化hook 【useCallback】、【useMemo】

这两个hook常常用来做性能优化,会在组件第一次渲染时执行,之后会在其依赖的变量发生改变时再次执行,并返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数

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

在这里,只有依赖的a,b值变化,缓存值才会改变。接着用倒计时的例子,当一个倒计时组件中有一个子组件,如图所示

code7.png 大家可以看到子组件使用的data.step的值一直没有改变,但是父组件更新后,依旧会触发子组件的更新

02.gif

这样就造成了不必要的更新,这时就需要useMemo登场了

code8.png

再看控制台,无论num怎么更新,子组件都不会在触发更新,只有当step变化时,才会触发更新

03.gif

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

useCallback(fn, deps) 相当于 useMemo(() => fn, deps),所以应用是基本相似的。继续上边的例子,如果我们往子组件中传递的是一个方法时

code9.png 子组件也会因为父组件的渲染而触发渲染

04.gif 使用useCallback就可以解决这个问题

code10.png 这样就不会因为父组件的更新渲染导致子组件的渲染了

05.gif

实例hook 【useRef】

const refContainer = useRef(initialValue);

看到useRef,大家首先想到可能就是用它访问dom元素,经常看到的例子,当然这是它最常用到的功能

code11.png 但是我们在看文档时有这样一段话返回的 ref 对象在组件的整个生命周期内保持不变,我们是不是可以做点什么呢? 还用我们倒计时的例子

code12.png 我们想在倒计时到55s的时候清除定时器。

06.gif

可是倒计时还是在走,这时就可以用到useRef来解决这个问题

code13.png 这样就可以愉快的清除定时器了^_^