(怎么写组件)Hooks 学习记录

124 阅读7分钟

1、 起源背景

起源于 React,在 Hooks 出现之前,函数组件对比类组件(class)形式有很多局限,例如:

1. 不能使用 state、ref 等属性,只能通过函数传参的方式使用 props
2. 没有生命周期钩子
3. 监听清理和资源释放问题
4. 组件间逻辑复用困难

当组件要销毁时,很多情况下都需要清除注册的监听事件、释放申请的资源。

事件监听、资源申请需要在 Mount 钩子中申请,当组件销毁时还必须在 Unmount 勾子中进行清理,这样写使得同一资源的生成和销毁逻辑不在一起,因为生命周期被迫划分成两个部分。

Vue 里面,在setup里面使用的响应式是有EffectScope包裹的会自动处理,所以并不需要手动处理,但在setup外面使用的响应式需要手动清理。

组件涵盖了太多的功能,导致暴露的能力接口过于复杂,与本身钻进绑定的逻辑太多,所以我们要细化组件的颗粒度,但是在这个过程中不能以牺牲代码的可读性和可维护性为代价。

2、 是什么

计算机行业概念:系统运行到某一时期时,会调用被注册到该时机的回调函数。

react: 一系列方法,提供了在函数式组件中完成开发工作的能力。

vue: 在 vue 组合式API里,以 “use” 作为开头的,一系列提供了组件复用、状态管理等开发能力的方法。

综合理解:

1、 是函数,不是新技术,是在前端组件化开发的场景下演变出来的新的编程理念:OOP ==》FP,因为hooks是以函数的形式展示,所以我们编写函数的相关规范可以套进写hooks中;

2、 是逻辑的内聚,我们不一定要以复用为目的去抽象hooks,它也可以是一种逻辑的内聚,单纯的为了我们代码的提高可读性,可维护性,它把原来分离在各个生命周期中的逻辑整合在一起了;

3、 从组件颗粒度的角度来看,可以是小小的一个逻辑,也可以是多个小逻辑的组合;

4、 从组件逻辑抽象的角度来看,把通用的东西抽象出来然后复用;

5、 没有囊括生命周期钩子和状态的hooks,其实更像是我们以往编程中分离的函数,换汤不换药。

2、命名和规范

一般来说,hooks都是以 use 开头的。

套用 react 官方文档的解释:一个假设,两个只在

zh-hans.reactjs.org/docs/hooks-…

一个假设:假设任何以use开头的并一个大写字母的函数就是一个hook

第一个只在: 只在 React 函数组件中调用 Hook,而不在普通函数中调用 Hook。(区分普通函数方法与Hooks)

第二个只在: 只在最顶层使用 Hook,而不要在循环,条件或嵌套函数中调用 Hook。

3、优点

1、 没有this

2、 让函数拥有生命周期钩子和自己的状态

3、 代码高度聚合,具有高可读性和高可维护性

4、 逻辑复用,属性和方法容易追溯,没重名问题,由于闭包可以多次复用

5、 比mixin、HOC更轻量,改造成本更小

6、 状态复用,组件状态的粒度变得更小,组件中的状态和 UI 变得更为清晰和隔离

7、 灵活性,Hooks 中可以引用另外的 Hooks形成新的Hooks,组合变化万千

8、 渐进式,可以慢慢抽离逻辑和状态,不一定需要全量修改

4、缺点

1、 只能放在顶层,不能放在回调中、循环、条件中(严格来看这不是缺点,因为本来的约定就是这样,放在回调、循环里面的是普通方法而不属于hooks)

2、 在闭包场景可能会引用到旧的状态值(严格来看这不是缺点,因为优秀的hooks是不会再顶层设置变量的,所以不会存在这个问题)

5、Vue 与 React 之间 Hooks的异同点

1、相似点: 总体思路是一致的 都遵照着 "定义状态数据","操作状态数据","隐藏细节" 作为核心思路。

2、差异点:

a. vue3 的组件里, setup 是作为一个早于 “created” 的生命周期存在的,无论如何,在一个组件的渲染过程中只会进入一次。

b. React 函数组件 则完全不同,如果没有被 memorized,它们可能会被不停地触发,不停地进入并执行方法。

c. React的hooks具有完整的生命周期,可以是一个很完善的程序,而vue并不能很好做到这一样,因为vue并没有完全独立的上下文。

d. react:只能在“函数组件”内使用hooks,vue:只能在“setup”周期内使用hooks

6、实际应用

1、 赋能组件:编写无头组件

组件的组成部分(基础术语)

视图:css和html、react的jsx或者vue的template代码 ==> 展示视图

交互:生命周期,按钮交互,事件,回调 ==》 实现需求

业务:登录注册,获取数据等与组件无关的业务抽象 ==》 基础功能

拆分原则

高内聚:组件代码只跟组件功能相关

低耦合:不与外部组件产生过多交互

代码位置

a. 只被页面内的组件复用,就放到页面文件夹下

b. 只在当前业务场景下的不同页面复用,就放到当前业务模块的文件夹下

c. 可以在不同业务场景间通用,就放到最顶层的公共文件夹,或者考虑做成组件库

怎么做

复杂业务:业务逻辑hooks、交互逻辑hooks、组件暴露数据,方法到视图

简单业务:业务逻辑hooks、组件中写交互逻辑、组件暴露数据,方法到视图

复杂业务示例:

业务逻辑:

const useUser = () => {
  // 用户状态
  const userInfo = ref({});
  
  // 获取用户状态
  const getUserInfo = () => {}
  // 修改用户状态
  const changeUserInfo = () => {};
  // 检查两次输入的密码是否相同
  const checkRepeatPass = (oldPass,newPass) => {}
  // 修改密码
  const changePassword = () => {};

  return {
    userInfo,
    getUserInfo,
    changeUserInfo,
    checkRepeatPass,
    changePassword,
  }
}

交互逻辑:

const useUserControl = () => {
    // 组合用户业务逻辑hook
    const { userInfo, getUserInfo, changeUserInfo, checkRepeatPass, changePassword } = useUser();

    // loading状态
    const loading = ref(false);
    // 弹窗状态
    const errorModalState = reactive({
        visible: false, 
        errorText: '',  
    });
    
    // 初始化数据
    const initData = () => {
        getUserInfo();
    }
    // 修改密码并提交表单
    const onChangePassword = ({ oldPass, newPass ) => {
        // 判断两次密码是否一致
        if (checkRepeatPass(oldPass, newPass)) {
            changePassword();
        } else {
            errorModalState.visible = true;
            errorModalState.text = '两次输入的密码不一致,请修改'
        }
    };

    return {
        // 用户数据
        userInfo,
        // 初始化数据
        initData: getUserInfo,
        // 修改密码
        onChangePassword,
        // 修改用户信息
        onChangeUserInfo: changeUserInfo,
    }
}

组件使用:

<template>
</template>
<script setup>
import useUserControl from './useUserControl';
import { onMounted } from 'vue';

const { userInfo, initData, onChangePassword, onChangeUserInfo } = useUserControl();

onMounted(initData);
<script>

简单业务示例:

业务逻辑:同上

组件使用:

<template>
</template>

<script setup>
import { onMounted } from 'vue';

// 相当于交互逻辑hook
const useUser = () => { 
    // 代码省略
}

const { userInfo, getUserInfo, changeUserInfo, checkRepeatPass, changePassword } = useUser();

// loading状态
const loading = ref(false);
// 弹窗状态
const errorModalState = reactive({
    visible: false, 
    errorText: '', 
});

// 初始化数据
const initData = () => { getUserInfo(); }
// 修改密码表单提交
const onChangePassword = ({ oldPass, newPass ) => {};
    
onMounted(initData);
<script>

2、自定义hooks:逻辑状态复用

import { ref, watch } from 'vue';
const useAdd= ({ num1, num2 })  =>{
    const addNum = ref(0)
    watch([num1, num2], ([num1, num2]) => {
        addFn(num1, num2)
    })
    const addFn = (num1, num2) => {
        addNum.value = num1 + num2
    }
    return {
        addNum,
        addFn
    }
}
export default useAdd
参考:开源hooks库:VueUse、ahooks

3、(重要)收束逻辑----faced模式:

如果有状态和生命周期钩子就是自定义hooks,纯逻辑运算的就是个函数(就是上面提到的无头组件的JS部分)

faced模式(外观模式):提供一个统一的接口,用来访问子系统中的一群接口。外观模式定义了一个高层的接口,让子系统更容易使用。

1、定义一个用户Hooks ==》 useUser

const useUser = () => {};

2、user 模块可以包含我们在开发中经常遇到的需求:

登录、登出、获取个人信息

const useUser = () => {
    const useLogin = () => {};
    const useLogout = () => {};
    const useInfo = () => {};
    
    export {
        useLogin,
        useLogout,
        useInfo
    };
};

8、总结

现在来看,hooks意义最要是体现在它可以内聚功能逻辑以及抽离逻辑上面。

9、参考:

全新的 React 组件设计理念 Headless UI

【Web技术】1445- 如何使用 Hooks 写出高质量的 React 和 Vue 组件?

在 Vue3 中实现 React 原生 Hooks(useState、useEffect),深入理解 React Hooks原理

换个角度思考 React Hooks

我所理解的 Hooks API

React hooks与Faced模式才是最佳实践?