学完Hooks,你会爱上React

1,138 阅读15分钟

1.Hook的优势

Hooks的概念是React 16.8的新增特性,他可以让我们在不编写class的组件的情况下还能使用state以及其他的一些React特性。Vue3的Composition API也采用了Hook的写法来填补Options API 不够灵活的开发方式。

首先我们可以先来回顾一下class组件相对于函数时组件有什么优势:

  • 可以定义自己的state,用来保存自己的内部状态,但是函数式组件不可以,函数时组件在每次调用时会产生新的临时变量
  • class组件有自己的生命周期,我们可以在生命周期中完成我们想要操作的业务,比如DOM操作或者网络请求等。但是在没有学习hooks之前,函数式组件是没有生命周期这个概念的
  • class组件可以在状态改变时只会重新执行render函数以及我们相应的生命周期函数。而函数式组件在重新渲染时,整个函数都会被执行

因此在hooks出现之前,我们开发中常用的其实是类组件,但是class也有不少问题,我们可以列举出来:

  • 复杂组件的难以理解
  • 难以理解的class中的this指向
  • 组件复用很难

而Hook的出现就解决了这些问题。

简单总结一下hooks:

  1. 它可以是我们在不编写class组件的情况下来进行使用state以及生命周期函数等
  2. 可以延伸出非常多的用法来让我们前面所提到的问题得以解决

1.1 Hook与类组件进行对比

我们可以通过一个计数器案例来对比一些class组件和结合hooks的函数式组件。

class组件实现:

import React, { PureComponent } from 'react'

export default class ClassCounter extends PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      counter: 0
    }
  }
  render() {
    return (
      <div>
        <p>{this.state.counter}</p>
        <button onClick={() => { this.add() }}>+</button>
        <button onClick={() => { this.sub() }}>-</button>
        <hr />
      </div>
    )
  }
  add() {
    this.setState({
      counter:this.state.counter+1
    })
  }
  sub() {
    this.setState({
      counter:this.state.counter-1
    })
  }
}

函数时组件实现:

	import React, { memo, useState } from 'react'

export default memo(function HooksCounter() {
  let [counter, setcounter] = useState(10);
  return (
    <div>	
      Hook实现计数器功能
      <p>{counter}</p>
      <button onClick={e => setcounter(counter + 1)}>+</button>
      <button onClick={e => setcounter(counter - 1)}>-</button>
      <hr />
    </div>
  )

})

我们会发现上面的代码差异非常大,函数时组件结合hook的代码明显非常简单,并且不用考虑this指向的问题。

我们可以分析一下上面的代码,首先使用usestate函数是一个由react对象导出的hook,一般有一个参数是用来初始化值的,也可以传入一个function。返回值是一个数组,第一个元素是当前状态的值,第二个元素是用来设置当前状态的函数。可以看出我们上面的代码是使用es6解构赋值的语法进行编写的从而取出两个元素。接着在button的点击事件中调用setCounter从而设置新值。

1.2 总结

Hooks实际上就是JavaScript的函数,这个函数可以帮助你钩入React的state以及生命周期等特性。

使用时他们会有两个额外的规定:

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

2. State Hook

State Hook的api就是usestate。

usestate会帮助我们定义一个state变量,usestate是一种新的方法,它与class里面的this.state提供的功能完全相同,一般来说,在函数退出变量就会消失。

usestate接收一个唯一的参数,在组件第一次被调用的时候用来做初始值。返回一个数组,可以通过解构的方式来进行拿出我们的状态以及设置状态的函数。

当然我们也可以在一个组件中定义多组变量和复杂变量(数组、对象)

这是我们分别操作数字、数组、对象的案例:

import React, { memo, useState } from 'react'

