React Hooks(一).md 12 KB

title: React Hooks(一) author: Gamehu tags:

  • React Hooks categories:
  • 前端 date: 2020-02-25 16:57:00

{% asset_img title.png %}

整理自团队内部的分享,因为从17年底启动的产品线,所以用的当时的最新版本16.3.1,由于种种原因一直没有升级,特别是自从出了Hooks之后,我是一直觉得应该进行版本升级了,因为升级这个事是避免不了的,除非你不再接收新的变化。

去年其实呼吁过一次不过被按下来了,犹不死心,所以就借着分享的机会,再团队内部普及一遍。

当然我说了只是普及不是教程,所以要点就两个:

  1. Hooks的特性
  2. 在项目中的实践

Before

在介绍Hooks之前先说说我在开发中的一些痛点

  1. 类组件没办法写的比较轻巧,毕竟好几个生命周期在那儿摆着,有时候不得不冷静一下想想用哪个生命周期合适。
  2. 本来是个函数组件,就因为需要添加一个变量(state),所以必须改成class组件。
  3. 有关状态管理的逻辑代码很难在组件之间复用、且该业务逻辑的实现代码很多时候被分分散到了不同的生命周期内,当能提组件的时候还好,如果不能提组件那这套代码如果其他地方有用只能重复造轮子
  4. class组件this的指向问题。

Hooks

什么是 Hooks?

我的理解啊,是这样,就是当你要完成一个动作(事情...),必然就需要一个过程的,有过程就可以分阶段,而在某个阶段,你可以在其前后插入事项从而实现对整个过程的扩展以及把控,这就是hook做的事情,挤进去搞事情。类似的比如Spring里的AOP。

而没有React Hooks之前想要实现上述效果,只能用class+生命周期函数,比如下图。

{% asset_img a.png from https://juejin.im/post/5a062fb551882535cd4a4ce3 %}

而React Hooks就是允许你在不编写 class 的情况下使用状态(state)和其他 React 特性。 你还可以构建自己的 Hooks, 跨组件共享可重用的有状态逻辑。React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和在整个渲染过程中进行功能扩展,就用钩子把外部代码"钩"进来。

从图1到图2的进化,忘掉class抱紧hooks。

{% asset_img 5LbsY.jpg 图1 %}

{% asset_img nTmNe.jpg 图2 %}

主要应用的Hook

列举使用频度较高的几个hook。

useRef 代替之前的 ref并且更加强大,不仅用于DOM引用。 “ ref”对象是一个通用容器,其当前属性是可变的,并且可以保存任何值,类似于类的实例属性。

useState 代替之前的 state

useReducer可实现redux类似的功能,其实state就算基于useReducer实现的

useEffect 则可以代替某些生命周期钩子函数,如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

实践

结合项目中的代码实操一下,感受一下hooks的魅力。

示例一

一个最简单的例子,只是为了加一个变量。

场景:实现弹出窗体的效果,需要一个变量visible控制窗口的显示和隐藏,代码如下:

class AlarmCards extends React.Component {
  constructor(props) {
    super(props);
    this.state = { visible: false };
  }

  updateVisible = (visible) => {
    this.setState({
      visible
    });
  };

  render() {
    return {...};
  }
} 

切换到Hooks代码如下:

  1. class变为函数组件
  2. 用useState

    import React, { useState } from 'react';
    const AlarmCards = ({ ...props }) => {
    // useState 直接声明变量visible,同时声明方法setVisible来更新visible
    // false 初始值
    const [visible, setVisible] = useState(false);
      
    return {...页面内容...};
    }
    }
    

示例二

看一个稍微复杂一点点的例子,只涉及到两个生命周期函数。

场景:从后端获取数据使其更新组件内容,并在该组件卸载时,更新重置状态(变量)到初始值,代码如下:

变量声明


export const initState = {
  loading: false,
  data: [],
  header: [],
  // 探测结果弹出层是否打开
  visible: false,
  // 探测结果弹出层参数对象
  drawerParams: {
    appId: null,
    appName: '',
    record: null
  }
};

请求方法封装

/**
 * 排查对比表
 * @param params 参数对象
 * @returns {Function}
 */
export const getComparisonTable = ({ ...params }) => {
  /**
   * alarmRule 告警规则(类型)
   * updateTime 告警更新时间
   * onDotClick 小圆点的点击事件
   * appId 当前告警的应用id
   * span 告警计算时间跨度
   */
  const { alarmId, alarmRule, updateTime, onDotClick, appId, span } = params;
  // 开始请求
  setState({ loading: true });
  const errorCallback = () => {
    setState({ loading: false });
  };
  req(BASE_WEB_API.GET_ALARM_DETAIL_COMPARISON, { alarmId, alarmRule, updateTime }, null, {
    errorCallback
  }).then(result => {
    if (!isAvailableArray(result)) {
      setState({ loading: false });
      return;
    }
    // 生成表格需要的表头和数据
    const data = generateTableObjs(result, onDotClick, appId);
    setState({ ...data, loading: false });
  });
};

展示组件

class AlarmTable extends Component {
  componentDidMount() {
    const { alarmId, alarmRule, updateTime, appId, getComparisonTable, span,
    onDotClick } = this.props;
    // 获取对比表数据
    getComparisonTable({ alarmId, alarmRule, updateTime, onDotClick, appId, span });
  }
  
  componentWillUnmount() {
    const { setState } = this.props;
    // 重置state避免脏数据影响折叠面板展开和关闭
    setState(model.initState);
  }
  
render() {
   ....
  return {...页面内容...};
  }
 }

