React之hooks

403 阅读11分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。详情

本篇文章知识点速览:hook的简介、为什么要引入hook,使用hook解决了什么问题、hook的api(10种hook)讲解、如何使用自定义hook、使用hook的注意事项等。

React Hook简介

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

1. 引入hook的动机

  1. 难以理解的class

    我们在学习react的时候,使用class组件需要去理解其概念和js中的this。还需要绑定事件处理器。除此之外,class组件的写法也很繁琐。理解成本和编写成本都很高。

  2. 复杂组件变得难以理解

    我们使用类组件后,随着业务逻辑变得复杂,组件的内容也越发庞大。组件中时常会包含很多不相关的逻辑状态或者副作用。比如在componentDidMount中我们需要获取多种数据,还要处理其他的逻辑,比如订阅,监听等。不同的事件处理都放在一起混杂且容易产生bug。而Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据) ,而并非强制按照生命周期划分。

  3. 组件之间的复用状态逻辑很难

    当我们类组件跨组件复用时(如把组件连接到store),我们可以使用render props或高阶组件。但是使用这些方案时需要修改当前组件结构且很复杂。且由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件嵌套过多会形成“嵌套地狱“。此种情况我们可以使用 自定义Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。

2. hook解决的问题

  1. 函数组件可以代替class:hook的出现使得函数组件有了状态,实现了类似React class组件的state、生命周期钩子等特性,可以执行副作用。
  2. 跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的HOC嵌套地狱
  3. 状态与UI隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。

3. 函数组件与类组件对比

  • 类组件需要继承 class,函数组件不需要;
  • 类组件可以访问生命周期方法,函数组件不能;
  • 类组件中可以获取到实例化后的 this,并基于这个 this 做各种各样的事情,而函数组件不可以;
  • 类组件中可以定义并维护 state(状态),而函数组件不可以;

当React引入hook后,我们就可以使用函数组件来代替class组件,以避免出现类组件的这些情况:

  • 不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
  • 时刻需要关注this的指向问题;
  • 代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;

4. 注意事项

  • 避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
  • 只有函数定义组件hooks 可以调用 hooks,避免在 类组件 或者 普通函数 中调用;
  • 不能在useEffect中使用useState,React 会报错提示;
  • 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存;

HOOK API

基础hook:

useState

用于定义组件的 State,相似于类组件中定义this.state的功能;

const [num, setNum] = useState(0);  // 0是num的初始值
setNum(1);  // 修改num为1
// useState和setState的方法还可以传入一个函数
const [num, setNum] = useState(()=>{
    const initNum = someExpensiveComputation(props)//一些复杂运算
    return initNum;
});
setNum(preNum=>preNum*2);

①调用useState的传参:可以直接传入一个初始值,也可以传入一个函数,通过复杂计算后生成初始值return给useState(state的惰性初始化)。初始值和初始函数的设置只在组件初始化渲染时起作用。

②调用useState的返回结果:返回数组,第一个值为一个组件状态值,上面示例命名为num接收。返回第二个值为一个修改这个状态值的方法,我们命名为setNum。

③调用setNum可以修改num,我们可以直接传入值,也可以是一个回调函数,回调函数可以拿到上一次这个状态的值。

useEffect

这个hook可以做一些副作用操作。默认情况下,effect 将在每轮渲染结束后执行。我们可以使用它达到class组件中生命周期钩子的作用。

useEffect(() => {
    // 组件挂载后执行事件绑定
    console.log('on')
    addEventListener('xx')
    
    // 组件 update 时会执行事件解绑
    return () => {
        console.log('off')
        removeEventListener('xx')
    }
}, [source]);

useEffect接收两个参数:第一个参数是执行副作用的函数;第二个参数是依赖项,是一个数组。

②当依赖项数组中有值变化时,执行第一个参数的函数。

③当依赖项为空数组[]时,相当于类组件的componentDidMount,在组件挂载后执行一次副作用函数。同时第一个参数 副作用函数可以return一个方法,在组件卸载时调用一次,类似于componentWillUnmount

const useMount = (fn) => useEffect(fn, [])
const useUnmount = (fn) => useEffect(() => fn, [])

④effect的执行时机是在浏览器重新渲染之后延迟执行操作。

【注】组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。创建依赖性后,effect只有依赖性更新后才会创建新的订阅

useContext

通过useContext我们可以使用Context进行组件传值通信。

import React, { useContext, useState } from 'react';
const Context = React.createContext();
​
// 1. 通过useContext使用Context组件通信
function HookDemo () {
  const value = useContext(Context)
  return(<div>
    useContext得到value:{value}
  </div>)
}
​
// 2.使用Context.Consumer方式通信
const ConsumerDemo = ()=>{
  return <Context.Consumer>
      { (value)=> <div> Context.Consumer方式获取value:{ value }</div> }
  </Context.Consumer>
}
​
// 父组件
export default function () {
  const [num,setNum] = useState(1)
  return <div>
    <Context.Provider value={num}>
      <HookDemo />
      <ConsumerDemo />
    </Context.Provider>
    <button onClick={()=>setNum(num+1)}>++</button>
  </div>
}

①通过React.createContex创建一个Context

②使用<Context.Provider value={num}></Context.Provider>将需要跨层级接收传值的组件包裹起来,其中的value是传给子级组件的值。

③使用hookuseContext()可以获取到Context.Provider传入的value值。useContext()方法需要传入①步骤中创建的同一个Context做参数。

④还有其他<Context.Consumer>方法来获取Context.Provider传递的value。


其他hook

useReducer

语法:

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

可以替代useState。在某些特殊场景下使用useReducer要比useState合适。

