看完这篇,你也能把 React Hooks 玩出花

原创不易,但愿能关注下咱们,再顺手点个赞~~

本文首发于政采云前端团队博客: 看完这篇,你也能把 React Hooks 玩出花javascript

摩卡.png

本文中出现的部分名称映射:前端

函数式组件 => Function Componentjava

类组件 => Class Componentreact

工具函数 => Util Function数组

钩子 => React Hook缓存

初始值 => initialValue性能优化

先讲概念

React v16.7.0-alpha 中第一次引入了 Hooks 的概念,在 v16.8.0 版本被正式发布。React Hooks 在 React 中只是对 React Hook 的概念性的描述,在开发中咱们用到的实际功能都应该叫作 React hookmarkdown

React Hook 是一种特殊的函数,其本质能够是函数式组件(返回 Dom 或 Dom 及 State ),也能够只是一个工具函数(传入配置项返回封装后的数据处理逻辑)。antd

再总结

React Hooks 的出现使函数式组件变得面目一新,其带来的最大的变化在于给予了函数式组件相似于类组件生命周期的概念,扩大了函数式组件的应用范围。闭包

目前函数式组件基本用于纯展现组件,一旦函数式组件耦合有业务逻辑,就须要经过 Props 的传递,经过子组件触发父组件方法的方式来实现业务逻辑的传递,Hooks 的出现使得函数组件也有了本身的状态与业务逻辑,简单逻辑在本身内部处理便可,再也不须要经过 Props 的传递,使简单逻辑组件抽离更加方便,也使使用者无需关心组件内部的逻辑,只关心 Hooks 组件返回的结果便可。

在我看来,Hooks 组件的目标并非取代类组件,而是增长函数式组件的使用率,明确通用工具函数与业务工具函数的边界,鼓励开发者将业务通用的逻辑封装成 React Hooks 而不是工具函数

之因此把总结放在前面,是想让你们在看后面的内容时有一个总体的概念去引导你们去思考 React Hooks 具体给函数式组件带来了什么变化。

Hooks 初识

官方提供的钩子

目前官方提供的钩子共分为两种,分为基本钩子以及拓展钩子

基本钩子共有:useStateuseEffectuseContext

额外的钩子有:useCallbackuseReduceruseMemouseRefuseLayoutEffectuseImperativeHandleuseDebugValue

不一样钩子用法

useState

该钩子用于建立一个新的状态,参数为一个固定的值或者一个有返回值的方法。钩子执行后的结果为一个数组,分别为生成的状态以及改变该状态的方法,经过解构赋值的方法拿到对应的值与方法。

使用方法以下:

export default function HookDemo() {
  const [count, changeCount] = useState(0);

  return (
    <div> {count} <button onClick={() => { changeCount(Math.ceil(Math.random() * 1000)); }}> 改变count </button> </div>
  );
}
复制代码

useState用法

useEffect

顾名思义,执行反作用钩子。主要用于如下两种状况:

  1. 函数式组件中不存在传统类组件生命周期的概念,若是咱们须要在一些特定的生命周期或者值变化后作一些操做的话,必须借助 useEffect 的一些特性去实现。
  2. useState 产生的 changeState 方法并无提供相似于 setState 的第二个参数同样的功能,所以若是须要在 State 改变后执行一些方法,必须经过 useEffect 实现。

该钩子接受两个参数,第一个参数为反作用须要执行的回调,生成的回调方法能够返回一个函数(将在组件卸载时运行);第二个为该反作用监听的状态数组,当对应状态发生变更时会执行反作用,若是第二个参数为空,那么在每个 State 变化时都会执行该反作用。

使用方法以下:

const [count, changeCount] = useState(0);

// 将在count变化时打印最新的count数据
useEffect(() => {
  message.info(`count发生变更,最新值为${count}`);
}, [count])
复制代码

useEffect

在上面代码中咱们实现了在 useEffect 这个钩子适用状况中的第二种状况,那么如何使用该钩子才能实现相似于类组件中生命周期的功能呢?既然第一个参数是反作用执行的回调,那么实现咱们所要功能的重点就应该在第二个参数上了。

componentDidMount && componentWillUnmout:这两个生命周期只在页面挂载/卸载后执行一次。前面讲过,全部的反作用在组件挂载完成后会执行一次 ,若是反作用存在返回函数,那么返回的函数将在卸载时运行。借助这样的特性,咱们要作的就是让目标反作用在初始化执行一次后不再会被调用,因而只要让与该反作用相关联的状态为空,无论其余状态如何变更,该反作用都不会再次执行,即实现了 componentDidMountcomponentWillUnmout

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

