Hook(钩子)是React v16.8新引入的特性,能以钩子的形式为函数组件附加类组件的状态、生命周期等特性。React的类组件有难以拆分、测试,状态逻辑分散,难以复用等问题,虽然可以通过渲染属性(Render Props)和高阶组件来提取状态逻辑,但会形成层层嵌套,而使用Hook后的函数组件就能避免这些问题。

  Hook本质上是一种特殊的JavaScript函数,名称以use为前缀,在使用它时需要遵循两条规则,如下所列:

  (1)在循环、条件语句或嵌套函数中调用Hook是不允许的,必须在函数的最顶层调用,确保Hook的调用顺序。

  (2)只能在React的函数组件或自定义的Hook中调用Hook。

  这两条规则可以结合后文的分析慢慢体会,接下来会详细讲解几个内置的Hook,并且会介绍如何自定义Hook,文中的示例来源于官网

一、State Hook(状态钩子)

  先来看一个简单的类组件,Btn组件会渲染出一个按钮,每次点击按钮,其文本会加一。

  1. import React from "react";
  2. class Btn extends React.Component {
  3. constructor() {
  4. super();
  5. this.state = {
  6. count: 0
  7. };
  8. this.dot = this.dot.bind(this);
  9. }
  10. dot() {
  11. this.setState({ count: this.state.count + 1 })
  12. }
  13. render() {
  14. return <button onClick={this.dot}>{this.state.count}</button>;
  15. }
  16. }

  然后将Btn组件改成相同功能的函数形式,如下代码所示,没有了构造函数和render()方法,通过useState()为函数组件附加状态。

  1. import { useState } from "react";
  2. function Btn() {
  3. const [count, setCount] = useState(0);
  4. return (<button onClick={() => setCount(count + 1)}>{count}</button>);
  5. }

  useState()是一个钩子函数,它的参数是状态的初始值,返回一个数组,包含两个元素:当前状态和更新状态的函数。通过数组解构的方式声明了一个名为count的状态变量和一个名为setCount的函数,相当于类组件中的this.state.count和this.setState()。在点击事件中读取状态或调用更新状态的函数都不需要this。

  注意,useState()可以被多次调用,React会根据useState()的出现顺序保证状态的独立性,并且与this.setState()不同的是,更新状态是替换而不是合并。

二、Effect Hook(副作用钩子)

  在React组件中有两种常见的副作用:无需清除和需要清除,接下来会逐个讲解。

1)无需清除

  在React更新DOM之后会运行一些无需清除的副作用,例如向服务器请求数据、变更DOM结构、记录日志等。在类组件中,这些副作用常在componentDidMount()和componentDidUpdate()生命周期方法中执行。以上一节的Btn组件为例,在更新计数后,修改页面标题,如下所示(只列出了核心代码)。

  1. class Btn extends React.Component {
  2. componentDidMount() {
  3. document.title = `You clicked ${this.state.count} times`;
  4. }
  5. componentDidUpdate() {
  6. document.title = `You clicked ${this.state.count} times`;
  7. }
  8. }

  注意,两个函数中的代码是重复的,因为很多情况下,在组件挂载和更新时会执行相同的操作,而React并未提供每次渲染之后可回调的函数。

  接下来用useEffect()钩子函数实现相同功能,同样只列出了核心代码,如下代码所示。useEffect()使得相同功能的副作用不用再分散到不同的生命周期中,即按照用途分离副作用。

  1. import { useEffect } from "react";
  2. function Btn() {
  3. useEffect(() => {
  4. document.title = `You clicked ${count} times`;
  5. });
  6. }

  useEffect()可接收两个参数,第一个参数是回调函数,叫做Effect,在每次渲染(包括第一次挂载和后续的DOM更新)之后Effect都会被执行,其中每次接收的Effect都是新的,不用担心状态过期的问题;第二个参数是可选的数组(由Effect的依赖项组成),用于控制Effect的执行,而是否执行Effect将取决于数组中的元素是否发生了变化,例如将count变量作为数组的元素(如下代码所示),当count的值与重新渲染后的count的值一样时,React会忽略这个Effect,优化性能。

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

  当把一个空数组([])传给useEffect()时,Effect只会运行一次,即仅在组件挂载和卸载时运行。由于Effect不依赖state或props中的任意值,因此永远都不需要重复执行。

  useEffect()相当于componentDidMount()、componentDidUpdate()和componentWillUnmount()三个生命周期方法的组合,但与componentDidMount()或componentDidUpdate()不同,使用useEffect()会异步执行副作用,可避免阻塞浏览器更新视图。

