说一下Hooks,为什么引入Hook,常见Hook的用法

Hook16.8出现的新特性。

hook的出现,使得在不编写Class组件的情况下,也可以使用State以及其他的React特性。

函数式???

一、 Q:既然类组件能做到相关功能,为什么还需要函数组件呢?

1.类组件在组件之间复用状态逻辑很难

场景: form表单,有很多的input,checkbox, 这些组件之间需要通信,还需要跟父组件通信,要复用状态逻辑,在form表单存储所有子组件状态,当某个子组件更新时,setState会更新整个form表单,这样就会导致不必要的更新。还可以使用高阶组件,consumer,props等方法组合,这些方法相互嵌套,逻辑就会很复杂。

可以使用Hook从组件中提取状态逻辑,Hook可以使在无需修改组件结构的情况下复用状态逻辑

2.复杂组件变得难以理解, hook可以使组件的颗粒度更小

类组件中的生命周期函数(例如:componentDidMount)只有一个,每个生命周期函数的功能不一样,如果要处理很多逻辑,这个生命周期函数就会很庞大,相互关联且需要对照修改的代码被进行了拆分(例如在componentDidMount中订阅,需要在componentWillUnmount里取消订阅),而完全不相关的代码会在同一个方法里。这样就很难理解,很容易产生bug。

而在函数组件中,可以多次使用hook函数,Hook将组件中相互关联的部分拆分成更小的函数

3.难以理解的class

在类组件中会出现找不到this的情况(如一个普通函数,如果没有使用箭头函数,没有进行bind绑定,使用的时候会出现找不到this的情况)
class 不能很好的压缩。

函数组件没有这些this的情况。

Hook 使你在非 class 的情况下可以使用更多的 React 特性

二、 Hook的使用规则

  1. 只能在函数最顶层调用Hook,不要在循环、条件或者子函数中使用。
  2. 只能在React的函数组件和自定义Hook中调用Hook,不要再其他JavaScript函数中调用。

三、 Hook API

  • 基础Hook
    • useState
    • useEffect
    • useContext
  • 额外Hook
    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeHandle
    • useLayoutEffect
    • useDebugValue
1.useState

在函数组件中使用state。

import React, { useState } from "react";

function Example() {
  const [count, setCount] = useState(0);
  
  return <div>
    <p>点击次数{count}</p>
    <button onClick={() => setCount(count+1)}>click</button>
  </div>
}

export default Example;

Q: useState 返回的是一个数组,为什么不定义成对象?

A: 如果定义成对象,就会固定key值,当使用多个状态时就会混乱。使用数组的话,第一个值定义状态变量,第二个值定义更新该状态的方法,状态名字用户自己定义即可,灵活方便。

state变量可以是数组或对象,更新state变量是替换,而不是合并

2.useEffect

给函数组件增加处理副作用(执行数据获取、修改DOM、订阅或取消订阅)的能力。

跟class组件的componentDidMount、componentDidUpdate、componentWillUnmount具有相同的的作用。

比如有一个需求是:每次点击都要在title上更新点击次数:

import { useState, useEffect } from "react";

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

  useEffect(() => {
    document.title = `页面点击${count}次`;
  })

  return <div>
    <p>当前页面点击{count}次</p>
    <button onClick={() => setCount(count + 1)}>click</button>
  </div>
}

export default Example;

然而,当点击其他路由时,title仍显示。现在来了新需求,要求跳转到原来的页面的时候,要恢复原来的title,这时就需要添加卸载,在effect返回一个清除函数。

import { useState, useEffect } from "react";

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

  useEffect(() => {
    const title = document.title;
    document.title = `页面点击${count}次`;
    // 卸载函数
    return () => {
      document.title = title;
    }
  })

  return <div>
    <p>当前页面点击{count}次</p>
    <button onClick={() => setCount(count + 1)}>click</button>
  </div>
}

export default Example;

然而,新需求又来了,要增加一个input输入框,发现该输入框更新,effect也会执行,会导致性能问题。现在提出新要求,要求input更新的时候,不执行title的副作用。其实useEffect还有第二个参数——依赖项数组,这个参数是个数组,包含副作用相关的变量。

import { useState, useEffect } from "react";

const Example = (props) => {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState("");

  useEffect(() => {
    console.log("useEffect");
    const title = document.title;
    document.title = `页面点击${count}次`;
    // 卸载函数
    return () => {
      document.title = title;
    }
  }, [count]); // 仅在count更新时更新

  return <div>
    <p>当前页面点击{count}次</p>
    <button onClick={() => setCount(count + 1)}>click</button>
    <input value={value} onChange={(event) => {
      setValue(event.target.value)
    }
    } />
  </div>
}

export default Example;

如果只想运行一次effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。

3.useContext
4.useLaoutEffect
5.useRef
6.useReducer
7.useCallback

返回一个 memoized 回调函数

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

把内联回调函数及依赖项数组作为参数传入useCallback, 它将返回该回调函数的memoized版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用医用相等性去避免非必要渲染(例如shouldComponentUpdate)的子组件时,他将非常有用。

import { useState, useEffect, useMemo, useCallback, PureComponent } from "react";
import useClock from "./useClock";

const UseHook = (props) => {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState("");

  useEffect(() => {
    console.log("useEffect");
    const title = document.title;
    document.title = `页面点击${count}次`;
    // 卸载函数
    return () => {
      document.title = title;
    }
  }, [count]); // 仅在count更新时更新

  // 计算1-count的累加和
  const accumulate = useMemo(() => {
    console.log("accumulate");
    let i = 1, sum = 0;
    for (; i <= count; i++) {
      sum += i;
    }
    return sum;
  }, [count]); // 只有在count更新时更新


  // 计算count的阶乘
  const multiply = useCallback(() => {
    let i = 1, multi = 1;
    for (; i <= count; i++) {
      multi *= i;
    }
    return count === 0 ? 0 : multi;
  },[count]);

  return <div>
    {useClock().toLocaleTimeString()}
    <p>当前页面点击{count}次</p>
    <p>{count}的累加和为:{accumulate}</p>
    <button onClick={() => setCount(count + 1)}>click</button>
    <input value={value} onChange={(event) => {
      setValue(event.target.value)
    }
    } />
    <Child multiply={multiply}/>
  </div>
}

class Child extends PureComponent {
  render() {
    console.log("child render");
    const { multiply } = this.props;
    return <div>
      <p>Child</p>
      <p>当前count的阶乘为:{multiply()}</p>
    </div>
  }
}

export default UseHook;
8.useMemo

返回一个 memoized 值。

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

有个需求是要计算count的累加数,希望在count改变的时候,重新计算累加数。于是写了一个accumulate函数,如下:

import { useState, useEffect, useMemo } from "react";
import useClock from "./useClock";

const UseHook = (props) => {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState("");

  useEffect(() => {
    console.log("useEffect");
    const title = document.title;
    document.title = `页面点击${count}次`;
    // 卸载函数
    return () => {
      document.title = title;
    }
  }, [count]); // 仅在count更新时更新

  // 计算1-count的累加和
  function accumulate() {
    console.log("accumulate");
    let i = 1, sum = 0;
    for (; i <= count; i++) {
      sum += i;
    }
    return sum;
  }

  return <div>
    {useClock().toLocaleTimeString()}
    <p>当前页面点击{count}次</p>
    <p>{count}的累加和为:{accumulate()}</p>
    <button onClick={() => setCount(count + 1)}>click</button>
    <input value={value} onChange={(event) => {
      setValue(event.target.value)
    }
    } />
  </div>
}

export default UseHook;

但是发现,由于页面有一个时钟,页面在一直更新,accumulate也一直在执行,如果在input框内输入内容,accumulate也会执行。这样严重影响性能。于是做了如下优化:

import { useState, useEffect, useMemo } from "react";
import useClock from "./useClock";

const UseHook = (props) => {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState("");

  useEffect(() => {
    console.log("useEffect");
    const title = document.title;
    document.title = `页面点击${count}次`;
    // 卸载函数
    return () => {
      document.title = title;
    }
  }, [count]); // 仅在count更新时更新

  // 计算1-count的累加和
  const accumulate = useMemo(() => {
    console.log("accumulate");
    let i = 1, sum = 0;
    for (; i <= count; i++) {
      sum += i;
    }
    return sum;
  }, [count]); // 只有在count更新时更新


  return <div>
    {useClock().toLocaleTimeString()}
    <p>当前页面点击{count}次</p>
    <p>{count}的累加和为:{accumulate}</p>
    <button onClick={() => setCount(count + 1)}>click</button>
    <input value={value} onChange={(event) => {
      setValue(event.target.value)
    }
    } />
  </div>
}

export default UseHook;

只有在count变化的时候,才会执行accumulate。

9.useImperativeHandle

四、 自定义Hook

自定义Hook是一个函数,必须以"use"开头,函数内部可以调用其他Hook。目的是达到状态逻辑复用。

import { useState, useEffect } from "react";

// 自定义hook,必须以use开头
function useClock() {
  const [time, setTime] = useState(new Date());
  useEffect(() => {
    const timer = setInterval(() => {
      setTime(new Date());
    }, 1000);
    return () => {
      clearInterval(timer);
    }
  }, []); // 只有在挂载的时候执行副作用

  return time;
}

export default useClock;

可以在多个地方引用:

import { useState, useEffect } from "react";
import useClock from "./useClock";

const UseHook = (props) => {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState("");

  ...

  return <div>
    {useClock().toLocaleTimeString()}
    ...
  </div>
}

export default UseHook;

上一篇:react Hooks 封装 useState


下一篇:聊聊H5浏览器实现扫一扫