// 1.定义一个初始状态值
const initState = {num: 0};
// 2. 定义一个reducer方法,reducer方法可以回调拿到上一次的state和一个action
function reducer(state, action) {
    // 3. 根据不同的action做不同操作
  switch (action.type) {
    case 'increment':
      return {num: state.num + 1};
    case 'decrement':
      return {num: state.num - 1};
    default:
      throw new Error();
  }
}
​
function Counter() {
  // 使用useReducer定义一个reducer方法,返回一个state和dispatch方法
  // 可以调用dispatch方法传入一个action后会触发reducer对state进行修改
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      值: {state.num}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

这个写法很类似与Reduxdva中的model

另外useReducer的第三个参数是传入一个函数做参数,函数return一个值做状态值的惰性初始化,与useState的类似。

useMemo

这个方法用法类似useEffect,都是传入一个函数和一个依赖性。

用于缓存传入的 props,避免依赖的组件每次都重新渲染;

const memoizedValue = useMemo(()=>{
    const val = someExpensiveComputation()//一些复杂运算
    return val
}, [props.num]) // 只有当依赖项中有变化时才执行函数

【注】:

传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo

传入useEffect的副作用函数不会在渲染阶段执行。在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。

③如果没有依赖性,useMemo 在每次渲染时都会计算新的值。

使用场景示例:减少dom循环生成

// 只有当dataList变化时才重新渲染相关列表dom的部分
{useMemo(() => (
      <div>
        {dataList.map((i, v) => (
              <span
                  className={style.listSpan}
                  key={v} >
                  {i.patentName} 
              </span>
          ))}
      </div>
), [dataList])}
​
// 只有当props中list列表改变的时候,子组件才重新渲染
const  goodListChild = useMemo(()=> <GoodList list={ props.list } /> ,[ props.list ])

useCallback

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

用于缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;

useMemo在依赖变化时执行传入的fn

useCallback在依赖变化时返回一个fn

const DemoChildren = React.memo((props)=>{
  /* 只有初始化的时候打印了 子组件更新 */
   console.log('子组件更新')
  useEffect(()=>{
      props.getInfo('小明')
  },[])
  return <div>子组件</div>
})
const DemoUseCallback=({ id })=>{
   const [number, setNumber] = useState(1)
   // 通过useCallback可以拿到子组件传过来的值:'小明'
   const getInfoCallback  = useCallback((sonName)=>{
         console.log(sonName)
   },[id])
   return <div>
       {/* 点击按钮触发父组件更新 ,但是子组件没有更新 */}
       <button onClick={ ()=>setNumber(number+1) } >增加{number}</button>
       <DemoChildren getInfo={getInfoCallback} />
   </div>
}

useLayoutEffect

  • DOM更新同步钩子。用法与useEffect类似,只是区别于执行时间点的不同
  • useEffect属于异步执行,而useLayoutEffect则会真正渲染后才触发;
  • 可以获取更新后的 state;

useEffect执行顺序: 组件更新挂载完成 -> 浏览器 dom 绘制完成 -> 执行 useEffect 回调。

useLayoutEffect 执行顺序: 组件更新挂载完成 -> 执行 useLayoutEffect 回调-> 浏览器dom绘制完成。

所以说 useLayoutEffect 代码可能会阻塞浏览器的绘制 。我们写的 effectuseLayoutEffectreact在底层会被分别打上PassiveEffectHookLayout,在commit阶段区分出,在什么时机执行。

const DemoUseLayoutEffect = () => {
    const target = useRef()
    useLayoutEffect(() => {
        /*我们需要在dom绘制之前,移动dom到制定位置*/
        const { x ,y } = getPositon() /* 获取要移动的 x,y坐标 */
        animate(target.current,{ x,y })
    }, []);
    return (
        <div >
            <span ref={ target } className="animate"></span>
        </div>
    )
}

useRef

使用useRef可以拿到元素/组件实例

import React, {useRef} from 'react';
function RefsTest() {
  const nameRef = useRef(null)
  const btnClick = () => {
    console.log(nameRef.current, nameRef.current.value)
  }
  return(
    <div>
      姓名:
      <input ref={nameRef}></input>
      <button onClick={btnClick}>提交</button>
    </div>
  )
}

ref的使用可以看这里

useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用:

function FancyInput(props, ref) {
  const inputRef = useRef();
  // 暴漏整个input元素出去
  useImperativeHandle(ref, ()=>inputRef.current);
  // 也可以只暴漏input元素的某些方法和值
  // useImperativeHandle(ref, () => ({
  //   value: inputRef.current.value;
  //   focus: () => {
  //    inputRef.current.focus();
  //   }
  // }));
  return <div><input ref={inputRef} /></div>;
}
// 需要结合forwardRef转发ref使用
const RefInput = React.forwardRef(FancyInput);
// 父组件
function RefsTest() {
  const nameRef = useRef(null)
  const btnClick = () => {
    console.log(nameRef.current, nameRef.current.value)
  }
  return(
    <div>
      <RefInput ref={nameRef} />
      <button onClick={btnClick}>提交</button>
    </div>
  )
}
export default RefsTest

ref的使用可以看这里

useDebugValue

useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。

例如,“自定义 Hook” 章节中描述的名为 useFriendStatus 的自定义 Hook:

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
​
  // ...
​
  // 在开发者工具中的这个 Hook 旁边显示标签  // e.g. "FriendStatus: Online"  useDebugValue(isOnline ? 'Online' : 'Offline');
  return isOnline;
}

自定义HOOK

基于 Hooks 可以引用其它 Hooks 这个特性,我们可以编写自定义钩子。

例如我们给每个页面自定义标题:

function useTitle(title) {
  useEffect(
    () => {
      document.title = title;
    });
}
​
// 使用:
function Home() {
    const title = '我是首页'
    useTitle(title)
    return (
        <div>{title}</div>
    )
}

注意事项

1. hook的使用限制

上面简介中我们说了,不可以在循环、条件嵌套函数中调用 Hook,这是为什么呢?

因为 Hooks 的设计是基于数组实现的。在调用时按顺序加入数组中,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。当然,实质上 React 的源码里不是数组,是链表

2. useEffect 与 useLayoutEffect 区别

image-20220217213520297.png

相同点:底层签名函数都是调用的 mountEffectImpl;用法一样;都用于处理副作用。

不同点:①useEffect在渲染过程中是异步调用的,适合大多场景。而 LayoutEffect 会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在 LayoutEffect 做计算量较大的耗时任务从而造成阻塞。

使用:如果实在分不清两者的应用场景,可以先用 useEffect,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect 即可。