2)需要清除

  有些副作用是必须清除的,例如订阅的外部数据源,将其清除后,可防止内存泄露。在类组件中,通常会在componentDidMount()中设置订阅,并在componentWillUnmount()中执行清除。

  假设有一个ChatAPI模块,用于订阅好友的在线状态,如下所示(只有关键部分),其中componentDidMount()和componentWillUnmount()处理的是关联的副作用。

  1. class FriendStatus extends React.Component {
  2. componentDidMount() {
  3. ChatAPI.subscribeToFriendStatus(
  4. this.props.friend.id,
  5. this.handleStatusChange
  6. );
  7. }
  8. componentWillUnmount() {
  9. ChatAPI.unsubscribeFromFriendStatus(
  10. this.props.friend.id,
  11. this.handleStatusChange
  12. );
  13. }
  14. handleStatusChange(status) {
  15. this.setState({
  16. isOnline: status.isOnline
  17. });
  18. }
  19. }

  接下来用函数组件实现相同的功能,同样只有关键部分的代码。由于添加和移除订阅的逻辑有很强的紧密性,因此useEffect()将它们组织在一起。当Effect返回一个函数时,React将在执行清除操作时调用它,如下所示。

  1. function FriendStatus(props) {
  2. useEffect(() => {
  3. function handleStatusChange(status) {
  4. setIsOnline(status.isOnline);
  5. }
  6. ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  7. return () => {
  8. ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  9. };
  10. });
  11. }

  注意,React会在执行当前Effect之前对上一个Effect进行清除,也就是说,副作用并不仅在组件卸载时被执行。

三、自定义Hook

  自定义的Hook用于保存组件中可复用的逻辑,它的参数和返回值都没有特殊要求,类似于一个普通的函数,但为了遵循Hook的规则,其名称必须以use开头。接下来将之前的FriendStatus组件中订阅好友在线状态的逻辑抽离到自定义的useFriendStatus()中,其参数为friendID,返回值为好友当前的状态,如下所示。

  1. function useFriendStatus(friendID) {
  2. const [isOnline, setIsOnline] = useState(null);
  3. useEffect(() => {
  4. function handleStatusChange(status) {
  5. setIsOnline(status.isOnline);
  6. }
  7. ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
  8. return () => {
  9. ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
  10. };
  11. });
  12. return isOnline;
  13. }

  在FriendStatus组件中调用自定义的Hook,其内部逻辑将变得非常简洁,如下所示。

  1. function FriendStatus(props) {
  2. const isOnline = useFriendStatus(props.friend.id);
  3. if (isOnline === null) {
  4. return 'Loading...';
  5. }
  6. return isOnline ? 'Online' : 'Offline';
  7. }

四、其它Hook

  除了上面所讲解的两个内置Hook,React还提供了其它功能的Hook,例如useContext()、useCallback()、useMemo()、useLayoutEffect()等,具体可参考官方的API索引

1)useContext()

  接收一个由React.createContext()创建的Context对象,返回该Context的当前值(即要传送的数据)。调用了useContext()的组件会在Context值发生变化时重新渲染。

2)useCallback()

  包含两个参数,第一个是回调函数,第二个是依赖项数组,返回回调函数的记忆版本。当某个依赖项发生改变时,会更新回调函数。注意,依赖项数组不会作为参数传给回调函数。

3)useMemo()

  包含回调函数和依赖项数组两个参数,回调函数的返回值就是useMemo()的返回值,它会被缓存,并且仅在某个依赖项发生改变时才重新计算它。之前的useCallback(fn, deps)相当于useMemo(() => fn, deps)。

4)useLayoutEffect()

  函数签名与useEffect()相同,但调用时机不同,它会在所有的DOM更新之后同步调用Effect,也就是在浏览器更新视图之前调用Effect。