export default memo(function HooksCounter() {
  let [counter, setcounter] = useState(()=>10);
  const [student, setstudent] = useState(['张三', '李四', '王五'])
  const [studentinfo, setstudentinfo] = useState([{ name: '张三', age: 12 }, { name: '李四', age: 5 }, { name: '王五', age: 10 }])
  function setbtn() {
    setstudent([...student, '123'])
  }
  function addage(index) {
    let newinfoarray=[...studentinfo]
    newinfoarray[index].age++
    setstudentinfo(newinfoarray)
  }
  return (
    <div>
      <p>{counter}</p>
      <button onClick={e => setcounter(counter + 1)}>+</button>
      <button onClick={e => setcounter(counter - 1)}>-</button>
      <hr />
      <ul>
        {
          student.map((index) => {
            return (
              <li key={index}>{index}</li>
            )
          })
        }
      </ul>
      <button onClick={e => setbtn()}>加姓名</button>
      <hr />

      <ul>
        {
          studentinfo.map((item,index) => {
            return (
              <li key={item.name}>
                <p>{item.name}</p>
                <p>{item.age}</p>
                <button onClick={e=>addage(index)}>age+1</button>
              </li>
            )
          })
        }
      </ul>
    </div>
  )
})

3. Effect Hook

这个hook主要是类似与处理网路请求相关的功能。实际上类似于网络请求、手动更新DOM、事件监听这些都是React更新DOM的一些副作用 ,因此对于完成这些功能的Hooks被称之为Effect Hook。

Effect基本使用:

假设有如下需求:页面的title总是显示counter的数字

import React, { memo, useEffect, useState } from 'react'

export default memo(function UseHooksChange() {
  let [counter,setCounnter]=useState(0);
  useEffect(() => {
    document.title=counter
  })
  return (
    <div>
       <div>
        <p>{counter}</p>
        <button onClick={()=>{setCounnter(counter++)}}>+1</button>
      </div>
    </div>
  )
})

上面就是我们使用useEffect这个api来完成这个需求,但是如果我们使用的是class组件的话,我们就需要同时在componentDidMountcomponentDidUpdate中同时进行设置才可以完成这个需求。

我们可以解析一下这个函数:

  • 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;
  • useEffect要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数
  • 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数

3.1 effect与componentWillUnmount

在class组件的编写过程中,某些副作用的代码,我们需要在componentWillUnmount中进行清除:

比如我们之前的事件总线或Redux中手动调用subscribe,都需要在componentWillUnmount有对应的取消订阅;

Effect Hook通过什么方式来模拟componentWillUnmount呢?

useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B

类型如下:

type EffectCallback = () => (void | (() => void | undefined));

使用方式如下:

 useEffect(() => {
    document.title = `当前计数: ${count}`;
    console.log("每次DOM更新时会回调");

    return () => {
      console.log("DOM被移除时会回调");
    }
  })

为什么要在effect中返回一个函数?

这是effect可选的清除机制,每个effect都可以返回一个清除函数;如此可以将添加和移除订阅的逻辑放在一起,他们都属于effect的一部分

React何时清除effect?

  • React 会在组件更新和卸载的时候执行清除操作;
  • 正如之前学到的,effect 在每次渲染的时候都会执行;

3.2 使用多个effect

使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题:

比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中;

使用Effect Hook,我们可以将它们分离到不同的useEffect中:

  useEffect(() => {
    console.log('修改counter相关的dom')
  }, [counter])
  useEffect(() => {
    document.title = counter
    console.log('订阅事件');
  }, [counter])
  useEffect(() => {
    document.title = counter
    console.log('网络请求');
  }, [])

Hook 允许我们按照代码的用途分离它们, 而不是像生命周期函数那样:

  • React 将按照 effect 声明的顺序依次调用组件中的每一个 effect;

3.3 effect的性能优化

默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:

  • 某些代码我们只是希望执行一次即可,类似于componentDidMount和componentWillUnmount中完成的事情;(比如网络请求、订阅和取消订阅);
  • 另外,多次执行也会导致一定的性能问题;

为了解决这个问题,其实useEffect实际上有两个参数:

  • 参数一:执行的回调函数;
  • 参数二:该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)

我们可以看一下一下这个案例:

在这个案例中,我们修改show的值,是不会让useEffect重新被执行的

因为我们规定了第二个参数为count,因此这个函数只有子count改变的时候才会执行,所以我们点击切换按钮并不会引发这个函数的执行。

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

export default function EffectPerformance() {
  const [count, setCount] = useState(0);
  const [show, setShow] = useState(true);

  useEffect(() => {
    console.log("修改DOM");
  }, [count])

  return (
    <div>
      <h2>当前计数: {count}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
      <button onClick={e => setShow(!show)}>切换</button>
    </div>
  )
}

如果我们一个函数不像希望依赖任何内容时,也就是说只执行一次的话,那么我们可以为其传递一个空的数组[]:

那么这里的两个回调函数分别对应的就是componentDidMount和componentWillUnmount生命周期函数了

useEffect(() => {
  console.log("监听事件");

  return () => {
    console.log("取消监听");
  }
}, [])

4. Context Hook

在之前的开发中,我们要在组件中使用共享的Context有两种方式:

  • 类组件可以通过 类名.contextType = MyContext方式,在类中获取context;
  • 多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context;

但是多个Context共享时的方式会存在大量的嵌套:

  • Context Hook允许我们通过Hook来直接获取某个Context的值;
const value = useContext(MyContext);

在App.js中使用Context:

import React, { createContext } from 'react';

import ContextHook from './04_useContext使用/01_ContextHook';

export const UserContext = createContext();
export const ThemeContext = createContext();

export default function App() {
  return (
    <div>
      <UserContext.Provider value={{name: "why", age: 18}}>
        <ThemeContext.Provider value={{color: "red", fontSize: "20px"}}>
          <ContextHook/>
        </ThemeContext.Provider>
      </UserContext.Provider>
    </div>
  )
}

在对应的函数式组件中使用Context Hook:

import React, { useContext } from 'react'
import { UserContext, ThemeContext } from '../App'

export default function ContextHook() {
  const user = useContext(UserContext);
  const theme = useContext(ThemeContext);
  console.log(user);
  console.log(theme);

  return (
    <div>
      ContextHook
    </div>
  )
}

注意事项:当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 context value 值。

5. Reducer Hook

这里的reducer并不是redux中的reducer。

useReducer仅仅是useState的一种替代方案:

  • 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分;
  • 或者这次修改的state需要依赖之前的state时,也可以使用

单独创建一个reducer/counter.js文件:

export function counterReducer(state, action) {
  switch(action.type) {
    case "increment":
      return {...state, counter: state.counter + 1}
    case "decrement":
      return {...state, counter: state.counter - 1}
    default:
      return state;
  }
}

home.js

import React, { useReducer } from 'react'
import { counterReducer } from '../reducer/counter'

export default function Home() {
  const [state, dispatch] = useReducer(counterReducer, {counter: 100});

  return (
    <div>
      <h2>当前计数: {state.counter}</h2>
      <button onClick={e => dispatch({type: "increment"})}>+1</button>
      <button onClick={e => dispatch({type: "decrement"})}>-1</button>
    </div>
  )
}

注意:useReducer只是useState的一种替代品,并不能替代Redux。并不能做到数据共享

7.Usecallback

useCallback实际的目的是为了进行性能的优化。

如何进行性能的优化呢?

  • useCallback会返回一个函数的 memoized(记忆的) 值;
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;

实际上在没有父类向子类传递这种操作时,是用不到我们的useCallback的。就像以下的一个栗子:

import React, { memo, useCallback, useState } from 'react'


let AoButtoon=memo((props) => {
  console.log('渲染了Ao组件', props.flag);
  return (
    <div>
      <button onClick={props.increment}>我是一个BUTTON组件</button>
    </div>
  )
})


export default function UseRightCallBack() {
  const [counter, setcounter] = useState(0)
  const [isshow, setisshow] = useState(true)
  let increment1 = () => {
    console.log('执行点击事件+1');
    setcounter(counter + 1)
  }
  let increment2 = useCallback(
    () => {
      console.log('执行callback');
      setcounter(counter + 1)
    },
    [counter],
  )
  return (
    <div>
      <p>{counter}</p>
      <AoButtoon flag='普通' increment={increment1}></AoButtoon>
      <AoButtoon flag='回调的方式' increment={increment2}></AoButtoon>
      <button onClick={() => { setisshow(!isshow) }}>隐藏显示button组件</button>
    </div>
  )
}

