Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。

一、简介:

请记住 Hook 是:

  • 完全可选的。 你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
  • 100% 向后兼容的。 Hook 不包含任何破坏性改动。
  • 现在可用。 Hook 已发布于 v16.8.0。
  • 没有计划从 React 中移除 class。

    Hook 不会影响你对 React 概念的理解。 恰恰相反,Hook 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。

    i、组件之间复用状态逻辑很难:

    React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。React 需要为共享状态逻辑提供更好的原生途径。

    你可以 使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。 Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。

    ii、复杂组件变得难以理解:

    组件起初很简单,但是逐渐会被 状态逻辑和副作用 充斥。每个生命周期常常包含一些不相关的逻辑。

    相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。

    在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。

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

    iii、class 是一个障碍:

    理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。

    class 不能很好的压缩,并且会使热重载出现不稳定的情况。

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

    例子:计数器,记录按键次数

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

    useState 唯一的参数就是初始 state 。在上面的例子中,我们的计数器是从零开始的,所以初始 state 就是 0 。值得注意的是,不同于 this.state 这里的 state 不一定要是一个对象 —— 如果你有需要,它也可以是 这个初始 state 参数只有在第一次渲染时会被用到

    我们声明了一个叫 count 的 state 变量,然后把它设为 0 。React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。我们可以通过调用 setCount 来更新当前的 count

    (2)等价的class 示例:

    state 初始值为 { count: 0 } ,当用户点击按钮后,我们通过调用 this.setState() 来增加 state.count

    对比class和hook两个例子:

    i、 在 class 中,我们通过在构造函数中设置 this.state { count: 0 } 来初始化 count state 为 0;

    ii、在函数组件中,我们没有 this ,所以我们不能分配或读取 this.state 。我们直接在组件中调用 useState Hook。

    (const [count, setCount] = useState(0);) 方括号是数组解构

    🌟(3)关于useState使用:

    i、 调用 useState 方法的时候做了什么? -- 它定义一个 “state 变量”。我们的变量叫 count , 但是我们可以叫他任何名字,比如 banana 。这是一种在函数调用时保存变量的方式 —— useState 是一种新方法, 它与 class 里面的 this.state 提供的功能完全相同 。一般来说,在函数退出后变量就就会”消失”, 而 state 中的变量会被 React 保留。

    ii、 useState 需要哪些参数? useState() 方法里面 唯一的参数就是初始 state 。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。在示例中,只需使用数字来记录用户点击次数,所以我们传了 0 作为变量的初始 state。(如果我们想要在 state 中存储 两个不同的变量,只需调用 useState() 两次 即可。)

    iii、 useState 方法的返回值是什么? 返回值为: 当前 state 以及更新 state 的函数 。这就是我们写 const [count, setCount] = useState() 的原因。这与 class 里面 this.state.count this.setState 类似 ,唯一区别就是你需要成对的获取它们。

    (4)为什么叫useState:

    “Create” 可能不是很准确,因为 state 只在组件首次渲染的时候被创建。在下一次重新渲染时, useState 返回给我们当前的 state。否则它就不是 “state”了!这也是 Hook 的名字 总是 use 开头的一个原因。

    🌟(5)如何读取state?

    i、class中: { this.state.count }

    ii、hook中: { count }

    🌟(6)更新 state?

    i、class中,用 this.setState ( { count : this.state.count + 1 } )

    ii、hook中,已经有setCount 和count 变量: setCount( count + 1 )

    (7)为什么react知道这个hook对应哪个组件?

    React 保持对当先渲染中的组件的追踪。

    当你用 useState() 调用一个 Hook 的时候,它会 读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。 这就是多个 useState() 调用会得到各自独立的本地 state 的原因。

    (8)用函数组件和hook 取代clss:

    函数组件,曾经称为“无状态组件”,但现在为他们引入使用React state(hook造成的)。

    三、使用 Effect Hook:(两种常见副作用操作:需要清除的和不需要清除的)

    (1)Effect Hook: { useEffect }

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

    useEffect 就是一个 Effect Hook ,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount componentDidUpdate componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。

    举例1: 上面例子添加功能--更新DOM后设置一个页面标题:(运行“副作用”)

    当你调用 useEffect 时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。 由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。默认情况下,React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候。

    (2)不需清除的effect:

    在 React 更新 DOM 之后运行一些额外的代码。比如发送 网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作 。因为我们在执行完这些操作之后,就可以忽略他们了。

    class中的副作用实现:

    render 函数是不应该有任何副作用的 。我们基本上都 希望在 React 更新 DOM 之后才执行我们的操作 。这就是为什么在 React class 中,我们把副作用操作放到 componentDidMount componentDidUpdate 函数中。

    对比hook和class 在副作用中的区别:

    i、 useEffect 做了什么? -- 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。

    ii、 为什么在组件内部调用 useEffect -- useEffect 放在组件内部让我们 可以在 effect 中直接访问 count state 变量(或其他 props) 。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。 Hook 使用了 JavaScript 的闭包机制 ,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

    iii、 useEffect 会在每次渲染后都执行吗? -- 是的,默认情况下,它在第一次渲染之后 每次更新之后都会执行。你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

    componentDidMount componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕 ,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。

    (3)需要清楚的 effect:

    例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以 防止引起内存泄露

    举例2: (清除副作用)订阅好友的在线状态,并通过取消订阅来清除操作:

    React 会在组件销毁时取消对 ChatAPI 的订阅,然后在后续渲染时重新执行副作用函数。

    通过使用 Hook,你可以把组件内相关的副作用组织在一起(例如创建订阅及取消订阅),而不要把它们拆分到不同的生命周期函数里。

    在class中会这样做:

    componentDidMount componentWillUnmount 之间相互对应。使用生命周期函数迫使我们拆分这些逻辑代码,即使这两部分代码都作用于相同的副作用。

    对比hook和class 的副作用写法:

    为什么要在 effect 中返回一个函数? -- 这是 effect 可选的清除机制 。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。

    React 何时清除 effect? -- React 会 在组件卸载的时候执行清除操作 。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 在执行当前 effect 之前对上一个 effect 进行清除。

    (4)使用Effect 的提示:

    i、使用多个 Effect 实现关注点分离:

    使用 Hook 其中一个目的就是要 解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题

    上面hook,effect事例就是使用多个effect的例子。

    Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。 React 将按照 effect 声明的顺序依次调用组件中的 每一个 effect。

    ii、为什么每次更新的时候都要运行 Effect:

    并不需要特定的代码来处理更新逻辑(class中的 componentDidUpdate ),因为 useEffect 默认 就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理。

    🌟iii、通过跳过 Effect 进行性能优化:

    这是很常见的需求,所以它被内置到了 useEffect 的 Hook API 中。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:

    如果 count 的值是 5 ,而且我们的组件重渲染的时候 count 还是等于 5 ,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的( 5 === 5 ),React 会跳过这个 effect,这就实现了性能的优化。

    对于清除操作的effect(return 即清除)同样:

    如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。

    如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以 传递一个空数组( [] 作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect ,因此会使得额外操作很方便。

  • 只能在函数最外层调用 Hook。 不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。 不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中,我们稍后会学习到。)
  • 同时,我们提供了 linter 插件 来自动执行这些规则。这些规则乍看起来会有一些限制和令人困惑,但是要让 Hook 正常工作,它们至关重要。