React躬行记(15)——React Hooks的更多相关文章

  1. React躬行记(3)——组件

    组件(Component)由若干个React元素组成,包含属性.状态和生命周期等部分,满足独立.可复用.高内聚和低耦合等设计原则,每个React应用程序都是由一个个的组件搭建而成,即组成React应用 ...

  2. React躬行记(16)——React源码分析

    React可大致分为三部分:Core.Reconciler和Renderer,在阅读源码之前,首先需要搭建测试环境,为了方便起见,本文直接采用了网友搭建好的环境,React版本是16.8.6,与最新版 ...

  3. React躬行记(13)——React Router

    在网络工程中,路由能保证信息从源地址传输到正确地目的地址,避免在互联网中迷失方向.而前端应用中的路由,其功能与之类似,也是保证信息的准确性,只不过来源变成URL,目的地变成HTML页面. 在传统的前端 ...

  4. React躬行记(5)——React和DOM

    React实现了一套与浏览器无关的DOM系统,包括元素渲染.节点查询.事件处理等机制. 一.ReactDOM 自React v0.14开始,官方将与DOM相关的操作从React中剥离,组成单独的rea ...

  5. React躬行记(8)——样式

    由于React推崇组件模式,因此会要求HTML.CSS和JavaScript混合在一起,虽然这与过去的关注点分离正好相反,但是更有利于组件之间的隔离.React已将HTML用JSX封装,而对CSS只进 ...

  6. React躬行记(2)——JSX

    JSX既不是字符串,也不是HTML,而是一种类似XML,用于描述用户界面的JavaScript扩展语法,如下代码所示.在使用JSX时,为了避免自动插入分号时出现问题,推荐在其最外层用圆括号包裹,并且必 ...

  7. React躬行记(4)——生命周期

    组件的生命周期(Life Cycle)包含三个阶段:挂载(Mounting).更新(Updating)和卸载(Unmounting),在每个阶段都会有相应的回调方法(也叫钩子)可供选择,从而能更好的控 ...

  8. React躬行记(6)——事件

    React在原生事件的基础上,重新设计了一套跨浏览器的合成事件(SyntheticEvent),在事件传播.注册方式.事件对象等多个方面都做了特别的处理. 一.注册事件 合成事件采用声明式的注册方式, ...

  9. React躬行记(7)——表单

    表单元素是一类拥有内部状态的元素,这些状态由其自身维护,通过这类元素可让用户与Web应用进行交互.HTML中的表单元素(例如<input>.<select>和<radio ...

随机推荐

  1. [Next] Next.js+Nest.js实现GitHub第三方登录

    GitHub OAuth 第三方登录 第三方登录的关键知识点就是 OAuth2.0 . 第三方登录,实质就是 OAuth 授权 . OAuth 是一个开放标准,允许用户让第三方应用访问某一个网站的资源 ...

  2. Netty 入门,这一篇文章就够了

    Netty是Java领域有名的开源网络库,特点是高性能和高扩展性,因此很多流行的框架都是基于它来构建的,比如我们熟知的Dubbo.Rocketmq.Hadoop等,针对高性能RPC,一般都是基于Net ...

  3. 还不会用FindBugs?你的代码质量很可能令人堪忧

    前言 项目中代码质量,往往需要比较有经验的程序员的审查来保证.但是随着项目越来越大,代码审查会变得越来越复杂,需要耗费越来越多的人力.而且程序员的经验和精力都是有限的,能审查出问题必定有限.而在对代码 ...

  4. [考试反思]0819NOIP模拟测试26:荒芜

    这么正式的考试,明天应该就是最后一次了吧 然而..今天,我仍然没能抓住机会 RNBrank1:.skyh还是稳.外校gmk拿走第三. 四五六名都是63-64.第七50.第八39.我和三个并列的是第九. ...

  5. 如何在Vue项目中使用Typescript

    0.前言 本快速入门指南将会教你如何在Vue项目中使用TypeScript进行开发.本指南非常灵活,它可以将TypeScript集成到现有的Vue项目中任何一个阶段. 1.初始化项目 首先,创建一个新 ...

  6. CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths——dsu on tree

    题目描述 一棵根为1 的树,每条边上有一个字符(a-v共22种). 一条简单路径被称为Dokhtar-kosh当且仅当路径上的字符经过重新排序后可以变成一个回文串. 求每个子树中最长的Dokhtar- ...

  7. m99 然而并没有想出来标题!

    这是放假回来的第一次考试,如同往常一样,我每逢放假回来第一次考试就会废掉,这次也不例外 这次不想粘成绩,因为实在是rp没了! 之前的几次都是别人在CE等等被lemon砍分,而我被lemon多测分. 但 ...

  8. 『题解』洛谷P3376 【模板】网络最大流

    Problem Portal Portal1:Luogu Description 如题,给出一个网络图,以及其源点和汇点,求出其网络最大流. Input 第一行包含四个正整数\(N,M,S,T\),分 ...

  9. JVM 运行参数 & 代码监控

    1.Java代码监控 JDK提供java.lang.management包, 其实就是基于JMX技术规范,提供一套完整的MBean,动态获取JVM的运行时数据,达到监控JVM性能的目的. packag ...

  10. [quartusⅡ] 使用quartusⅡ的过程中,遇到过的一些“软件上的问题”

    1.USB blaster的驱动在设备管理器上点“更新驱动软件”,更新不了,说什么哈希值不在指定目录下,如下图, 解决方法是,https://blog.csdn.net/rdgfdd/article/ ...