在发生点击时,我们会发现接受increment2的子组件不会重新渲染,但是接受increment1的子组件会重新渲染;

usecallback最主要用于性能渲染的地方应该是和memo结合起来,决定子组件是否需要重新渲染;

8.UseMemo

seMemo实际的目的也是为了进行性能的优化。

如何进行性能的优化呢?

  • useMemo返回的也是一个 memoized(记忆的) 值;
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;

我们可以来看一个栗子:

  • 无论我们点击了是 +1还是 切换 案例都会重新计算一次;
  • 事实上,我们只是希望在count发生变化时重新计算;
import React, { useState, useMemo } from 'react';

function calcNum(count) {
  let total = 0;
  for (let i = 0; i < count; i++) {
    total += i;
  }
  console.log("计算一遍");
  return total
}

export default function MemoHookDemo() {
  const [count, setCount] = useState(10);
  const [isLogin, setIsLogin] = useState(true);

  const total = calcNum(count);

  return (
    <div>
      <h2>数字和: {total}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
      {isLogin && <h2>Coderwhy</h2>}
      <button onClick={e => setIsLogin(!isLogin)}>切换</button>
    </div>
  )
}

这个时候,我们可以使用useMemo来进行性能的优化:

import React, { useState, useMemo } from 'react';

function calcNum(count) {
  let total = 0;
  for (let i = 0; i < count; i++) {
    total += i;
  }
  console.log("计算一遍");
  return total
}
export default function MemoHookDemo() {
  const [count, setCount] = useState(10);
  const [isLogin, setIsLogin] = useState(true);

  const total = useMemo(() => {
    return calcNum(count);
  }, [count]);

  return (
    <div>
      <h2>数字和: {total}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
      {isLogin && <h2>Coderwhy</h2>}
      <button onClick={e => setIsLogin(!isLogin)}>切换</button>
    </div>
  )
}

不仅如此我们的useMemo也可以用于子组件的性能优化:也就是说它不仅可以返回一个基本类型的数据,也可以返回一个引用 类型的数据。

9.useRef

useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变。

最常用的ref是两种用法:

  • 用法一:引入DOM(或者组件,但是需要是class组件)元素;
  • 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变;

**用法一:**操作dom

import React, { useRef } from 'react';

export default function RefHookDemo() {
  const inputRef = useRef();
  const titleRef = useRef();

  const handleOperating = () => {
    titleRef.current.innerHTML = "我是coderwhy";
    inputRef.current.focus();
  }

  return (
    <div>
      <input type="text" ref={inputRef}/>
      <h2 ref={titleRef}>默认内容</h2>

      <button onClick={e => handleOperating()}>操作</button>
    </div>
  )
}

用法二:

  • useRef可以想象成在ref对象中保存了一个.current的可变盒子;
  • useRef在组件重新渲染时,返回的依然是之前的ref对象,但是current是可以修改的;

下面代码是显示我们改变counter前和改变dom后的数值:大概流程就是在每一次生命周期函数执行前也就是渲染时,此时的countRef.current就是我们上一次的值,而count就是我们当前的值,而在每次生命周期初期对current设置为当前值,以此类推,有点类似于链表的解构。

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

let preValue = 0;

export default function RefHookDemo02() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  return (
    <div>
      <h2>前一次的值: {countRef.current}</h2>
      <h2>这一次的值: {count}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
    </div>
  )
}

10.useImperativeHandle

这个hook的作用实际上就是为了解决一个问题,就是,当我们父组件获取子组件的DOM时,防止我们父组件直接对DOM进行操作,而是将操作子组件DOM的一些方法写在了子元素本身,然后将这些操作事件的函数暴露给父组件,从而解决父组件直接操作子组件的问题。实际上也是做了一层优化。