Hooks切换,代码如下:

  1. class变为函数组件
  2. 用useState+useEffect

    import React, { useEffect, useState } from 'react';
    const AlarmTable = ({ ...props }) => {
    
    const { alarmId, alarmRule, updateTime, appId, closeLoading, span, onDotClick } = props;
    // 变量声明
    const [tableData, setTableData] = useState({ data: [], header: [] });
    
    // 开始请求, useEffect可当作componentDidMount,componentDidUpdate 和 
    // componentWillUnmount三个生命周期的组合
    
    useEffect(() => {
    const { alarmId, alarmRule, updateTime, appId, onDotClick, closeLoading } = props;
    const errorCallback = () => {
    closeLoading(false);
    };
    req(BASE_WEB_API.GET_ALARM_DETAIL_COMPARISON, { alarmId, alarmRule, updateTime }, null, {
    errorCallback
    }).then(result => {
    if (!isAvailableArray(result)) {
      errorCallback();
      return;
    }
    // 生成表格需要的表头和数据
    const data = generateTableObjs(result, onDotClick, appId);
    setTableData({ ...data });
    closeLoading(false);
    });
    
    // 卸载函数
    return function cleanup() {
    // 重置state避免脏数据影响页面呈现
    setTableData(({ data: [], header: [] });
    };
    });
    
    ....
    return {...页面内容...};
    }
    

继续优化:

自定义钩子封装

function useCpmparisonTable(closeLoading, alarmId, alarmRule, updateTime, onDotClick, appId) {
  const [drawer, setDrawer] = useState({
    visible: false,
    drawerParams: { appId: null, appName: null, record: null }
  });
  const [tableData, setTableData] = useState({ data: [], header: [] });
  
  useEffect(() => {
    // 开始请求
    const errorCallback = () => {
      closeLoading(false);
    };
    req(BASE_WEB_API.GET_ALARM_DETAIL_COMPARISON, { alarmId, alarmRule, updateTime }, null, {
      errorCallback
    }).then(result => {
      if (!isAvailableArray(result)) {
        errorCallback();
        return;
      }
      // 生成表格需要的表头和数据
      const data = generateTableObjs(result, onDotClick, appId);
      setTableData({ ...data });
      closeLoading(false);
    });
    
    // 功能等同componentWillUnmount
    return  ()=> {
       // 重置state避免脏数据影响页面呈现
       setTableData(({ data: [], header: [] });
    };
  });
  return [drawer, setDrawer, tableData];
}

页面组件

const AlarmTable = ({ ...props }) => {
const { alarmId, alarmRule, updateTime, appId, closeLoading, onDotClick } = props;
// 自定义钩子,通常用use开头于官方的钩子呼应,使其能一眼看出这是一个hook
const [ tableData ] = useComparisonTable(closeLoading, alarmId, alarmRule, updateTime, onDotClick, appId);
....
  return {...页面内容...};
}

小结

从示例中可以看出Hooks的带来的一些变化,当然篇幅有限只写了两个Hook,useState和useEffect。

简单总结一下Hooks带来的优势

  1. 干掉了生命周期(夸张了一点点),不用在多个生命周期函数中徘徊

  2. 为后面的第三方组件等升级做铺垫。

    特别是基于React的三方库,比如我们用的Ant Design,
    官方前几天发布了4.0有重大升级,假设我们有一天要升级到AntD 4.0,
    它会告诉你先要把React升到16.8以后也就是支持Hooks之后的版本。
    
  3. 减少代码量,且从面向函数编程细化到面向业务逻辑块编程

    1. 比如不用再bind方法或者不用再写方法体来改变state可用自定的hook封装业务逻辑 使业务逻辑内聚,便于整套业务逻辑能够在不同的组件间复用,组件在使用的时候也不需要关注其内部的实现。

    2. Hook能够在传统的类组件基础上上,实现细化到逻辑层面的代码复用,而不仅仅是停留在组件级别, 而且Hook的复用并不是停留在将某些常用的逻辑方法代码抽成一个公共方法,而是可以将之前散落在类组件中各个生命周期中的用于实现某个业务的逻辑代码合并在一起封装成一个自定义的Hook,其他地方随用随调。

      比如我们的各种CRUD的表单...
      比如我们的表格一些通用交互,过滤、刷新、排序、查询...
      比如我们的图表的一些通用交互,框选、点选...
      
  4. 更简洁易测的组件。

    比如后续期望分享的前端单元测试的工具,如果我们要把单元测试用起来,
    你会发现class和函数写单元测试的差别真的很大
    

Hooks不足:

当然不能吹爆React的Hooks,虽然业界公认包括官方规划都指出,Hooks是React的未来,未来需要一个过程。

  1. 现在的Hooks还不能完全替代class

  2. 使用的Hooks必须保证顺序,即内部是通过两个数组来管理的,所以不要在循环,条件判断,嵌套函数里面调用 Hooks。使其下标对不上从而导致state发生混乱,这在前期可能很容易发生bug。

下图可简单理解一下内部的原理:

{% asset_img lQQX7.png %}

  1. 使用hook后,代码归类不会像之前class组件时代的一样有语法的强制规划了,什么意思呢?在class组件时代,redux的有关的代码是放到connect里的,state生命是放constructor里的,其他逻辑是放每个有关的生命周期里的。而在hook的时代,没有这些东西了,一切都直接放在函数组件内部,如果写得混乱,看起来就是一锅粥,所以,制定组件的书写规范和通过注释来归类不同功能的逻辑显得尤为重要。这有助于后期的维护,也有助于保持一个团队在代码书写风格上的一致性。

最后

Peace & Love,没有银弹。