我们前端都在诟病专业版,它的组件,它的耦合嵌套之深,它的性能。

我们希望改善,我们认为,如果……就好了。

如果重构就好了,如果技术栈统一就好了,如果有规范就好了。

其实,不用等,我们只要在写代码,就可以进行优化。

关键的一点,就是,如何写正确的代码。如果不能写正确的代码,以前的老问题没解决,又会加上新的问题。

我们现在基本上能达成一个共识,就是函数组件比类组件要好(参见https://reactjs.bootcss.com/docs/hooks-intro.html难以理解的 class)

所以,我们在写新组件的时候,优先会写成函数组件。

基于以上,我们来好好讨论,了解一下函数组件中常用的hooks。

Hooks的核心原理和实战

react

特性

  • 虚拟dom机制

    • diff算法

  • jsx语法

原理/本质

  • react中文含义-反应/响应

    • 数据驱动UI

    • 从 Model 到 View 的映射

      React 本身正是为动态的状态变化而设计的,而可能引起状态变化的原因基本只有两个:用户操作产生的事件,比如点击了某个按钮。副作用产生的事件,比如发起某个请求正确返回了。这两种事件本身并不会导致组件的重新渲染,但我们在这两种事件处理函数中,一定是因为改变了某个状态,这个状态可能是 State 或者 Context,从而导致了 UI 的重新渲染。

元素

  • 组件

    • 形式-树状结构

      • 内置组件

        div input等 小写字母

      • 自建组件

        大写字母开头

  • 状态

    • props

    • state

      • useState

  • JSX

    • 语法糖

    • 数据驱动UI变化

      • 数据绑定

        • UI 的展现看成一个函数的执行过程

          Model 是输入参数,函数的执行结果是 DOM 树,也就是 View。而 React 要保证的,就是每当 Model 发生变化时,函数会重新执行,并且生成新的 DOM 树,然后 React 再把新的 DOM 树以最优的方式更新到浏览器。

其他

  • Fiber

hooks

特点

可以实现class组件的所有能力

  • 状态管理

  • 生命周期

  • 函数式思想

    区别于dialog.show(),对象方式,细粒度控制UI

目的

  • 了解hooks的边界

    • 能做什么

    • 不能做什么

原理

  • 为什么要发明hooks

    • hooks 钩子 定义

      • State => View 映射

      • Hooks 就是把某个目标结果钩到某个可能会变化的数据源或者事件源上,那么当被钩到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果。

      • 模型

    • 简化逻辑复用

      • Hooks 中被钩的对象,不仅可以是某个独立的数据源,也可以是另一个 Hook 执行的结果,这就带来了 Hooks 的最大好处:逻辑的复用

      • 替代高阶组件

        • eg 窗口大小变化

    • 关注分离

      • 高内聚,低耦合

        Class 组件中,你不得不把同一个业务逻辑的代码分散在类组件的不同生命周期的方法中。

      • 代码区别

        图的左侧是 Class 组件,右侧是函数组件结合 Hooks。蓝色和黄色代表不同的业务功能。可以看到,在 Class 组件中,代码是从技术角度组织在一起的,例如在 componentDidMount 中都去做一些初始化的事情。而在函数组件中,代码是从业务角度组织在一起的,相关代码能够出现在集中的地方,从而更容易理解和维护。

      • 解决了 Class 组件代码冗余、难以逻辑复用的问题

  • useEffect就是生命周期函数吗

与class组件差异

  • 思考方式差异

    • class

      • 思考方式:某个生命周期方法中我要做什么

      • class BlogView extends React.Component {
        // ...
        componentDidMount() {
        // 组件第一次加载时去获取 Blog 数据
        fetchBlog(this.props.id);
        }
        componentDidUpdate(prevProps) {
        if (prevProps.id !== this.props.id) {
        // 当 Blog 的 id 发生变化时去获取博客文章
        fetchBlog(this.props.id);
        }
    • hooks

      • 思考方式:当某个状态发生变化时,我要做什么

        function BlogView({ id }) {
        useEffect(() => {
        // 当 id 变化时重新获取博客文章
        fetchBlog(id);
        }, [id]); // 定义了依赖项 id
        }
        • 忘掉生命周期概念

  • state差异

    • class

      • constructor

        • state-一个对象

        • 定义类的实例成员

    • hooks

      • useState可以有多个(更好语义化)

        • state改变,重新渲染

  • 设计模式

    • class

      • 面向对象开发

        • 构造函数

          • 在所以其它代码执行之前的一次性初始化工作

    • hooks

      • 函数式开发

        • hooks内部进行初始化

          import { useRef } from 'react';
          
          // 创建一个自定义 Hook 用于执行一次性代码
          function useSingleton(callback) {
          // 用一个 called ref 标记 callback 是否执行过
          const called = useRef(false);
          // 如果已经执行过,则直接返回
          if (called.current) return;
          // 第一次调用时直接执行
          callBack();
          // 设置标记为已执行过
          called.current = true;
          } import useSingleton from './useSingleton'; const MyComp = () => {
          // 使用自定义 Hook
          useSingleton(() => {
          console.log('这段代码只执行一次');
          }); return (
          <div>My Component</div>
          );
          };
  • 代码

    • class

      • 不同生命周期

        • 松散

    • hooks

      • 内聚

  • 其他

    • hooks没法实现这几个生命周期

      • getSnapshotBeforeUpdate, componentDidCatch, getDerivedStateFromError

        • 用的少

    • hooks和class可以存于同一个项目中

      • class没必要一定重构成hooks

      • 能正确工作的代码就是好代码

    • 类组件和函数组件可以互相引用

    • Hooks 很容易就能转换成高阶组件,并供类组件使用

介绍

  • useState

    • 总结

      useState(initialState) 的参数 initialState 是创建 state 的初始值,它可以是任意类型,比如数字、对象、数组等等。

      useState() 的返回值是一个有着两个元素的数组。第一个数组元素用来读取 state 的值,第二个则是用来设置这个 state 的值。在这里要注意的是,state 的变量(例子中的 count)是只读的,所以我们必须通过第二个数组元素 setCount 来设置它的值。 如果要创建多个 state,那么我们就需要多次调用 useState。

    • 与class组件区别

      • 类组件中的 state 只能有一个(一个对象),useState可以有多个(更好语义化)

    • 注意

      • state 中永远不要保存可以通过计算得到的值

        • props传递过来的值

        • URL获取的值

        • 从 cookie、localStorage 中读取的值

      • 清空state

        • 编辑或者创建完成以后

  • useEffect

    • 理解

      • 副作用

        • useEffect(callback, dependencies)

      • useEffect 是每次组件 render 完后判断依赖并执行

        对应到 Class 组件,那么 useEffect 就涵盖了 ComponentDidMount、componentDidUpdate 和 componentWillUnmount 三个生命周期方法。不过如果你习惯了使用 Class 组件,那千万不要按照把 useEffect 对应到某个或者某几个生命周期的方法。你只要记住,useEffect 是每次组件 render 完后判断依赖并执行就可以了。

    • 用法

      每次 render 后执行:不提供第二个依赖项参数。比如useEffect(() => {})。 仅第一次 render 后执行:提供一个空数组作为依赖项。比如useEffect(() => {}, [])。 第一次以及依赖项发生变化后执行:提供依赖项数组。比如useEffect(() => {}, [deps])。 组件 unmount 后执行:返回一个回调函数。比如useEffect() => { return () => {} }, [])。

      • 无依赖项

        • 每次 render 后都会重新执行

      • 依赖项[ ]

        • 只在首次执行时触发

          • = componentDidMount

      • 依赖项[id]

        • 首次以及id变化执行

      • return 卸载

        import React, { useEffect } from 'react';
        import comments from './comments'; function BlogView({ id }) {
        const handleCommentsChange = useCallback(() => {
        // 处理评论变化的通知
        }, []);
        useEffect(() => {
        // 获取博客内容
        fetchBlog(id);
        // 监听指定 id 的博客文章的评论变化通知
        const listener = comments.addListener(id, handleCommentsChange); return () => {
        // 当 id 发生变化时,移除之前的监听
        comments.removeListener(listener);
        };
        }, [id, handleCommentsChange])
        } useEffect 接收的返回值是一个回调函数,这个回调函数不只是会在组件销毁时执行,而且是每次 Effect 重新执行之前都会执行,用于清理上一次 Effect 的执行结果。
    • 依赖项

      • 依赖项中定义的变量一定是会在回调函数中用到的,否则声明依赖项其实是没有意义的。

      • 依赖项一般是一个常量数组,而不是一个变量。

        • 死循环

      • React 会使用浅比较来对比依赖项是否发生了变化,所以要特别注意数组或者对象类型。

        如果你是每次创建一个新对象,即使和之前的值是等价的,也会被认为是依赖项发生了变化。这是一个刚开始使用 Hooks 时很容易导致 Bug 的地方。

    • 规则

      • 只能在顶级作用域使用

        • 顺序执行

          React 组件内部,其实是维护了一个对应组件的固定 Hooks 执行列表的,以便在多次渲染之间保持 Hooks 的状态,并做对比。

          如果if,第一次和第二次hook不一样,就会报错。 if,else不会报错

        • 所有hook必须被执行到

        • 不能在循环,条件判断,函数内执行

      • 只能在函数组件使用

        • 或者自定义hook使用

      • 安装eslint-plugin-react-hooks

        npm install eslint-plugin-react-hooks --save-dev
        
        然后在你的 ESLint 配置文件中加入两个规则:rules-of-hooks 和 exhaustive-deps。如下:
        
        {
        "plugins": [
        // ...
        "react-hooks"
        ],
        "rules": {
        // ...
        // 检查 Hooks 的使用规则
        "react-hooks/rules-of-hooks": "error",
        // 检查依赖项的声明
        "react-hooks/exhaustive-deps": "warn"
        }
        }
  • useCallback

    • 缓存回调函数

      • 多次渲染间,维持一个状态

        • 避免,每次创建新函数,让接收事件处理函数的组件重新渲染

    • 只有当依赖项目变化,才重新定义回调函数

      useCallback(fn, deps)
      
      fn 是定义的回调函数,deps 是依赖的变量数组。只有当某个依赖变量发生变化时,才会重新声明 fn 这个回调函数。
      
      import React, { useState, useCallback } from 'react';
      
      function Counter() {
      const [count, setCount] = useState(0);
      const handleIncrement = useCallback(
      () => setCount(count + 1),
      [count], // 只有当 count 发生变化时,才会重新创建回调函数
      );
      // ...
      return <button onClick={handleIncrement}>+</button>
      }
  • useMemo

    • 缓存计算结果

      • 如果某个数据是通过其它数据计算得到的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该需要重新计算。

        useMemo(fn, deps);

  • useRef

    • 在多次渲染之间共享数据

      • 唯一的 current 属性

    • 保存某个 DOM 节点的引用

  • useContext

自定义hook

  • 定义

    • 函数中用到hooks

    • 以use开头的函数

  • 作用

    • 复用逻辑

    • 语义化

  • 应用

    • 抽取业务逻辑

    • 封装通用逻辑

      • useAsync

        import { useState } from 'react';
        
        const useAsync = (asyncFunction) => {
        // 设置三个异步逻辑相关的 state
        const [data, setData] = useState(null);
        const [loading, setLoading] = useState(false);
        const [error, setError] = useState(null);
        // 定义一个 callback 用于执行异步逻辑
        const execute = useCallback(() => {
        // 请求开始时,设置 loading 为 true,清除已有数据和 error 状态
        setLoading(true);
        setData(null);
        setError(null);
        return asyncFunction()
        .then((response) => {
        // 请求成功时,将数据写进 state,设置 loading 为 false
        setData(response);
        setLoading(false);
        })
        .catch((error) => {
        // 请求失败时,设置 loading 为 false,并设置错误状态
        setError(error);
        setLoading(false);
        });
        }, [asyncFunction]); return { execute, loading, data, error };
        }; 应用: import React from "react";
        import useAsync from './useAsync'; export default function UserList() {
        // 通过 useAsync 这个函数,只需要提供异步逻辑的实现
        const {
        execute: fetchUsers,
        data: users,
        loading,
        error,
        } = useAsync(async () => {
        const res = await fetch("https://reqres.in/api/users/");
        const json = await res.json();
        return json.data;
        }); return (
        // 根据状态渲染 UI...
        );
        }
    • 监听浏览器状态

      • 窗口大小,滚动条位置,cookies,localStorage, URL

    • 拆分复杂组件

      • 代码过长

        function BlogList() {
        // 获取文章列表...
        // 获取分类列表...
        // 组合文章数据和分类数据...
        // 根据选择的分类过滤文章... // 渲染 UI ...
        } --------- import React, { useEffect, useCallback, useMemo, useState } from "react";
        import { Select, Table } from "antd";
        import _ from "lodash";
        import useAsync from "./useAsync"; const endpoint = "https://myserver.com/api/";
        const useArticles = () => {
        // 使用上面创建的 useAsync 获取文章列表
        const { execute, data, loading, error } = useAsync(
        useCallback(async () => {
        const res = await fetch(`${endpoint}/posts`);
        return await res.json();
        }, []),
        );
        // 执行异步调用
        useEffect(() => execute(), [execute]);
        // 返回语义化的数据结构
        return {
        articles: data,
        articlesLoading: loading,
        articlesError: error,
        };
        };
        const useCategories = () => {
        // 使用上面创建的 useAsync 获取分类列表
        const { execute, data, loading, error } = useAsync(
        useCallback(async () => {
        const res = await fetch(`${endpoint}/categories`);
        return await res.json();
        }, []),
        );
        // 执行异步调用
        useEffect(() => execute(), [execute]); // 返回语义化的数据结构
        return {
        categories: data,
        categoriesLoading: loading,
        categoriesError: error,
        };
        };
        const useCombinedArticles = (articles, categories) => {
        // 将文章数据和分类数据组合到一起
        return useMemo(() => {
        // 如果没有文章或者分类数据则返回 null
        if (!articles || !categories) return null;
        return articles.map((article) => {
        return {
        ...article,
        category: categories.find(
        (c) => String(c.id) === String(article.categoryId),
        ),
        };
        });
        }, [articles, categories]);
        };
        const useFilteredArticles = (articles, selectedCategory) => {
        // 实现按照分类过滤
        return useMemo(() => {
        if (!articles) return null;
        if (!selectedCategory) return articles;
        return articles.filter((article) => {
        console.log("filter: ", article.categoryId, selectedCategory);
        return String(article?.category?.name) === String(selectedCategory);
        });
        }, [articles, selectedCategory]);
        }; const columns = [
        { dataIndex: "title", title: "Title" },
        { dataIndex: ["category", "name"], title: "Category" },
        ]; export default function BlogList() {
        const [selectedCategory, setSelectedCategory] = useState(null);
        // 获取文章列表
        const { articles, articlesError } = useArticles();
        // 获取分类列表
        const { categories, categoriesError } = useCategories();
        // 组合数据
        const combined = useCombinedArticles(articles, categories);
        // 实现过滤
        const result = useFilteredArticles(combined, selectedCategory); // 分类下拉框选项用于过滤
        const options = useMemo(() => {
        const arr = _.uniqBy(categories, (c) => c.name).map((c) => ({
        value: c.name,
        label: c.name,
        }));
        arr.unshift({ value: null, label: "All" });
        return arr;
        }, [categories]); // 如果出错,简单返回 Failed
        if (articlesError || categoriesError) return "Failed"; // 如果没有结果,说明正在加载
        if (!result) return "Loading..."; return (
        <div>
        <Select
        value={selectedCategory}
        onChange={(value) => setSelectedCategory(value)}
        options={options}
        style={{ width: "200px" }}
        placeholder="Select a category"
        />
        <Table dataSource={result} columns={columns} />
        </div>
        );
        }
        • 需要保持每个函数到短小

          尽量将相关的逻辑做成独立的 Hooks,然后在函数组中使用这些 Hooks,通过参数传递和返回值让 Hooks 之间完成交互。

          拆分逻辑的目的不一定是为了重用,而可以是仅仅为了业务逻辑的隔离。所以在这个场景下,我们不一定要把 Hooks 放到独立的文件中,而是可以和函数组件写在一个文件中。这么做的原因就在于,这些 Hooks 是和当前函数组件紧密相关的,所以写到一起,反而更容易阅读和理解。

注意

  • state

    • 1、状态最小化原则,避免冗余状态

    • 2、唯一数据源原则,避免中间状态

本博客其他有关文章

React Hooks

Hooks的核心原理梳理的更多相关文章

  1. 服务治理演进剖析 & Service Mesh、 xDS核心原理梳理

    基于XDS协议实现控制面板与数据面板通信分享 基于这段时间在同程艺龙基础架构部的蹲坑,聊一聊微服务治理的核心难点.历史演进.最新动态, 以上内容属自我思考,不代表同程艺龙技术水准.如理解有偏差.理解不 ...

  2. 《大型网站技术架构:核心原理与案例分析》【PDF】下载

    <大型网站技术架构:核心原理与案例分析>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230062557 内容简介 本书通过梳理大型网站 ...

  3. 新书介绍 -- 《Redis核心原理与实践》

    大家好,今天给大家介绍一下我的新书 -- <Redis核心原理与实践>. 后端开发的同学应该对Redis都不陌生,Redis由于性能极高.功能强大,已成为业界非常流行的内存数据库. < ...

  4. 【算法】(查找你附近的人) GeoHash核心原理解析及代码实现

    本文地址 原文地址 分享提纲: 0. 引子 1. 感性认识GeoHash 2. GeoHash算法的步骤 3. GeoHash Base32编码长度与精度 4. GeoHash算法 5. 使用注意点( ...

  5. docker核心原理

    容器概念. docker是一种容器,应用沙箱机制实现虚拟化.能在一台宿主机里面独立多个虚拟环境,互不影响.在这个容器里面可以运行着我饿们的业务,输入输出.可以和宿主机交互. 使用方法. 拉取镜像 do ...

  6. HDFS 核心原理

    HDFS 核心原理 2016-01-11 杜亦舒 HDFS(Hadoop Distribute File System)是一个分布式文件系统文件系统是操作系统提供的磁盘空间管理服务,只需要我们指定把文 ...

  7. 剖析SSH核心原理(一)

      在我前面的文章中,也试图总结过SSH,见 http://blog.csdn.net/shan9liang/article/details/8803989 ,随着知识的积累,总感觉以前说得比较笼统, ...

  8. 关于Ajax的技术组成与核心原理

    1.Ajax 特点: 局部刷新.提高用户的体验度,数据从服务器商加载 2.AJax的技术组成 不是新技术,而是之前技术的整合 Ajax: Asynchronous Javascript And Xml ...

  9. Libevent核心原理

    Libevent 是一个事件驱动框架, 不能仅说他是一个网络库. notejs就是采用与libevent类似的libev来做核心驱动的.   Libevent支持三种事件:io事件.信号事件.时间事件 ...

  10. 高性能消息队列 CKafka 核心原理介绍(上)

    欢迎大家前往腾讯云技术社区,获取更多腾讯海量技术实践干货哦~ 作者:闫燕飞 1.背景 Ckafka是基础架构部开发的高性能.高可用消息中间件,其主要用于消息传输.网站活动追踪.运营监控.日志聚合.流式 ...

随机推荐

  1. 跟运维学 Linux - 01

    跟运维学 Linux - 01 运维的诞生 运维工程师有很多叫法:系统运维.Linux 工程师.系统管理员... 网管可以说是运维工程师最早的雏形.在个人电脑未普及时,大家去网吧玩游戏. 玩家:&qu ...

  2. 关于 Task 简单梳理

    〇.前言 Task 是微软在 .Net 4.0 时代推出来的,也是微软极力推荐的一种多线程的处理方式. 在 Task 之前有一个高效多线程操作累 ThreadPool,虽然线程池相对于 Thread, ...

  3. struct 结构体分析

    struct分析 1.无成员的空结构体size为 1byte 2.通过/zp可以调整对齐值,默认是8字节 //设编译对齐设定值为Zp //设成员变量的类型为 member type //设成员变量在结 ...

  4. 【go语言】2.1.3 函数的定义和使用

    在 Go 语言中,函数是一种代码抽象和复用的方式.函数可以接受参数,执行特定的操作,并返回结果. 函数的定义 函数的定义以 func 关键字开始,后面跟着函数名.参数列表.返回值列表(可选)以及函数体 ...

  5. WebSSH之录屏安全审计(三)

    第一篇:Gin+Xterm.js实现WebSSH远程Kubernetes Pod(一) 第二篇:WebSSH远程管理Linux服务器.Web终端窗口自适应(二) 支持用户名密码认证 支持SSH密钥认证 ...

  6. 解决:ValueError: Cannot mask with non-boolean array containing NA / NaN values

    错误原因:这里就是说,分组这一列里面,包含了非字符串的内容,比如数字.因为 .str.contains 的使用就要求这个字段必须是字符串,不能掺杂数字的. 解决方案: # 包含对应关系的所有行 dat ...

  7. 武汉工程大学第五届程序设计新生赛 I题 题解

    (2022,12,3) 原题链接(来自牛客竞赛) 抽象题意 题目有点长,我们需要抽象出一个模型: 一个长度为\(n\)的序列\(a_i\),从\(a_1\)开始向后跳,每次可以从\(a_i\)跳到下一 ...

  8. 更专业省心的来了,你没必要研究UE4和Unity官方推流了!

    在当今互联网时代,所有的内容制作者都希望尽可能触达到更多的目标受众,那就需要全平台发布内容并且可以轻松跨平台分享,包括手机.平板电脑.个人电脑以及交互式屏幕,用户能畅快的获得高质量的体验.需求催生了一 ...

  9. papricice

    2023-07-14 题目 题目传送门 题目大意 给定一个 \(n\) 个点的树,这 \(n\) 个点编号为 \(1\) 到 \(n\). 现在要选择断掉两条边,会形成三个连通块,假设这三个连通块内的 ...

  10. 三维模型OSGB格式轻量化顶点压缩主要技术方法分析

    三维模型OSGB格式轻量化顶点压缩主要技术方法分析 在三维模型应用中,轻量化处理是提高数据传输效率.减少渲染时间和优化用户体验的重要手段.而OSGB格式是一种常见的三维模型格式,在进行轻量化处理时,顶 ...