function Child({ visible }) {
  useEffect(() => {
    message.info('我只在页面挂载时打印');
    return () => {
      message.info('我只在页面卸载时打印');
    };
  }, []);

  return visible ? 'true' : 'false';
}

export default function HookDemo() {
  const [visible, changeVisible] = useState(true);


  return (
    <div> { visible && <Child visible={visible} /> } <button onClick={() => { changeVisible(!visible); }}> 改变visible </button> </div> ); } 复制代码

didMount

componentDidUpdate:该生命周期在每次页面更新后都会被调用。那么按照以前的逻辑,就应该把全部的状态所有放在第二个状态中,可是这样的话新增/删除一个状态都须要改变第二参数。其实,若是第二个参数为空,那么在每个 State 变化时都会执行该反作用,那么若是要实现 componentDidUpdate 就很是简单了。

useEffect(() => {
  // ...反作用逻辑
}) // 注意上面说的关联状态为空不是说不传递第二个参数,而是第二个参数应该为一个空数组
复制代码

didUpdate

在类组件中,若是在 componentDidMount 中屡次调用 setState 设置一个值(固然不推荐这样作),并在成功的回调中打印该值,那么最后的结果极可能会打印不少个相同的最后一次设置的值。是由于类的 setState 是一个类异步的结果,他们会将全部变更的内容进行收集而后在合适的时间去统一赋值。

而在 useEffect 中,全部的变量的值都会保留在该反作用执行的时刻,相似于 for 循环中的 let 或者 闭包,全部的变量都维持在反作用执行时的状态,也有人称这个为 Capture Value。

useCallback

生成 Callback 的钩子。用于对不一样 useEffect 中存在的相同逻辑的封装,减小代码冗余,配合 useEffect 使用。

该钩子先看例子会比较好理解一下:

const [count1, changeCount1] = useState(0);
const [count2, changeCount2] = useState(10);

const calculateCount = useCallback(() => {
  if (count1 && count2) {
    return count1 * count2;
  }
  return count1 + count2;
}, [count1, count2])

useEffect(() => {
    const result = calculateCount(count, count2);
    message.info(`执行反作用,最新值为${result}`);
}, [calculateCount])
复制代码

useCallback

在上面的例子中咱们经过 useCallback 的使用生成了一个回调,useCallback 的使用方法和 useEffect 一致,第一个参数为生成的回调方法,第二个参数为该方法关联的状态,任一状态发生变更都会从新生成新的回调

经过上面代码的使用,咱们将 count1 / count2 的值与一个叫作 calculateCount 的方法关联了起来,若是组件的反作用中用到计算 count1 和 count2 的值的地方,直接调用该方法便可。

其中和直接使用 useEffect 不一样的地方在于使用 useCallback 生成计算的回调后,在使用该回调的反作用中,第二个参数应该是生成的回调。其实这个问题是很好理解的,咱们使用 useCallback 生成了一个与 count1 / count2 相关联的回调方法,那么当关联的状态发生变化时会从新生成新的回调,反作用监听到了回调的变化就会去从新执行反作用,此时 useCallbackuseEffect 是按顺序执行的, 这样就实现了反作用逻辑的抽离。

useRef

useRef 接受一个参数,为 ref 的初始值。相似于类组件中的 createRef 方法 ,该钩子会返回一个对象,对象中的 current 字段为咱们 指向的实例 / 保存的变量,能够实现得到目标节点实例或保存状态的功能。

useRef 保存的变量不会随着每次数据的变化从新生成,而是保持在咱们最后一次赋值时的状态,依靠这种特性,再配合 useCabllbackuseEffect 咱们能够实现 preProps/preState 的功能。

const [count, changeCount] = useState(0);
const [count1, changeCount1] = useState(0);
// 建立初始值为空对象的prestate
const preState = useRef({});
// 依赖preState进行判断时能够先判断,最后保存最新的state数据
useEffect(() => {
  const { ... } = preState.current;
  if (// 条件判断) {
    // 逻辑
  }
  // 保存最新的state
  preState.current = {
    count,
    count1,
  }
});
复制代码

useRef用于保存preState

