Skip to content
MiTu

MiTu

A Web developer who loves to Reading and photography

hook

简介

什么是 hook?

Hook 是一个特殊的函数,它可以让你“钩入” React 的特性

这里的 React 特性指的是生命周期、state 状态等。

当我们希望使用函数式组件,并且希望能够让不同功能代码更聚合,更好维护时候,可以使用 hook。

为什么需要 hook?

为了让功能代码聚合在一起,增加了组件的可维护性。hook 提供了功能逻辑代码复用的新的方式。React 需要提供一些额外的 API 来实现,由于 React 更推崇函数式组件的简洁和声明式编程的理念,因此将这种 API 增加在了函数式组件中,而非 class 组件中。

hook 给函数式组件增加状态和逻辑,并将功能代码聚合在一起。

hook 的规则:

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

自定义 hook

自定义 hook 是一个使用了 React 的原生 hook 和其他自定义 hook 的函数。自定义的 hook 就是一个功能复用的单元。

常用的 hook

useState 和 useEffect

在函数式组件中添加状态需要使用 useState 这个 hook;在函数式组件中添加一些有副作用的操作(如网络请求、操作 DOM、绑定事件回调),需要用到 useEffect 这个 hook。

useState

  • useState 函数接收一个参数作为初始值
  • useState 返回值为一个数组,数组第一个元素是状态变量,第二个元素是设置该状态的方法
  • 调用设置状态的方法,会同步触发 diff 和更新操作

注意:相比单个 state,更推荐多个 state,因为单个 state 无法达到聚合功能代码的目的,所以应该每个 state 属性都调用 useState 创建一个对应的状态。我应该使用单个还是多个 state 变量?

useEffect

  • useEffect 接收第一个参数是一个回调 callback,会在特定时机执行
  • 给 useEffect 传入的 callback,可以返回一个方法 clearEffect,React 会在组件卸载时候调用 clearEffect,清除副作用(比如停止定时器,解除事件绑定)
  • useEffect 第二个参数是一个数组,数组中是依赖项,当依赖变化时候才会执行 useEffect 传入的方法
  • 如果是空数组,则只在组件初始挂载时候执行,类似 componentDidMount
  • 如果第二个参数不传,则在组件更新时候执行,类似 componentDidMount 和 componentDidUpdate

下面看一个简单的示例

useState 返回的 set 方法,当数值相同时候不会触发子组件 render,这个和 class 组件不同。另外每次 set 都会立即触发更新,而不是像 this.setState 一样,合并异步更新。

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

const ChildComponent = props => {
  let [digit, setDigit] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      // 立即触发更新,不会有延时
      setDigit(digit++);
    }, 1000);

    return () => {
      clearInterval(timer);
    };
  }, []);

  return <div>{digit}</div>;
};

class App extends React.PureComponent {
  state = {
    showChild: true
  };

  componentDidMount() {
    setTimeout(() => {
      this.setState({ showChild: false });
    }, 10 * 1000);
  }

  render() {
    return this.state.showChild && <ChildComponent />;
  }
}

export default App;

useMemo 和 useCallback

useMemo 的作用类似于 Vue 中的 computed,会缓存计算结果,只有当依赖的数据改变时候,才会执行其中的计算方法。

useCallback 一般用于生成事件处理函数,防止每次生成新的函数,导致不必要的子组件重新 render。场景是计算属性和注册回调。

useMemo

  • useMemo 接收的第一个参数是一个函数,这个函数用来根据 state 计算新的值。
  • useMemo 接收的第二个参数是一个依赖项数组。如果不传,则每次都会调用计算函数,没有任何优化效果;如果传空数组,则只在第一次调用计算函数,返回的结果永远不变;如果数组传入计算的依赖项,则依赖项变化时候调用计算函数,得到最新的结果,在依赖项不变时候,返回之前的结果,因此起到了“缓存计算结果”的效果。

useCallback

  • useCallback 接收第一个参数 callback。
  • useCallback 接收第二个参数,是依赖项。
  • useCallback 返回一个函数,如果依赖项不传,则每次生成一个新的函数;如果依赖项传空数组,则只在第一次生成一个函数,后面每次 render 都使用这个函数;如果传入依赖项,则当依赖项发生变化时候生成一个新的函数。
  • 注意,useCallback 的回调中如果使用了某个 state,就会形成一个闭包,如果给 useCallback 传入第二个参数是空数组,则生成的函数引用的永远是 state 初始化的值,因此如果回调中依赖了某个 state,应该把依赖项加到第二个参数的数组中。(看下面注释加深理解)

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

