lihongxun945 / myblog

言川的博客-前端工程师的笔记
1.41k stars 128 forks source link

React Hooks 1 - State Hook , Effect Hook, Custom Hook #41

Open lihongxun945 opened 5 years ago

lihongxun945 commented 5 years ago

什么是Hooks

从19年初 React V16.8 开始,正式支持Hooks特性。React Hooks 是一种能让你在函数组件中使用state和组件生命周期的一种方式,在Hooks出来之前,你必须把函数组件改成class组件才能用到这些特性。 而且,Hooks特性是完全兼容老版本代码的,所以不会对已有代码造成任何影响。并且官网也不推荐为了用Hooks而重构老代码。

Hooks分为几种:

为什么要弄出一个Hooks特性?最重要的原因是为了解决逻辑复用的问题,相比对 HoC 或者 render props,他能用更少更简洁的代码实现逻辑复用。在后续的例子中我们可以看到如何用Hooks实现逻辑代码复用。

官方给出的Hooks使用规范:

下面我们看看最重要的两个Hook: useStateuseEffect

useState

State Hook 可以让我们在函数式组件中就能直接使用 state,而在这之前只能声明一个类并且初始化state才行。以下是官方给的简单使用的例子:

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

逻辑上等价于如下代码:

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

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

可以明显看到 useState 代码更加简洁,并且不会把数据和UI的声明周期混在一起。其中最神奇的一行代码就是:

const [count, setCount] = useState(0);

这行代码第一眼会非常难以理解。其基本工作原理是,useState 会返回一个数组,这个数组的结构是 [{变量值}, {设置变量的方法}],所以我们通过解构语法就能把 countsetCount 取出来,当然,这里你随便取什么名字都是没关系的。第一次运行的时候,会返回一个初始值,就是你传入的参数,之后每一次调用都会返回当前值。

setState 不同的地方是, setCount 并不会执行merge操作,而是每一次都是直接替换(划重点了)。

这就是 State Hook 的基本用法,其实很简单好理解。

State Hook的作用域

State Hook作用域和类组件的State有很大不同,State Hook 每一次都会直接替换掉旧的state,每一次render的时候都会通过闭包获取一个全新的state引用(上一次render时替换的新值),并且在本次render过程中保持不变(被改变之后要在下一次render中才能获得新的值)。而类组件其实多次render我们都是读写的同一份State。画一个图表示他们的区别:

Effect Hook

相比于State Hook,Effect Hook会复杂一些,他的作用是:在更新DOM之后执行一些有副作用的方法,比如加载数据、修改DOM等。一般我们会在 componentDidMountcomponentDidUpdate 这两个生命周期中做,并且可能需要在 componentWillUnmount 中做一些清理工作。而现在我们可以把这三个生命周期统一到一个Effect Hook 中。

放在生命周期中做一些逻辑操作会有什么缺点呢?其实主要是两个方面:

  1. 组件生命周期有时候和一些逻辑操作的生命周期并不一致,导致代码的庸余
  2. 很多逻辑操作自己本身会分成几个阶段,这几个阶段应该放在一起才好理解,现在却拆分到各个生命周期里面,和其他逻辑混在一起。并且这样分开代码也会导致作用域分开而不得不进行this绑定等操作。

借用官网的一个例子来说明,假设我们有一个组件需要在 title 上显示数据,传统的写法要这样:

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>
    );
  }
}

在上述代码中,其实我们就是需要在render执行完了改变title,然而我们把同一段逻辑重复了两次。如果有其他逻辑,可能我们都需要这种重复代码。如果用Effect Hook重写,不单解决了重复代码的问题,也解决了代码逻辑散落在各个生命周期中的问题:

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Effect Hook的执行特点:

清理 Effect Hook

那么如果我们的操作还需要进行“清理”应该怎么办呢?比如订阅了一个事件,当结束的时候需要取消订阅。在传统的做法中,我们一般通过 “umount” 生命周期来做。在Hooks的实现中,我们只需要返回一个函数即可。React会在合适的时机调用这个函数进行清理。

官方示例:

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