另外,当咱们将使用 useState 建立的状态赋值给 useRef 用做初始化时,手动更改 Ref 的值并不会引发关联状态的变更。从该现象来看,useRef 彷佛只是在内存空间中开辟了一个堆空间将初始化的值存储起来,该值与初始化的值存储在不一样的内存空间,修改 Ref 的值不会引发视图的变化。

export default function HookDemo() {
  const [count] = useState({ count: 1 });
  
  const countRef = useRef(count);

  return (
    <div> {count.count} <button onClick={() => { countRef.current.count = 10; }}> 改变ref </button> </div>
  );
}
复制代码

useRef正常

useMemo

Memo 为 Memory 简写,useMemo 即便用记忆的内容。该钩子主要用于作性能的优化。

前面咱们说过了当状态发生变化时,没有设置关联状态的 useEffect 会所有执行。一样的,经过计算出来的值或者引入的组件也会从新计算/挂载一遍,即便与其关联的状态没有发生任何变化

在类组件中咱们有 shouldComponetUpdate 以及 React.memo 帮助咱们去作性能优化,若是在函数组件中没有相似的功能显示是违背了官方的初衷的,因而就有了 useMemo 这个钩子。

在业务中,咱们能够用 useMemo 来处理计算结果的缓存或引入组件的防止重复挂载优化。其接受两个参数,第一个参数为一个 Getter 方法,返回值为要缓存的数据或组件,第二个参数为该返回值相关联的状态,当其中任何一个状态发生变化时就会从新调用 Getter 方法生成新的返回值。

具体代码以下:

import React, { useState, useMemo } from 'react';
import { message } from 'antd';

export default function HookDemo() {
  const [count1, changeCount1] = useState(0);
  const [count2, changeCount2] = useState(10);

  const calculateCount = useMemo(() => {
    message.info('从新生成计算结果');
    return count1 * 10;
  }, [count1]);
  return (
    <div> {calculateCount} <button onClick={() => { changeCount1(count1 + 1); }}>改变count1</button> <button onClick={() => { changeCount2(count2 + 1); }}>改变count2</button> </div>
  );
}

复制代码

useMemo作计算结果优化

初次接受 useMemo 时可能咱们会以为该钩子只是用来作计算结果的缓存,返回值只能是一个数字或字符串。其实 useMemo 并不关心咱们的返回值类型是什么,它只是在关联状态发生变更时从新调用咱们传递的 Getter 方法 生成新的返回值,也就是说 useMemo 生成的是 Getter 方法与依赖数组的关联关系。所以,若是咱们将函数的返回值替换为一个组件,那么就能够实现对组件挂载/从新挂载的性能优化

代码以下:

import React, { useState, useMemo } from 'react';
import { message } from 'antd';

function Child({ count }) {
  return <p>当前传递的count为:{count}</p>;
}

export default function HookDemo() {
  const [count1, changeCount1] = useState(0);
  const [count2, changeCount2] = useState(10);

  const child = useMemo(() => {
    message.info('从新生成Child组件');
    return <Child count={count1} />; }, [count1]); return ( <div> {child} <button onClick={() => { changeCount1(count1 + 1); }}>改变count1</button> <button onClick={() => { changeCount2(count2 + 1); }}>改变count2</button> </div> ); } 复制代码

useMemo作组件优化

其余钩子

今天主要讲了组件中经常使用的几个钩子,剩下的未讲解的钩子中,如 useLayoutEffect useImperativeHandle useDebugValue ,其功能都比较简单就不在此赘述。

还有一个比较重要的钩子 useContext,是 createContext 功能在函数式组件中的实现。经过该功能能够实现不少强大的功能,能够是说官方的 Redux,不少人对此应该有很多的了解。该钩子内容太多,后续单独使用一个章节进行描述。

编写本身的钩子

其实从上面讲解的内容来看,钩子并非什么高深莫测的东西,它只是对咱们经常使用逻辑的一些封装,接下来就会经过具体的代码来教你们写一个本身的钩子。

最基本的钩子

最基本的钩子也就是返回包含了更多逻辑的 State 以及改变 State 方法的钩子。拿计数器来讲,其最基本的就是返回当前的数字以及减小/增长/重置等功能,明确完功能后能够开始动手作了。

import React, { useState } from 'react';

