1_-Ijet6kVJqGgul6adezDLQ.png

在以前的react 开发中我们习惯使用class类的写法写react组件,但是现在随着函数式编程的流行,函数作为js的第一公民,我们在react使用函数编写业务组件,在原来只能写无状态的组件,没有自身的state,hook的出现可以让函数组件钩入一些方法,可以让我们在函数组件中使用state和一些class类的组件写法,那么我们这篇笔记将以react-hook文档为基础来做一些总结。

Hook 简介 - React

State Hook

import React, { useState } from "react";

// 计算器的组件
function Main() {
  const [count, setCount] = useState(0);
  return (
    <div>
      {count}
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        增加
      </button>
    </div>
  );  
}

export default Main;

我们可以使用useState这个hook来声明state,返回是一个数组,第一个是我们的变量,第二个是设置这个变量的函数,我们使用数组解构的方式结构出来,那么在这个函数的作用域中直接使用count来展示这个state,我们调用函数直接调用setCount这个函数,这样我们就简单的使用statehook这个钩子来实现class组件this.state这个功能了,注意这个set函数和class中的setState不同,在class组件中的setState中,是要传入当前示例的全部state对象,我们往往要使用对象合并API来操作,但是hook中的set函数只需要传递当前的要设置的值即可。

setCount(100);

我们可以设置更多的state

const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);

我们没有必要使用这样的hook来声明多个变量,在之后的Q&A中,文档会建议我们更好的方法。

Effect Hook

在class写法中的各种生命周期的钩子,在函数组件中也有对应的hook来替代它们,那么这些钩子我们叫做effect hook,在组件渲染成功后通常会进行一些基于业务的操作,所以我们也称之为“副作用hook”,下面我们对比一下class写法和hook写法

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

在componentDidMount以及componentDidUpdate这两个生命周期中,当组件被挂载或者已有prop或者state变更都会触发对应的document.title的业务逻辑,那么在class这样的写法,同样的逻辑在不同的生命写,这会让代码又臭又长,不好维护,我们使用hook来写,那么就非常简单。

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

// 计算器的组件
function Main() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = "你点击了" + count + "次";
  });
  return (
    <div>
      {count}
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        增加
      </button>
    </div>
  );
}

export default Main;

useEffect可以传递一个函数作为入参,我们写一个useEffect就相当于写了组件挂载和更新2个钩子,那么当组件销毁(清除副作用)我们可以这么写

useEffect(() => {
    API.on(); // 比如当页面进入调用订阅API
        return () => {
            API.off(); // 销毁时再取消订阅API
        }
});

useEffect会在每次挂载和更新都会执行我们传入的逻辑,如何控制它将在后面我们会有具体的例子。

那么我们使用effectHook和传统的class中的挂载和更新生命周期还是有区别的。effectHook不会阻塞浏览器的屏幕更新,它会让应用看起来更快。

我们肯定写过下面的逻辑代码:

componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
        // 调用A API
        A();
        // 调用B API
        B();
        // 调用C API
        C();
  }

不得不说,不同的业务逻辑混在一个生命去写,尽管我们有时会把不同的逻辑封装到不同函数中进行组合调用,但是代码还是难以直接去表达业务逻辑,这让维护代码的人叫苦连连,那么我们使用hook,将可以使用像stateHook一样,可以调用多个hook:

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  useEffect(() => {
    API.on(); // 比如当页面进入调用订阅API
    return () => {
      API.off(); // 销毁时再取消订阅API
    };
  });
  // ...
}

react会顺序执行多个effectHook

那么在官方文档中,提到了一个我也很苦恼的问题,在我进行第一次学习effectHook的时候,为什么组件更新还需要调用effect,直到文档做了一个很简单的查询好友信息的demo:

通过好友ID查询好友详情,好友详情组件需要接收一个prop,那么如果当prop进行更新,组件内的effect回调不会触发还是展示上一个好友的信息,而在清除副作用的时候,因为ID错误导致程序异常,那么显然effect设计如此是为了规避程序BUG,那么在class类中,我们应该使用如下方式来规避这样的更新问题:

componentDidUpdate(prevProps) {
    // 取消订阅之前的 friend.id
      API.off(prevProps.id); // 销毁时再取消订阅API
    // 订阅新的 friend.id
    API.on(prevProps.id);
 }

而使用hook则不需要关心更新组件带来的异常, 因为使用effect时,在调用一个新effect时就会清除上一个effect(触发清除回调effect),也就不会产生因为忘记写update逻辑造成的BUG

我们既然知道了effectHook更新设计的原因,那么当部分业务逻辑因为更新而造成的多余性能浪费,class和hook解决方案是怎么样的呢?

// class的写法,比较ID是否变更,变更再进行逻辑
componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

// effectHook提供了第二个可选参数,我们可以传入一个数组,比如
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

当我们传入一个空数组时

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, []);

那么此effect将在挂载销毁执行,任何值的update都和此effect无关,所以这样的写法一定要注意应用场景。

总结Effect Hook:

  1. 自带的清除机制,可以避免在class中因为update而造成的问题
  2. 使用effect可以让业务逻辑分离解耦,不会像class中的生命周期分离逻辑变的很难

Hook规则概要

hook本质就是JS的函数,但是使用它也需要规则,虽然有专门的语法检查工具:

eslint-plugin-react-hooks

  1. hook需要写在函数中最高的层级,不要在循环,嵌套中去使用它们
  2. 只能在React中使用hook,不要在普通JS函数中去使用hook

关于插件的使用,文档也有非常清楚的配置方法

Hook 规则 - React

至于为什么hook只能在函数中最高层级去使用,官方也在文档中说明了:

demo1:

function Form() {
  // 1. Use the name state variable
  const [name, setName] = useState('Mary');

  // 2. Use an effect for persisting the form
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState('Poppins');

  // 4. Use an effect for updating the title
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}

useState和useEffect如果都在最高层(函数作用域的最高层)中,那么React就可以把对应的Maryname 进行关联Poppinssurname进行关联,react通过声明的顺序去查找对应的变量,每次渲染的顺序和声明的顺序都是相同的,所以React才可以找到,那么如果我们写了如下的代码:

if(name !== null){
    useEffect(function persistForm() {
    localStorage.setItem('name ', name);
  });
}

这样触犯了hook的第一条规定,导致了程序有可能不会声明useEffect这个钩子,如果没有声明,那么声明的顺序和渲染的顺序就会不一致,就导致这个钩子之后的所有声明的钩子都将会被提前执行,导致BUG产生

自定义Hook的实现

我们自己写一个求差的小demo,参数传入减数和被减数简单体验一下自定义hook:

这个demo展示了hook和组件的传参,当组件的count改变时会触发到hook中的effect,effect会重新计算差并返回。

import { useState, useEffect } from "react";

export function useSeekingDifference(
  subtraction: number,
  minuend: number
): number {
  const [difference, setDifference] = useState(0);
  useEffect(() => {
    setDifference(subtraction - minuend);
  }, [subtraction, minuend]);
  return difference;
}

使用hook

import React, { useState, useEffect } from "react";
// 引入自定义hook
import { useSeekingDifference } from "./../hook/computed";

// 计算器的组件
function Main() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = "你点击了" + count + "次";
  });
 console.log(useSeekingDifference(count * 2, count)); // 使用自定义hook
}

export default Main;

我们写自定义hook的目标就是:

  1. 抽离业务逻辑

但是对于业务性非常强,以及对状态管理有频繁操作的建议使用redux(尽管我没用过,但是官方建议这样)

我们书写hook时需要注意:

  1. hook不是react特有,只是遵循了hook的规定的函数
  2. 必须使用use开头,因为不仅仅代表了它是一个hook,也可以让插件去做校验规则
  3. 相同hook不会共享state,每一个hook都是有独立的state和effect的,是重用状态逻辑机制

标签: none

添加新评论