阅读 211
React Hooks基本使用详解

React Hooks基本使用详解

Hooks let you use state and other React features without writing a class

Hooks可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性

一、类组件不足:

  • 状态难以复用(渲染属性Render Props、高阶组件HOC、Mixin、Hooks可以解决这个问题)在hooks出来之前,常见的代码重用方式是 HOC 和render props,这两种方式带来的问题是:你需要解构自己的组件,同时会带来很深的组件嵌套
    • 缺少复用机制
    • 渲染属性Render Props和高阶组件HOC导致层级冗余
      • Render Props
        • å
        • image-20210912214946082
      • HOC(调用时候比render props方便)
        • image-20210912215446423
        • image-20210912215522321
  • 趋向复杂难以维护
    • 生命周期函数经常包含不相干逻辑
    • 相干逻辑被打散在不同生命周期,理解代码逻辑也很吃力
  • this指向困扰
    • 内联函数过度创建新句柄(每次都是新的,会重新触发,导致子组件不停渲染)
    • 类成员函数不能保证this

image-20210912204940022

image-20210912220051867

二、Hooks优势(优化类组件的三大问题):

  • 函数组件无this问题(都在函数内部,没有实例化的概念)
  • 自定义Hook方便复用状态逻辑
  • 副作用的关注点分离(不是发生在数据向视图转化之中,都是在之外的。例如:发起网络请求、访问原型上的DOM元素、写本地持久化缓存、绑定解绑事件都是数据渲染视图之外的。这些一般都是放在生命周期中的。useEffect都是在每次渲染完成之后调用)

image-20210912205030495

image-20210912221047875

研发挑战:

image-20210912192246856

一开始的时候觉得 hooks 非常地神秘,写惯了 class 式的组件后,我们的思维就会定格在那里,生命周期,state,this等的使用。 因此会以 class 编写的模式去写函数式组件,导致我们一次又一次地爬坑,接下来我们就开始我们的实现方式讲解。(提示:以下是都只是一种简单的模拟方法,与实际有一些差别,但是核心思想是一致的)

三、Hooks的API

1.使用 State Hook

演示地址:2.useState class类 Demo:codesandbox.io/s/react-hoo…

import React, { Component } from "react";

class UseClassState extends Component {
  state = {
    count: 0
  };
  render() {
    const { count } = this.state;
    return (
      <button
        type="button"
        onClick={() => {
          this.setState({
            count: count + 1
          });
        }}
      >
        Click({count})
      </button>
    );
  }
}
export default UseClassState;
复制代码

以上代码很好理解,点击按钮让 count 值加 1

接下来我们使用 useState 来实现上述功能。

栗子:1.useState Democodesandbox.io/s/react-hoo…

import React, {useState} from 'react'

function App () {
  //返回两个参数 一个是当前的状态,还有一个是修改当前状态的函数
  const [count, setCount] = useState(0)
  // 动态传入参数 //延迟初始化,提高效率,只会在初始化时候执行一次
  // const [count,setCount] = useState(()=>{return props.defaultCount||0})
  
  onst [name, setName] = useState('小圆脸儿')
  return (
    <button type="button"
      onClick={() => {setCount(count + 1) }}
    >Click({count})</button>
  )
}
复制代码

在这里,useState 就是一个 Hook。通过在函数组件里调用它来给组件添加一些内部 state,React 会在重复渲染时保留这个 state

useState 会返回一对值**:当前状态**和一个让你更新它的函数。你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并。useState 唯一的参数就是初始 state

useState 让代码看起来简洁了,但是我们可能会对组件中,直接调用 useState 返回的状态会有些懵。既然 useState 没有传入任何的环境参数,它怎么知道要返回的的是 count 的呢?而且还是这个组件的 count 不是其它组件的 count

初浅的理解: useState 确实不知道我们要返回的 count,但其实也不需要知道,它只要返回一个变量就行了。数组解构的语法让我们在调用 useState 时可以给 state 变量取不同的名字。

useState 怎么知道要返回当前组件的 state?

因为 JavaScript 是单线程的。在 useState 被调用时,它只能在唯一一个组件的上下文中。