// 编写咱们本身的hook,名字以use开头
function useCounter(initialValue) {
  // 接受初始化的值生成state
  const [count, changeCount] = useState(initialValue);
  // 声明减小的方法
  const decrease = () => {
    changeCount(count - 1);
  }
  // 声明增长的方法
  const increase = () => {
    changeCount(count + 1);
  }
  // 声明重置计数器方法
  const resetCounter = () => {
    changeCount(0);
  }
  // 将count数字与方法返回回去
  return [count, { decrease, increase, resetCounter }]
}

export default function myHooksView() {
  // 在函数组件中使用咱们本身编写的hook生成一个计数器,并拿到全部操做方法的对象
  const [count, controlCount] = useCounter(10);
  return (
  	<div> 当前数量:{count} <button onClick={controlCount.decrease}>减小</button> <button onClick={controlCount.increase}>增长</button> <button onClick={controlCount.resetCounter}>重置</button> </div>
  )
}
复制代码

在上面的例子中,咱们将在 useCounter 这个钩子中建立了一个关联了 initialValue 的状态,并建立减小/增长/重置的方法,最终将其经过 return 返回出去。这样在其余组件须要用到该功能的地方,经过调用该方法拿到其返回值,便可实现对 useCounter 组件封装逻辑的复用。

演示效果如图:

counter

返回 DOM 的钩子

返回 DOM 其实和最基本的 Hook 逻辑是相同的,只是在返回的数据内容上有一些差别,具体仍是看代码,以一个 Modal 框为例。

import React, { useState } from 'react';
import { Modal } from 'antd';

function useModal() {
  const [visible, changeVisible] = useState(false);

  const toggleModalVisible = () => {
    changeVisible(!visible);
  };

  return [(
    <Modal visible={visible} onOk={toggleModalVisible} onCancel={toggleModalVisible} > 弹窗内容 </Modal>
  ), toggleModalVisible];
}

export default function HookDemo() {
  const [modal, toggleModal] = useModal();
  return (
    <div> {modal} <button onClick={toggleModal}>打开弹窗</button> </div>
  );
}

复制代码

这样咱们就实现了一个返回了弹窗内容以及改变弹窗显示状态的 Hook,其实能够封装的内容还有不少不少,能够经过配置项的设置实现更丰富的封装。

演示效果如图:

modal

钩子/最终总结

钩子总结

钩子 用法 做用
useState const [state, changeState] = useState(initialValue) 用于生成状态以及改变状态的方法
useEffect useEffect(fn, [...relativeState]) 用于生成与状态绑定的反作用
useCallback useCallback(fn, [...relativeState]) 用于生成与状态绑定的回调函数
useMemo useMemo(fn, [...relativeState]) 用于生成与状态绑定的组件/计算结果
useRef const newRef = useRef(initialValue) 用于 获取节点实例 / 数据保存

从上面的表格中咱们能够看出,在官方提供的 Hook 中,除了基本的 useStateuseRef 外,其余钩子都存在第二个参数,第一个方法的执行与第二个参数相互关联。因而咱们能够得出一个结论,在使用了 Hook 的函数式组件中,咱们在使用反作用/引用子组件时都须要时刻注意对代码进行性能上的优化

最终总结

我在前面的总结里是这么评价 React Hooks 的:

Hooks 组件的目标并非取代 class component 组件,而是增长函数式组件的使用率,明确通用工具函数与业务工具函数的边界,鼓励开发者将业务通用的逻辑封装成 React Hooks 而不是工具函数

但愿看完这篇文章的你也有本身的一些见解,欢迎拍砖讨论。

招贤纳士

招人,前端,隶属政采云前端大团队(ZooTeam),50 余个小伙伴正等你加入一块儿浪。若是你想改变一直被事折腾,但愿开始能折腾事;若是你想改变一直被告诫须要多些想法,却无从破局;若是你想改变你有能力去作成那个结果,却不须要你;若是你想改变你想作成的事须要一个团队去支撑,但没你带人的位置;若是你想改变既定的节奏,将会是“5年工做时间3年工做经验”;若是你想改变原本悟性不错,但老是有那一层窗户纸的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但愿参与到随着业务腾飞的过程,亲手参与一个有着深刻的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我以为咱们该聊聊。任什么时候间,等着你写点什么,发给 ZooTeam@cai-inc.com

推荐阅读

Vue 组件数据通讯方案总结

自动化 Web 性能优化分析方案

CSS 层叠上下文(Stacking Context)

相关文章
相关标签/搜索