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 的调用顺序,找到修改后的状态返回,这样组件就可以拿到正确的状态了。