有人可能会问,如果一个组件内有多个 usreState,那 useState 怎么知道哪一次调用返回哪一个 state 呢?

这个就是按照第一次运行的次序来顺序来返回的。

接着上面的例子我们在声明一个 useState:

...
const [count, setScount] = useState(0)
const [name, setName] = useState('小圆脸儿')
...
复制代码

然后我们就可以断定,以后APP组件每次渲染的时候,useState 第一次调用一定是返回 count,第二次调用一定是返回 name

为了防止我们使用 useState 不当,React 提供了一个 ESlint 插件帮助我们检查。

优化点

通过上述我们知道 useState 有个默认值,因为是默认值,所以在不同的渲染周期去传入不同的值是没有意义的,只有第一次传入的才有效。如下所示:

...
const defaultCount = props.defaultCount || 0
const [count, setCount] = useState(defaultCount)
...
复制代码

state 的默认值是基于 props,在 APP 组件每次渲染的时候 const defaultCount = props.defaultCount || 0 都会运行一次,如果它复杂度比较高的话,那么浪费的资料肯定是可观的。

useState 支持传入函数,来延迟初始化:

const [count, setCount] = useState(() => {
  return props.defaultCount || 0
})
复制代码

2.使用Effect Hooks

Effect Hook 可以让你在函数组件中执行副作用操作(绑定事件、发送请求、访问DOM元素)。数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或是"副作用"这个名字,应该都在组件中使用过它们。

副作用的时机

  • Mount 之后 对应 componentDidMount
  • Update 之后 对应 componentDidUpdate
  • Unmount 之前 对应 componentWillUnmount

现在使用 useEffect 就可以覆盖上述的情况。

为什么一个 useEffect 就能涵盖 Mount,Update,Unmount 等场景呢?

useEffect 标准上是在组件每次渲染之后调用,并且会根据自定义状态来决定是否调用还是不调用。

第一次调用就相当于componentDidMount,后面的调用相当于 componentDidUpdateuseEffect 还可以返回另一个回调函数,这个函数的执行时机很重要。作用是清除上一次副作用遗留下来的状态。

image-20210912230254965

比如一个组件在第三次,第五次,第七次渲染后执行了 useEffect 逻辑,那么回调函数就会在第四次,第六次和第八次渲染之前执行。严格来讲,是在前一次的渲染视图清除之前。如果 useEffect 是在第一次调用的,那么它返回的回调函数就只会在组件卸载之前调用了,也就是 componentWillUnmount

image-20210912230856569

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

在线举栗子说明一下:4.class类 useClassEffect Democodesandbox.io/s/react-hoo…

import React, { Component } from "react";
class UseClassEffect extends Component {
  state = {
    count: 0,
    size: {
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    }
  };
  onResize = () => {
    this.setState({
      size: {
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight
      }
    });
  };
  // https://developer.mozilla.org/zh-CN/docs/Web/API/Window/resize_event
  componentDidMount() {
    document.title = this.state.count;
    window.addEventListener("resize", this.onResize, false);
  }
  componentWillMount() {
    window.removeEventListener("resize", this.onResize, false);
  }
  componentDidUpdate() {
    document.title = this.state.count;
  }
  render() {
    const { count, size } = this.state;
    return (
      <button
        type="button"
        onClick={() => {
          this.setState({ count: count + 1 });
        }}
      >
        Click({count}) size: {size.width}x{size.height}
      </button>
    );
  }
}
export default UseClassEffect;

复制代码

对于生命周期我们在复习一遍:

image-20210913001249095

上面主要做的就是网页 title 显示count 值,并监听网页大小的变化。这里用到了componentDidMountcomponentDidUpdate 等副作用,因为第一次挂载我们需要把初始值给 title, 当 count 变化时,把变化后的值给它 title,这样 title 才能实时的更新。

注意,我们需要在两个生命周期函数中编写重复的代码。

这边我们容易出错的地方就是在组件结束之后要记住销毁事件的注册,不然会导致资源的泄漏。现在我们把 App 组件的副作用用 useEffect 实现。

