Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
lihongxun945 opened this issue May 28, 2019 · 2 comments
Open

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

lihongxun945 opened this issue May 28, 2019 · 2 comments

Comments

@lihongxun945
Copy link
Owner

lihongxun945 commented May 28, 2019

什么是Hooks

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

Hooks分为几种:

  • State Hooks,通过 useState 来在函数组件中使用 state,而不必声明一个类
  • Effect Hooks,处理任何能产生副作用的操作,比如数据请求。
  • 还有 useContextuseReducer

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

官方给出的Hooks使用规范:

  • 放在顶层代码,不要放在任何循环、条件、嵌套分支中,原因后面会讲到。
  • 只能从react组件调用Hooks,不要在其他自定义函数中调用Hook

下面我们看看最重要的两个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 的执行时机是在 每一次DOM更新之后进行,也就是其实 useEffect 会记住你传入的方法,然后每一次render执行完之后都会调用。
  • 因为我们每一次都会执行 useEffect,所以其实每一次更新其实我们都是创建了一个新的方法。所以如果有清理操作,每一次更新之前都会进行清理,而不是只在 unmount 的时候进行清理。
  • useEffect 其实是异步执行的,并不会阻塞DOM更新!而传统的通过 componentDidMount 或者 componentDidUpdate 进行副作用的操作其实会阻塞DOM的更新

清理 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 就不好处理。

参考资料

@lihongxun945 lihongxun945 changed the title React Hooks 1 - 初识 State Hook and Effect Hook React Hooks 1 - State Hook , Effect Hook, Custom Hook May 28, 2019
@fylz1125
Copy link

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

@tms2003
Copy link

tms2003 commented Nov 11, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants