React Hook 不彻底指南

前言

React HookReact16.8.0版本以后提出的新增特性,因为以前的项目都不怎么用到React,所以也就匆匆了解一下,最近由于换工做,主要技术栈变为React了,因此须要着重研究一下React的一些特性以更好地应用到项目开发中和更好地进行知识沉淀。javascript

Hook是什么

在解释这个问题以前,能够先看一段代码:css

import React, { useState } from 'react'
function Example() {
    // 声明一个叫 "count" 的 state 变量
    const [count, setCount] = useState(0)
    // 与 componentDidMount and componentDidUpdate效果相似
    useEffect(() => {
        // Update the document title using the browser API
        document.title = `You clicked ${count} times`
    })

    return (
        <div>
        <p>You clicked {count} times</p>
        <button onClick={() => setCount(count + 1)}>
            Click me
        </button>
        </div>
    );
}

Hook是一个特殊的函数,它可让你“钩入”React的特性。例如,useState是容许你在React函数组件中添加stateHook。若是你在编写函数组件并意识到须要向其添加一些state,之前的作法是必须将其它转化为class。如今你能够在现有的函数组件中使用Hook;又例如useEffect Hook能够告诉React组件须要在渲染后执行某些操做,React会保存你传递的函数(咱们将它称之为 “effect”),而且在执行 DOM 更新以后调用它,能够把它看作componentDidMountcomponentDidUpdatecomponentWillUnmount这三个函数的组合。html

官方解释:Hook 是 React 16.8 的新增特性。它可让你在不编写 class 的状况下使用 state 以及其余的 React 特性。

为何要提出React Hook

在组件之间复用状态逻辑很难

React没有提供将可复用性行为“附加”到组件的途径(例如,把组件链接到 store)。若是你使用过 React一段时间,你也许会熟悉一些解决此类问题的方案,好比 render props高阶组件。可是这类方案须要从新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。若是你在 React DevTools中观察过 React应用,你会发现由 providers,consumers,高阶组件,render props等其余抽象层组成的组件会造成“嵌套地狱”。尽管咱们能够在 DevTools过滤掉它们,但这说明了一个更深层次的问题: React须要为共享状态逻辑提供更好的原生途径。

Hook能够在无需修改组件结构的状况下复用状态逻辑,这使得在组件间或社区内共享Hook变得更便捷java

复杂组件变得难以理解

咱们常常维护一些组件,组件起初很简单,但随着业务复杂度的提高,组件逐渐会变得比较复杂,使得每一个生命周期经常包含一些不相关的逻辑。例如,组件经常在componentDidMountcomponentDidUpdate中获取数据。可是,同一个componentDidMount中可能也包含不少其它的逻辑,如设置事件监听,而以后需在componentWillUnmount中清除。相互关联且须要对照修改的代码被进行了拆分,而彻底不相关的代码却在同一个方法中组合在一块儿,如此很容易产生bug,而且致使逻辑不一致,维护起来也会显得比较吃力。react

为了解决这个问题,Hook将组件中相互关联的部分拆分红更小的函数(好比设置订阅或请求数据),而并不是强制按照生命周期划分。你还可使用reducer来管理组件的内部状态,使其更加可预测。npm

难以理解的 class

引用官方的话:编程

除了代码复用和代码管理会遇到困难外,咱们还发现 class是学习 React的一大屏障。你必须去理解 JavaScriptthis的工做方式,这与其余语言存在巨大差别。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码很是冗余。你们能够很好地理解 propsstate和自顶向下的数据流,但对 class却束手无策。即使在有经验的 React开发者之间,对于函数组件与 class组件的差别也存在分歧,甚至还要区分两种组件的使用场景。

另外,React已经发布五年了,咱们但愿它能在下一个五年也与时俱进。就像SvelteAngularGlimmer等其它的库展现的那样,组件预编译会带来巨大的潜力。尤为是在它不局限于模板的时候。最近,咱们一直在使用Prepack来试验component folding,也取得了初步成效。可是咱们发现使用class组件会无心中鼓励开发者使用一些让优化措施无效的方案。class也给目前的工具带来了一些问题。例如,class不能很好的压缩,而且会使热重载出现不稳定的状况。所以,咱们想提供一个使代码更易于优化的APIsegmentfault

Hook可以在非class的状况下使用更多的React特性。 其实, React组件一直更像是函数。而Hook则拥抱了函数,同时也没有牺牲React的精神原则。Hook提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。数组

最重要的是,Hook是向下兼容的,它和现有代码能够同时工做,你能够渐进式地使用他们,不用急着迁移到Hook浏览器

内置经常使用HOOK概览

React中内置的Hook API

  • 基础Hook

    • useState
    • useEffect
    • useContext
  • 额外的Hook

    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeHandle
    • useLayoutEffect
    • useDebugValue

State Hook

能够看下面的代码:

import React, { useState } from "react";
import "./styles.css";

export default function App() {
  const [count, setCount] = useState(0);
  return (
    <div className="App">
      <h1>这是一个示例</h1>
      <div>点击了{count}次</div>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        点击
      </button>
      <button
        onClick={() => {
          setCount(0);
        }}
      >
        清除
      </button>
    </div>
  );
}

上述代码中,useState就是一个Hook。经过在函数组件里调用它来给组件添加一些内部stateReact会在重复渲染时保留这个stateuseState会返回一对值:当前状态和一个让你更新它的函数,你能够在事件处理函数中或其余一些地方调用这个函数。它相似class组件的this.setState,可是它不会把新的state和旧的state进行合并。

useState惟一的参数就是初始state。在上面的例子中,咱们的计数器是从零开始的,因此初始state就是0。值得注意的是,不一样于this.state,这里的state不必定要是一个对象,但若是你有须要,它也能够是。这个初始state参数只有在第一次渲染时会被用到。

你也能够在函数组件中屡次使用state Hook

调用 useState 方法的时候作了什么?

它定义一个 “state 变量”。在上面的示例中该变量叫count, 但它能够是任意的变量名,好比banana。这是一种在函数调用时保存变量的方式,useState是一种新方法,它与class里面的this.state提供的功能彻底相同。通常来讲,在函数退出后变量就会”消失”,而state中的变量会被React保留。

useState 须要哪些参数?

useState()方法里面惟一的参数就是初始state。不一样于class的是,咱们能够按照须要使用数字或字符串对其进行赋值,而不必定是对象。在示例中,只需使用数字来记录用户点击次数,因此咱们传了0 做为变量的初始state。(若是咱们想要在state中存储两个不一样的变量,只需调用useState()两次便可。)

useState 方法的返回值是什么?

返回值为:当前state以及更新state的函数。这就是咱们写 const [count, setCount] = useState() 的缘由。这与class里面this.state.countthis.setState相似,惟一区别就是你须要成对的获取它们。

继续深刻

每一次渲染都有它本身的props和state

咱们直接看代码来方便理解:

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

<p>You clicked {count} times</p>看到这段代码,有必定经验的人可能会想,这其中的原理是否是经过watcher,或者是data binding或者是proxy来实现的呢?都不是,count仅仅只是一个数字类型的变量而已,不是上述中的任何一个,就像下面的普通的变量赋值同样:

const count = 42;
// ...
<p>You clicked {count} times</p>

组件在第一次渲染的时候,从useState()拿到count的初始值0。当咱们调用setCount(1)React会再次渲染组件,这一次count1。就如同下面示例的同样:

// During first render
function Counter() {
  const count = 0; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>
  // ...
}

// After a click, our function is called again
function Counter() {
  const count = 1; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>
  // ...
}

// After another click, our function is called again
function Counter() {
  const count = 2; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>
  // ...
}

当咱们更新状态的时候,React会从新渲染组件。每一次渲染都能拿到独立的count状态,这个状态值是函数中的一个常量

因此下面的这行代码没有作任何特殊的数据绑定:

<p>You clicked {count} times</p>

它仅仅只是在渲染输出中插入了count这个数字。这个数字由React提供。当setCount的时候,React会带着一个不一样的count值再次调用组件。而后,React会更新DOM以保持和渲染输出一致。

这里关键的点在于任意一次渲染中的count常量都不会随着时间改变。渲染输出会变是由于咱们的组件被一次次调用,而每一次调用引发的渲染中,它包含的count值独立于其余渲染。

Effect Hook

什么是反作用?React官网是这么定义的:

你以前可能已经在 React组件中执行过数据获取、订阅或者手动修改过 DOM。咱们统一把这些操做称为“反作用”,或者简称为“做用”。

useEffect就是一个Effect Hook,给函数组件增长了操做反作用的能力。它跟class组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount具备相同的用途,只不过被合并成了一个API

例如,下面这个组件在React更新DOM后会设置一个页面标题:

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

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

  // 至关于 componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 使用浏览器的 API 更新页面标题
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

当你调用useEffect时,就是在告诉React在完成对DOM的更改后运行你的“反作用”函数。因为反作用函数是在组件内声明的,因此它们能够访问到组件的propsstate。默认状况下,React会在每次渲染后调用反作用函数,包括第一次渲染的时候。

反作用函数还能够经过返回一个函数来指定如何“清除”反作用。例如,在下面的组件中使用反作用函数来订阅好友的在线状态,并经过取消订阅来进行清除操做:

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

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

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);

    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

在这个示例中,React会在组件销毁时取消对ChatAPI的订阅,而后在后续渲染时从新执行反作用函数。

useState同样,你能够在组件中屡次使用useEffect。经过使用Hook,你能够把组件内相关的反作用组织在一块儿(例如建立订阅及取消订阅),而不要把它们拆分到不一样的生命周期函数里。这样就有利于你对代码的维护。也再一次说明了React官方为何会使用Hook

深刻

每次渲染都有它本身的Effects

再次看到官网文档中的例子:

function Counter() {
  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是如何读取到最新的count状态值的呢?

也许,是某种data bindingwatching机制使得count可以在effect函数内更新?也或许count是一个可变的值,React会在咱们组件内部修改它以使咱们的effect函数总能拿到最新的值?

都不是。

咱们已经知道count是某个特定渲染中的常量。事件处理函数“看到”的是属于它那次特定渲染中的count状态值。对于effects也一样如此:

并非count的值在“不变”的effect中发生了改变,而是effect函数自己在每一次渲染中都不相同。

每个effect版本“看到”的count值都来自于它属于的那次渲染:

// During first render
function Counter() {
  // ...
  useEffect(
    // Effect function from first render
    () => {
      document.title = `You clicked ${0} times`;
    }
  );
  // ...
}

// After a click, our function is called again
function Counter() {
  // ...
  useEffect(
    // Effect function from second render
    () => {
      document.title = `You clicked ${1} times`;
    }
  );
  // ...
}

// After another click, our function is called again
function Counter() {
  // ...
  useEffect(
    // Effect function from third render
    () => {
      document.title = `You clicked ${2} times`;
    }
  );
  // ..
}

React会记住你提供的effect函数,而且会在每次更改做用于DOM并让浏览器绘制屏幕后去调用它。

因此虽然咱们说的是一个effect(这里指更新documenttitle),但其实每次渲染都是一个不一样的函数 — 而且每一个effect函数“看到”的propsstate都来自于它属于的那次特定渲染。

Hook使用规则

Hook 就是 JavaScript 函数,可是使用它们会有两个额外的规则:

  • 只能在函数最外层调用 Hook不要在循环、条件判断或者子函数中调用。
  • 只能在React的函数中调用调用Hook。不要在普通的JavaScript函数中调用Hook,你能够:

    • React的函数组件中调用Hook
    • 在自定义Hook中调用其余Hook

为了更好地执行这个规则,react提供了eslint插件帮助你去检测和强制执行上述规则:eslint-plugin-react-hooks

为何是这样的规则呢?

这要从React内部执行Hook的机制提及:

React函数组件中,可使用多个useState或者useEffect,那么React怎么知道哪一个state对应哪一个useState?答案是React靠的是Hook调用的顺序。只要Hook的调用顺序在屡次渲染之间保持一致,React就能正确地将内部state和对应的Hook进行关联。若是咱们将一个Hook调用放在了条件语句中,就有可能会扰乱Hook的调用的顺序,致使内部错误的对应state和useState,进而致使bug的产生。

自定义Hook

自定义Hook是一个函数,其名称以 “use” 开头,函数内部能够调用其余的Hook
当咱们想在两个函数之间共享逻辑时,咱们会把它提取到第三个函数中。而组件和Hook都是函数,因此也一样适用这种方式。

能够直接看下面的例子:

import { useState, useEffect } from 'react';

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

React组件不一样的是,自定义Hook不须要具备特殊的标识。咱们能够自由的决定它的参数是什么,以及它应该返回什么。换句话说,它就像一个正常的函数,可是它的名字应该始终以use开头,这样能够一眼看出其符合Hook的规则。

此处useFriendStatusHook目的是订阅某个好友的在线状态。这就是咱们须要将friendID做为参数,并返回这位好友的在线状态的缘由。

使用自定义Hook

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

自定义Hook是一种天然遵循Hook设计的约定,而并非React的特性。

自定义Hook必须以 “use” 开头吗?必须如此。这个约定很是重要。不遵循的话,因为没法判断某个函数是否包含对其内部Hook的调用,React将没法自动检查你的Hook是否违反了Hook的规则。

在两个组件中使用相同的Hook会共享state?不会。自定义Hook是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),因此每次使用自定义Hook时,其中的全部state和反作用都是彻底隔离的。

自定义Hook如何获取独立的state?每次调用Hook,它都会获取独立的state。因为咱们直接调用了useFriendStatus,从React的角度来看,咱们的组件只是调用了useStateuseEffect。正如咱们在以前章节中了解到的同样,咱们能够在一个组件中屡次调用useStateuseEffect,它们是彻底独立的。

总结

零零碎碎写了这么多,做为一个入门参考,看了这篇文章,应该会对React Hook有了大体的了解,文章中也有深刻其内部机制剖析的地方,可是仅仅对state和effect部分作了简要的深刻,而实际上React Hook中间还有不少的点值得去深刻推敲,因为实际项目工做中用到的很少,所以也无法抓住某个坑作深刻的研究,准备后续认真研读一下react的源码,对其内部机制作深刻的研究。好好静下心来沉淀。

参考文档

相关文章
相关标签/搜索