在线栗子5.useEffect Demo:useEffectHook:codesandbox.io/s/react-hoo…

import React, {useState, useEffect } from "react";
function UseEffectHook(props) {
  const [count, setCount] = useState(0);

  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight
  });
  const onResize = () => {
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    });
  };
  // 第一个useEffect代表生命周期中的componentDidUpdate,
  useEffect(() => {
    document.title = count;
  }); // 不用第二个参数的情况执行几次呢((不传数组意味着每一次执行都会))
  // (只调用一次) 第二个useEffect代表生命周期中的componentDidMount组件更新 和 componentWillMount组件卸载
  useEffect(() => {
    window.addEventListener("resize", onResize, false);
    return () => {
      window.removeEventListener("resize", onResize, false);
    };
  }, []); //第二个参数才是useEffect精髓,并且能优化性能,只有数组的每一项都不变的情况下,useEffect才不会执行
  useEffect(() => {
    console.log("count:", count);
  }, [count]); //第二个参数我们传入 [count], 表示只有 count 的变化时,我才打印 count 值,resize 变化不会打印。

  return (
    <button
      type="button"
      onClick={() => {
        setCount(count + 1);
      }}
    >
      Click({count}) size: {size.width}x{size.height}
    </button>
  );
}
export default UseEffectHook;

复制代码

对于上述代码的第一个 useEffect,相比类组件,Hooks 不在关心是 mount 还是 update。用 useEffect统一在渲染后调用,就完整追踪了 count 的值。

对于第二个 useEffect,我们可以通过返回一个回调函数来注销事件的注册。回调函数在视图被销毁之前触发,销毁的原因有两种:重新渲染和组件卸载

这边有个问题,既然 useEffect 每次渲染后都执行,难道我们每次都要绑定和解绑事件吗?当然是完全不需要,只要使用 useEffect 第二个参数,并传入一个空数组即可。第二个参数是一个可选的数组参数,只有数组的每一项都不变的情况下,useEffect 才不会执行。第一次渲染之后,useEffect 肯定会执行。由于我们传入的空数组,空数组与空数组是相同的,因此 useEffect 只会在第一次执行一次。

这也说明我们把 resize 相关的逻辑放在一起写,不在像类组件那样分散在两个不同的生命周期内。同时我们处理 title 的逻辑与 resize 的逻辑分别在两个 useEffect 内处理,实现关注点分离,不同的事情要分开放。

以上验证了hooks组件相对于类组件编写副作用逻辑类型有两点:

1.提高了代码复用

2.实现了关注点分离

我们在定义一个 useEffect,来看看通过不同参数,第二个参数的不同作用。

 useEffect(() => {
    console.log('count:', count)
  }, [count])//第二个参数我们传入 [count], 表示只有 count 的变化时,我才打印 count 值,resize 变化不会打印。


复制代码

image-20210913002339993

第二个参数的三种形态,undefined,空数组及非空数组,我们都经历过了,但是咱们没有看到过回调函数的执行。

我现在描述一种场景就是在组件中访问 Dom 元素,在 Dom元素上绑定事件,在上述的代码中添加以下代码:

...
 const onClick = () => {
  console.log('click');
 }
// 新增一个 DOM 元素,在新的 useEffect 中监听 span 元素的点击事件。
// useEffect(() => {
//   document.querySelector('#size').addEventListener('click', onClick, // // // false);
// },[])
  useEffect(() => {
    document.querySelector("#size").addEventListener("click", onClick, false);
    return () => {
      document
        .querySelector("#size")
        .removeEventListener("click", onClick, false);
    };
  }, []);
  return (
    <div>
    ...
   {/* 情况一::*/}   
   {/* <span id="size">size: {size.width}x{size.height}</span>*/}
     {/* 情况二:假如我们 span 元素可以被销毁重建,我们看看会发生什么情况,改造一下代码:*/}
     {
        count%2
        ? <span id="size">我是span</span>
        : <p id="size">我是p</p>
      }
      
  </div>
)
复制代码

情况一打印结果:codesandbox.io/s/react-hoo…