看下面简单的示例

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

class Button extends React.PureComponent {
  render() {
    console.log('render Button');
    return <button onClick={this.props.onButtonClick}>计算平方</button>;
  }
}

const App = () => {
  console.log('render App');
  const [digit, setDigit] = useState(0);

  // 避免了Button组件每次点击计算按钮时候都会更新
  const onButtonClick = useCallback(() => {
    setDigit(document.getElementById('_input').value);
    // 这里访问的digit是在调用App方法时候,useState返回的
    // 每次调用App都会返回一个新的变量(但值可能不会变)
    // 如果第二个参数是"[]",则onButtonClick一直不会变
    // 初始的回调中访问的digit是0,因此后面即使digit改变,调用onButtonClick还是打印0
    // 除非在依赖项数组中加入digit,onButtonClick在digit改变时候重新创建新的函数,就可以打印最新的digit了
    console.log(digit);
  }, []);

  // 当digit不变时,square不会重新计算,而是返回之前的值
  const square = useMemo(() => {
    console.log('calculate square');
    return digit * digit;
  }, [digit]);

  return (
    <div>
      <input id="_input" type="number" placeholder="请输入整数" />
      <Button onButtonClick={onButtonClick} />
      <div>平方:{square}</div>
    </div>
  );
};

export default App;

useRef

useRef 可以用来实现获取一个原生 DOM 或者子组件实例的引用,本质上,它可以用来保存任何可变值。

  • useRef 接收一个参数作为初始值。
  • useRef 返回一个对象,这个对象包含一个 current 属性,这个 current 属性会被初始化为初始值,并且可以更改。
  • 每次调用渲染函数,都返回同一个对象。

看下面的示例

jsx
// 获取input元素的引用
function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
jsx
// 用来保存timer的id
function Timer() {
  const intervalRef = useRef();

  useEffect(() => {
    intervalRef.current =  = setInterval(() => {
      // ...
    });
  }, []);

  const handleCancelClick = useCallback(() => {
    clearInterval(intervalRef.current);
  }, []);

  // ...
}

useState 原理

useState 让函数式组件能够拥有自己的状态,如何让函数式组件拥有自己的状态呢?

有两个关键:

  1. 状态存储在哪里?
  2. 状态以什么格式存储?

状态存储在哪里呢?对于 class 组件可以直接挂载实例上面,函数式组件又应该存储在哪里呢?答案是挂在虚拟 DOM 节点上,渲染过程中,调用函数组件时候 React 是知道在处理哪个节点的,因此当在函数式组件中调用 useState 方法,React 就可以把创建的状态挂到相应的节点上面。在下一次调用函数组件时候,React 发现已经有了这个状态,就把值返回。

状态如何存储?首先我们想到可以类比 class 组件,把整个 state 作为一个对象存储,但是上面提到,这样存储不能达到分离功能逻辑的目的;因此我们希望可以将状态分开,每个状态有自己对应的 key,

例如这样:const [a, setA] = useState('a', '');

即用对象存储状态,useState 方法传递一个 key 和一个初始值,React 把这个 key 和对应 value 存储到对象上。但是这样会比较冗余。React 实现方式是只需要传递初始值,不用传递 key,React 根据 useState 调用的顺序来识别是哪一个状态。那么函数式组件的状态就是一个数组格式,每个状态在函数式组件中调用 useState 声明的顺序,也是它们在状态数组中存储的顺序。

这也是 useState 必须在函数顶层调用的原因。

useState 工作的过程如下:

  1. 首次调用函数组件,执行 useState,React 获取到当前正在遍历的节点,创建_state 对象挂载到节点上,根据调用 useState 的顺序,在数组中加入状态,并根据传入的初始值初始化状态。
  2. 当调用状态的 set 方法时候,React 修改_state 上面的状态,并更新相应的组件。
  3. 后面再调用函数组件时候,useState 找到对应的节点的_state,并根据 useState 的调用顺序,找到修改后的状态返回,这样组件就可以拿到正确的状态了。