具体何时进行清理就比较特殊了,因为我们前面说过,useEffect 是每一次都会创建一个新的方法,每次render都会调用一遍,所以清理操作也是每次render都需要做的,而不是仅仅在 unmount 才做,具体的时机是“每一次render前都会清理上一次的effects”。也就是当前执行render结束后不会清理这一次的,而是清理上一次render调用的。

通过返回函数进行清理的方式还有一个好处,就是我们一般清理操作都会用到原来的一些变量,放在同一个函数中,就不会出现作用域隔离而不得不绑定到 this 上来共享变量的问题。

有些同学会有疑问,如果有些操作消耗比较大,不想每次 render 都做怎么办呢?React 官方提供了一种方式,可以指定只有某些变量发生变化了才调用,具体的用法如下:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

有一个小技巧,如果有一个Effect 只想运行一次,那么直接传一个空数组即可。

Hook 背后的实现原理

假设我们在一个组件中写了多个Hook,比如:

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');
  // ...
}

那么在多次渲染的过程中,React是怎么知道不同的 useStateuseEffect 对应哪一个呢? 其实React内部是通过一个数组进行记忆的,也就是React并没有记住谁是谁,仅仅按照顺序来分配。所以,多次render的过程中顺序一定不能乱,举个例子:

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

向这样加了条件判断,那么就有可能导致顺序的不同而出现错误。所以官方文档强调“一定要把Hook的代码放在顶级,不能被任何其他代码包裹,也不能通过外部函数调用”。如果确实需要加一些条件,那么就放在Hook里面去做。

通过 Custom Hook 封装复用业务逻辑

上一篇主要讲了内置的 State Hook 和 Effect Hook,这一篇我们讲一下 Custom Hook。自定义Hook的主要目的是为了封装代码逻辑。

还是直接用官网的例子,假设我们有一个组件,会显示好友的在线状态:

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

其中最重要的一段逻辑就是通过API获取好友的状态,如果有其他组件也需要这样的逻辑,就可以把这段逻辑封装成一个自定义Hook。当然,如果你用 HoC 或者 render props 也完全可以实现逻辑封装,只是实现方式有一些差异。通过 Custom Hook 我们可以这样来实现:

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

这里我们定义了一个全新的 Custom Hook,注意,这不是一个“函数组件”,因为很明显我们接收的参数不是 props,并且返回的也不是VDOM,这就是一种全新的类型。这样在我们显示好友状态的组件中,直接调用这个 Custom Hook就行了:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

Custom Hook 背后的原理

可能大家第一反应是 Custom Hook 是不是就是一个简单的函数? 从语法上看,确实就是一个简单的函数,但是从功能上看,细想一下,答案显然不是。因为我们在 useFriendStatus 中调用了 useEffect,前面讲过Hooks的规范,不能在自定义的函数中调用。为什么呢?

React 实现Hook的基本原理,是通过一个数组记录,必须严格保证调用顺序始终不变。在组件中用Hook,每个组件其实都有隔离的state。那么Custom Hook 的状态是不是隔离的呢?下面要划重点了:

  1. Custom Hook 也有一个自己独立的隔离的环境,并不会和调用他的组件共享。
  2. Custom Hook 不仅有隔离的环境,并且每一次调用都会创建隔离的环境。

正因为React会给Custom Hook创建隔离的环境,所以他在运行时,显然和我们调用其他的自定义函数是有区别的,这也是为什么我们的Hook必须以 “use” 开头,不能随便取名字,不然React就不知道函数到底是不是Hook了,毕竟他们在JS语法上没区别。 而且React官方强调的一个概念就是:所有的Hooks其实执行原理都一样,Custom Hook 和 内置的 useState/useEffect 等没有本质区别。

常见问题

Hooks能用在Class Component里面吗? 答案是不能。 Hooks 会完全代替 HoC 和 Render Props吗? 答案是不能。不能完全代替的原因是:Hooks仅仅适用于逻辑上的复用,如果想有渲染上的一些复用,还是后面两个比较合适。比如有一个 List 组件,可以通过 renderItem 来自定渲染,这种情况用 Hooks 就不好处理。

参考资料

fylz1125 commented 5 years ago

大神居然还打dota,好久不打了,大神来上海看TI9么

tms2003 commented 5 years ago

很久不更新了。。。。。坚持才是最难的事吧。