image-20210914153129502

情况二打印结果:

image-20210914153816522

可以看出一旦 dom 元素被替换,我们绑定的事件就失效了,所以咱们始终要追踪这个dom 元素的最新状态。

使用 useEffect ,最合适的方式就是使用回调函数来处理了,同时要保证每次渲染后都要重新运行,所以不能给第二次参数设置 [],改造如下:

useEffect(() => {
 document.querySelector('#size').addEventListener('click', onClick, false);
   return () => {
     document.querySelector('#size').removeEventListener('click', onClick, false);
   }
})
复制代码

情况三:

image-20210914153225979

无论是之前的生命周期函数,还是useEffect都是处理副作用的,之前的生命周期函数在命名的时候比较容易理解,但其实都是围绕着组件的渲染和重渲染的;useEffect抽象了一层,通过第二个参数执行的时机与生命周期是等价的,大家需要理解什么样的useEffect参数与什么样的生命周期函数是对应的,差不多也就灵活运用useEffect了。当然参数化的useEffect肯定不止有这点能耐,只要你能精确控制第二个参数,就能节省运行时性能还能写出可维护性很高的代码,无论如何还是建议多练习。

问题:useEffect的第二个参数只要在数组成员都不变的情况下才不会运行副作用,那么如何理解这个不变呢?

3.使用 Context Hooks

我们已经学了useState和useEffect,他俩已经能满足大部分的开发需求了,但是为了开发效率和性能着想,我们接下来还要认识使用率没那么高,但是在函数组件的编写中依然发挥着重要角色的几个hook函数

useContext

这个context就是React高级用法中提到的Context,复习一下Context,Context能够允许数据跨越组件层级直接传递。

使用 Context ,首先顶层先声明 Provier 组件,并声明 value 属性,接着在后代组件中声明 Consumer组件,这个 Consumer 子组件,只能是唯一的一个函数,函数参数即是 Context 的负载。如果有多个 Context , Provider 和 Consumer 任意的顺序嵌套即可。

image-20210913005848115

此外我们还可以针对任意一个 Context 使用 contextType 来简化对这个 Context 负载的获取。但在一个组件中,即使消费多个 Context, contextType 也只能指向其中一个。

image-20210913010105164

在 Hooks 环境中,依旧可以使用 Consumer,但是 ContextType 作为类静态成员肯定是用不了。Hooks 提供了 useContext,不但解决了 Consumer 难用的问题同时也解决了 contextType 只能使用一个 context 的问题。

image-20210913010242138

最基本的Context使用栗子:6.Context Democodesandbox.io/s/react-hoo…

import React, { Component, createContext, useState,useContext } from "react";
const CountContext = createContext();
// 1.使用类形式的栗子:
class Foo extends Component {
  render() {
    return (
      <CountContext.Consumer>
        {(count) => <h1>基本 Context Demo:{count}</h1>}
      </CountContext.Consumer>
    );
  }
}
// 2.接着将 Foo 改成用 contextType 的形式:
class Bar extends Component {
  static contextType = CountContext;

  render() {
    const count = this.context;

    return <h1>使用contextType Context Demo:{count}</h1>;
  }
}
// 3.接着使用 useContext 形式:
// 在函数组件中,useContext可不是紧紧可以获取一个Context,从语法上看对Context的数量完全没有限制;解决了 Consumer 难用的问题同时也解决了 contextType 只能使用一个 context 的问题。
function Counter() {
  const count = useContext(CountContext);

  return <h1>在函数组件中,useContext demo:{count}</h1>;
}
function ContextDemo(props) {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button
        type="button"
        onClick={() => {
          setCount(count + 1);
        }}
      >
        点击({count})
      </button>
      <CountContext.Provider value={count}>
        <Foo />
        <Bar />
        <Counter/>
      </CountContext.Provider>
    </div>
  );
}
export default ContextDemo;
复制代码

useContext 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <CountContext.Provider> 的 value prop 决定。

当组件上层最近的 更新时,该 Hook 会触发重渲染,并使用最新传递给 CountContext provider 的 context value 值。

别忘记 useContext 的参数必须是 context 对象本身:

  • 正确:useContext(MyContext)
  • 错误:useContext(MyContext.Consumer)
  • 错误:useContext(MyContext.Provider) 调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化。

4.使用 Memo /CallBack Hooks

useCallback是useMemo的变种。

回顾:meno 用来优化函数组件重渲染的行为,当传入属性值都不变的情况下,就不会触发组件的重渲染,否则就会触发组件重渲染。

image-20210913013025135image-20210913013149740image-20210913013149740

useMemo 与 memo

image-20210913013342998

memo针对的是一个组件的渲染是否重复执行,而 useMemo 定义的是一段函数逻辑是否重复执行,本质都是利用同样的算法来判定依赖是否发生改变,进而决定是否触发特定逻辑。有很多这样的逻辑输入输出是对等的,相同的输入一定产生相同的输出,数学上称之为幂等,useMemo就可以减少重复的重复计算减少资源浪费。所以严格来讲不适用memo或者useMemo不应该会导致你的业务逻辑发生变化,换句话说memo和useMemo仅仅用来做性能优化使用。

memo和useMemo紧紧用来性能优化使用。

举个栗子:7.useMemo Democodesandbox.io/s/react-hoo…

import React, { useState, useMemo } from "react";
function Foo(props) {
  return <h1>{props.count}</h1>;
}

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

  const double = useMemo(() => {
    return count * 2;
  }, [count]);

  return (
    <div>
      <button
        type="button"
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click ({count}) double : ({double})
      </button>

      <Foo count={count} />
    </div>
  );
}
export default UseMemoHook;

复制代码

如上所示, useMemo 语法与 useEffect 是一致的。第一个参数是需要执行的逻辑函数,第二个参数是这个逻辑依赖输入变量组成的数组,如果不传第二个参数,这 useMemo 的逻辑每次就会运行, useMemo 本身的意义就不存在了,所以需要传入参数。所以传入空数组就只会运行一次,策略与 useEffect 是一样的,但有一点比较大的差异就是调用时机, useEffect 执行的是副作用,所以一定是渲染之后才执行,但 useMemo 是需要返回值的,而返回值可以直接参与渲染,因此 useMemo 是在渲染期间完成的,有这样一前一后的区别。

稍微改动一下,count===3,则参数就变成了布尔值Boolean

const double = useMemo(() => {
    return count * 2;
  }, [count === 3]);
复制代码

codesandbox.io/s/react-hoo…

现在能断定, count 在等于 3 之前,由于这个条件一直保存 false 不变,double 不会重新计算,所以一直是 0,当 count 等于 3, double 重新计算为 6,当 count 大于 3, double 在重新计算,变成 8,然后就一直保存 8 不变。所以只要找到你真正依赖了哪些参数,就能尽可能的避免没有必要的计算。

useMemo也可以依赖另外一个useMemo,比如:

//useMemo也可以依赖另外一个useMemo,比如:
  const half = useMemo(()=>{
    return double/4
  },[double])
复制代码

但是一定不要循环依赖,会把浏览器搞崩溃的

记住,传入的 useMemo 的函数会在渲染期间执行,请不要在这个函数内部执行与渲染无关的听任,诸如副作用这类操作属于 useEffect 的适用范畴,而不是 useMemo

你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。

使用Callback Hooks

先重现一个使用 memo 优化子组件重渲染的场景:8.memo Democodesandbox.io/s/react-hoo…

import React, { useState, useMemo, memo } from "react";
const Counter = memo(function Counter(props) {
  console.log("counter");
  return <h1>{props.count}</h1>;
});

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

  //稍微改动一下,count===3,则参数就变成了布尔值Boolean
  const double = useMemo(() => {
    return count * 2;
  }, [count === 3]);
  //useMemo也可以依赖另外一个useMemo,比如:
  const half = useMemo(() => {
    return double / 4;
  }, [double]);
  return (
    <div>
      <button
        type="button"
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click ({count}) double : ({double}) half:({half})
      </button>

      <Counter count={double} />
    </div>
  );
}
export default UseMemoHook;
复制代码