我们先来回顾一下ref和forwardRef结合使用:

  • 通过forwardRef可以将ref转发到子组件;
  • 子组件拿到父组件中创建的ref,绑定到自己的某一个元素中;
import React, { useRef, forwardRef } from 'react';

const HYInput = forwardRef(function (props, ref) {
  return <input type="text" ref={ref}/>
})

export default function ForwardDemo() {
  const inputRef = useRef();

  return (
    <div>
      <HYInput ref={inputRef}/>
      <button onClick={e => inputRef.current.focus()}>聚焦</button>
    </div>
  )
}

上面的做法本身没有什么问题,但是我们是将子组件的DOM直接暴露给了父组件:

  • 直接暴露给父组件带来的问题是某些情况的不可控;
  • 父组件可以拿到DOM后进行任意的操作;
  • 但是,事实上在上面的案例中,我们只是希望父组件可以操作的focus,其他并不希望它随意操作;

通过useImperativeHandle可以只暴露固定的操作:

  • 通过useImperativeHandle的Hook,将传入的refuseImperativeHandle第二个参数返回的对象绑定到了一起;
  • 所以在父组件中,使用 inputRef.current时,实际上使用的是返回的对象
  • 比如我调用了 focus函数,甚至可以调用 printHello函数
import React, { useRef, forwardRef, useImperativeHandle } from 'react';

const HYInput = forwardRef(function (props, ref) {
  // 创建组件内部的ref
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    printHello: () => {
      console.log("Hello World")
    }
  }))

  // 这里绑定的是组件内部的inputRef
  return <input type="text" ref={inputRef}/>
})

export default function ImperativeHandleHookForwardDemo() {
  const inputRef = useRef();

  return (
    <div>
      <HYInput ref={inputRef}/>
      <button onClick={e => inputRef.current.focus()}>聚焦</button>
      <button onClick={e => inputRef.current.printHello()}>Hello World</button>
    </div>
  )
}

11.useLayoutEffect

useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:

  • useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
  • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;

如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect。

我们来看下面的一段代码:

  • 这段代码在开发中会发生闪烁的现象;
  • 因为我们先将count设置为了0,那么DOM会被更新,并且会执行一次useEffect中的回调函数;
  • 在useEffect中我们发现count为0,又执行一次setCount操作,那么DOM会再次被更新,并且useEffect又会被执行一次;
import React, { useEffect, useState, useLayoutEffect } from 'react';

export default function EffectHookDemo() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    if (count === 0) {
      setCount(Math.random()*200)
    }
  }, [count]);

  return (
    <div>
      <h2>当前数字: {count}</h2>
      <button onClick={e => setCount(0)}>随机数</button>
    </div>
  )
}

事实上,我们上面的操作的目的是在count被设置为0时,随机另外一个数字:

  • 如果我们使用useLayoutEffect,那么会等到useLayoutEffect代码执行完毕后,再进行DOM的更新;
	useLayoutEffect(() => {
    if (count === 0) {
      setCount(Math.random()*200)
    }
  }, [count]);

图片useEffect和useLayoutEffect对比

12.自定义hooks

自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性。

我们通常将一些复用性较高的hook操作可以按照其特性封装为不同的函数,只是它与普通函数唯一的区别就是我们这里的函数名要use,剩下的和我们普通的函数时没有区别的。

我们这里有一个案例:

获取context:

因此我们可以自定义hook函数:

import { useContext } from "react";
import { Token, UserInfo } from "../App";

function useUserContent() {
  let a=useContext(UserInfo)
  let b=useContext(Token)
  return [a,b]
}
export default useUserContent

然后直接在用到的地方使用即可

import React, { memo, useContext } from 'react'
import useUserContent from '../hook/use-context';

export default memo(function UseSelfHook() {
  let [user,token]=useUserContent()
  console.log(user,token);
  return (
    <div>
    </div>
  )
})

当然这种方式的使用场景非常多,非常灵活,也不是很复杂,因此在实际开发中使用的频率时比较高的。