使用 memo 包裹 Counter 组件,这样只有当 double 变化时, Counter 组件才会重新渲染,执行里面的 log,运行结果请看8.memo Democodesandbox.io/s/react-hoo…

现在在给 Counter 中的 h1 添加一个 click 事件:

const Counter = memo(function Counter(props) {
  console.log("counter");
  return <h1 onClick={props.onClick}>{props.count}</h1>;
});
复制代码

然后在 UseMemoHook2 组件中声明 onClick 并传给 Counter组件:

function UseMemoHook2(props) {
  ...
  // 优化前
  const onClick=()=>{
    console.log('onClick')
  }
  return (
    <div>
      ...
      <Counter count={double} onClick={onClick} />
    </div>
  );
}
export default UseMemoHook2;
复制代码

结果请看8.memo Demo 优化前片段

image-20210914203606205

可以看出,每次点击,不管 Counter 是否有变化, Counter 组件都会被渲染。那就说明每次UseMemoHook2 重新渲染之后, onClick 句柄的变化,导致 Counter 也被连带重新渲染了。 count 经常变化可以理解,但是 onClick就不应该经常变化了,毕竟只是一个函数而已,所以我们要想办法让 onClick 句柄不变化。

想想我们上面讲的 useMemo,可以这样来优化 onClick:

// 优化后
   const onClick = useMemo(() => {
     return ()=>{
       console.log("onClick")
      } 
   },[])
复制代码

由于我们传给 useMemo 的第二个参数是一个空数组,那么整个逻辑就只会运行一次,理论上我们返回的 onClick 就只有一个句柄。

运行效果:请看8.memo Demo 优化后片段

现在我们把 useCallback 来实现上页 useMemo 的逻辑。

// 优化后
   const onClick = useCallback(() => {
       console.log("onClick")
   },[])
复制代码

如果 useMemo 返回的是一个函数,那么可以直接使用 useCallback 来省略顶层的函数。

useCallback(fn,deps)=useMemo(()=>fn,deps)useCallback(fn,deps)=useMemo(()=>fn,deps)

大家可能有一个疑问, useCallback 这几行代码明明每次组件渲染都会创建新的函数,它怎么就优化性能了呢?

注意,大家不要误会,使用 useCallback 确实不能阻止创建新的函数,但这个函数不一定会被返回,换句话说很可能这个新创建的函数就被抛弃不用了。 useCallback解决的是:传入子组件的函数参数过度变化,导致子组件过度渲染的问题,这里一定要理解好,不要对useCallback有误解。

上述我们这个onClick函数什么都不依赖,因此useCallback的第二个参数才是空数组,也就是让useCallback的逻辑只运行一次,在实际业务中不会这么简单,至少也要更新一下状态,举个栗子:

function useMemo(props) {
...
  const [clickCount, setClickCount] = useState(0);

  const onClick = useCallback(() => {
    console.log("Click");

    setClickCount(clickCount + 1);
  }, [clickCount, setClickCount]);//setClickCount不需要写,因为 React 能保证 setState 每次返回的都是同个句柄
  ...
}
复制代码

在 UseMemoHook2 组件中在声明一个 useState,然后在 onClick 中调用 setClickCount,此时 onClick 依赖 clickCount, setClickCount

其实这里的 setClickCount 是不需要写的,因为 React 能保证 setState 每次返回的都是同个句柄。不信,可以看下官方文档 :

注意

React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffectuseCallback 的依赖列表中省略 dispatch

这里的场景,除了直接使用 setClickCount+1 赋值以外, 还有一种方式甚至连 clickCount都不用依赖。 setState 除了传入对应的 state 最新值以外,还可以传入一个函数,函数的参数即这个 state的当前值,返回就是要更新的值:

// 传入一个函数,函数的参数即这个 `state`的当前值,返回就是要更新的值
  const onClick = useCallback(() => {
    console.log("Click");
    setClickCount((clickCount) => clickCount + 1);
  }, []);
复制代码

useMemo、useCallback小结

memo 根据属性来决定是否重新渲染组件一样, useMemo 可以根据指定的依赖来决定一段函数逻辑是否重新执行,从而优化性能。

如果 useMemo 的返回值是函数的话,那么就可以简写成 useCallback 的方式,只是简写而已,实际并没有区别。

需要特别注意的是,当依赖变化时,我们能断定 useMemo 一定重新执行。但是注意,即使依赖不变化我们不能假定它就一定不会重新执行,也就是说,它也可能重新执行,就是考虑内存优化的结果。总之,useMemo和useCallback是用来锦上添花的优化手段,不可以过度依赖他是否触发重新渲染,因为React没有给我们打包票说一定重新执行或者一定不重新执行,useMemo使用场景很多,特别是useCallback传递给useCallback子组件

我们可以把 useMemo, useCallback 当做一个锦上添花优化手段,不可以过度依赖它是否重新渲染,因为 React 目前没有打包票说一定执行或者一定不执行。

5.使用 Ref Hooks

image-20210915145137527

类组件中使用 Ref 一般有:

  • String Ref
  • Callback Ref
  • CreateRef

上述在函数组件中没有办法使用它们,取而代之的是 useRef Hooks。

useRef 主要有两个使用场景:

  • 获取子组件或者 DOM 节点的句柄
  • 渲染周期之间的共享数据的存储

大家可能会想到 state 也可跨越渲染周期保存,但是 state 的赋值会触发重渲染,但是 ref 不会,从这点看 ref 更像是类属性中的普通成员。

粟例说明一下:使用useRef获取子组件或者 DOM 节点的句柄:9.useRef Democodesandbox.io/s/react-hoo…

import React, {
  PureComponent,
  useState,
  useMemo,
  useCallback,
  useRef
} from "react";
// const Counter = memo(function Counter(props) {
//   console.log("counter");
//   return <h1 onClick={props.onClick}>{props.count}</h1>;
// });
class Counter extends PureComponent {
  speak() {
    console.log(`now counter is:${this.props.count}`);
  }
  render() {
    const { props } = this;
    return <h1 onClick={props.onClick}>{props.count}</h1>;
  }
}

function UseRefHook(props) {
  const [count, setCount] = useState(0);
  // 依赖多个数据变化的时候
  const [clickCount, setClickCount] = useState(0);
  const counterRef = useRef();

  //稍微改动一下,count===3,则参数就变成了布尔值Boolean
  const double = useMemo(() => {
    return count * 2;
  }, [count === 3]);
  const onClick = useCallback(() => {
    console.log("Click");
    setClickCount((clickCount) => clickCount + 1);
    console.log(counterRef.current);
    counterRef.current.speak();
  }, [counterRef]);
  return (
    <div>
      <button
        type="button"
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click ({count}) double : ({double})
      </button>
      <Counter ref={counterRef} count={double} onClick={onClick} />
    </div>
  );
}
export default UseRefHook;

复制代码

下面我们来尝试一下,不用ref来保存DOM或者组件,而是保存一个普通变量:

粟例说明一下:同步渲染周期之间的共享数据的存储

假设我有这样一个场景,组件一挂载就让count状态每秒钟自动+1,当count>=10以后就不再自动增加,显然这是一个副作用,首先我们需要引入useEffect,在UseRefHook中我们需要定义两个副作用:第一个仅作用一次,用来启动定时器;第二个始终作用用来判断count的值是否满足>=10的条件;

let it;
// 第一个ueeEffect:仅作用一次
useEffect(() => {
    it = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
  }, []);
// 第二个ueeEffect始终作用用来判断count的值是否满足>=10的条件
  useEffect(() => {
    if (count >= 10) {
      clearInterval(it);
    }
  });
复制代码

image-20210915155402209

然而结果到10以后并没有停止增加,为什么?显然就是因为在clearInterval的时候,定时器的句柄it这个变量已经不是setInterval的赋值了,每次UseRefHook重渲染都会重置他,那我们把it放在state中么?用useState声明能解决么?但是it并没有参与渲染,而且弄不好在副作用里面更新或导致死循环,那么现在useRef就派上用场了:

let it = useRef();
useEffect(() => {
    it.current = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
  }, []);
  useEffect(() => {
    if (count >= 10) {
      clearInterval(it.current);
    }
  });
复制代码

使用 useRef 来创建一个 it, 当 setInterval 返回的结果赋值给 itcurrent 属性。

image-20210915160013664

这回效果到10停止了更新,ref帮我们解决了这个问题,是不是很像类属性成员,如果遇见

需要访问上一次渲染时候的数据,甚至是state,就把他们同步到ref中,下一次渲染就能够正确的获取到了。

两种ref的使用场景都讲过了:

useRef 主要有两个使用场景:

  • 获取子组件或者 DOM 节点的句柄
  • 渲染周期之间的共享数据的存储

你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以 <divref={myRef}/> 形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。

然而, useRef()ref 属性更有用**。它可以很方便地保存任何可变值**,其类似于在 class 中使用实例字段的方式。

这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current:...} 对象的唯一区别是, useRef 会在每次渲染时返回同一个 ref 对象。

请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

在副作用里面如何判定一个元素或者组件在本次渲染和上次渲染之间有过重新创建呢?

6.自定义 Hooks

前面三篇,我们讲到优化类组件的三大问题:

  • 方便复用状态逻辑
  • 副作用的关注点分离
  • 函数组件无 this 问题

对于组件的复用状态没怎么说明,现在使用自定义 Hook 来说明一下。

首先我们把上面的例子用到 count 的逻辑的用自定义 Hook 封装起来:10.自定义Hook Demo:codesandbox.io/s/react-hoo…

import React, { useState, useEffect, useRef } from "react";
function useCount(defaultCount) {
  const [count, setCount] = useState(0);
  let it = useRef();
  useEffect(() => {
    it.current = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
  }, []);
  useEffect(() => {
    if (count >= 10) {
      clearInterval(it.current);
    }
  });

  return [count, setCount];
}

function CustomHook(props) {
  const [count, setCount] = useCount(0);
  return (
    <div>
      <h1>{count}</h1>
    </div>
  );
}
export default CustomHook;

复制代码

image-20210915172334394

可以看出运行效果跟上面是一样的。

定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。我们在函数自定义写法上似乎和编写函数组件没有区别,确实自定义组件与函数组件的最大区别就是输入与输出的区别

再来一个特别的 Hook 加深一下映像。在上述代码不变的条件下,我们在加一个自定义 Hook 内容如下:

function useCounter(count) {
  return <h1>{count}</h1>;
}
function CustomHook(props) {
  const [count, setCount] = useCount(0);
  const Counter = useCounter(count);
  return (
    <div>
      <h2>{Counter}</h2>
    </div>
  );
}
复制代码

我们自定义 useCounter Hook返回的是一个 JSX,运行效果是一样的,所以 Hook 是可以返回 JSX 来参与渲染的,更说明 Hook 与函数组件的相似性。

7.Hooks使用法则

只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useStateuseEffect 调用之间保持 hook 状态的正确。(如果你对此感到好奇,我们在下面会有更深入的解释。)

只在 React 函数中调用 Hook

**不要在普通的 JavaScript 函数中调用 Hook。**你可以:

  • ✅ 在 React 的函数组件中调用 Hook
  • ✅ 在自定义 Hook 中调用其他 Hook (我们将会在下一页 中学习这个。)

遵循此规则,确保组件的状态逻辑在代码中清晰可见。

8.Hooks 使用过程中注意常见问题

官方文章上说的很明白,此处不在赘述:zh-hans.reactjs.org/docs/hooks-…

小结

本文主要是对React Hooks的基本使用的介绍,其中的DEMO链接如下:codesandbox.io/s/react-hoo…

所有demo都被引用在了一个index.js文件中,可根据文章介绍demo序号进行操作演示

image-20210915212403933

参考

  • 慕课《React劲爆新特性Hooks 重构旅游电商网站火车票PWA》
  • react 官网

感谢阅读

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章

关注公众号小圆脸儿,一个专注于web前端基础、工程化、面试的前端公众号

文章分类
前端
文章标签