20230526----重返学习-TaskOA任务管理系统-其它Hook

本文详细介绍了在React项目中使用AntDesign的Form组件进行表单管理,包括自定义表单验证、Modal对话框的使用,以及axios的配置和错误处理。此外,还涉及到了Day.js时间格式化、服务端窗口管理和事件委托的原理。文章还讨论了useMemo和useCallbackHook在性能优化中的作用,以及自定义Hook的基本概念。
摘要由CSDN通过智能技术生成

day-078-seventy-eight-20230526-TaskOA任务管理系统-其它Hook

TaskOA任务管理系统

  • 对话弹框Modal

  • 在React项目中,如果遇到表单操作,如果不使用antd中的组件,自己去开发的步骤:

    <div>
      <label>任务描述:</label>
      <textarea
        value={xxx}
        onChange={(ev) => {
          setXxx(ev.target.value.trim());
        }}
        onBlur={() => {}}
      ></textarea>
    </div>
    
    1. 搭建结构写样式。
    2. 定义相关的状态,并且监听 onChange 事件,确保当表单中的内容改变,我们需要把对应的状态也更改!
    3. 还需要对每个表单做规则校验,包括最后提交的时候,所有的校验也要走一遍。
    4. 关闭Modal框的时候,还需要自己手动的清楚表单中的信息及校验信息。
    • antd中提供了<Form>组件,可以帮助我们快速进行表单操作,类似于实现了上述4个要做的事情
      • <Form><Form.Item>
        1. 除了结构样式外。
        2. 组件会自动收集每一个 Item 中,表单中输入的最新内容,并且可以给每个表单设置初始值「无需我们自己定义状态及基于onChange事件去监听内容的变化,从而修改状态值」。
        3. 并且提供了优秀的表单校验方案。
        4. 提供了大量方法,有触发表单校验的方法,也有清除表单信息及校验信息的方法!

Form组件

  • <Form><Form.Item>

    1. 除了结构样式外。
    2. 组件会自动收集每一个 Item 中,表单中输入的最新内容,并且可以给每个表单设置初始值「无需我们自己定义状态及基于onChange事件去监听内容的变化,从而修改状态值」。
    3. 并且提供了优秀的表单校验方案。
    4. 提供了大量方法,有触发表单校验的方法,也有清除表单信息及校验信息的方法!
  • 获取Form实例的方案:

    let formIns = Form.useForm()
    <Form colon={false} form={formIns}></Form>
    console.log(`确认新增任务`,formIns);
    // 清除表单信息及校验信息
    formIns.resetFields()
    
    1. 基于组件库提供的 form / Form.useForm()。

      1. 调用Form.useForm()提供的hook函数。

        import { Form } from 'antd'
        let [formIns] = Form.useForm()
        
      2. 在Form组件上的from属性上绑定。

        <Form  form={formIns} ></Form>
        
      3. 在函数中调用实例。

        console.log(formIns)
        
    2. 基于ref获取。

    • 获取实例后,就可基于其提供的方法,做一些事情!
  • <Form>就是基于<Form.Item>每一项的name,进行信息自动收集。

    1. Form的配置设置1
    收集的信息={
      task:'...',
      time:'...',
    }
    
  • <Form.Item>

  • Form表单校验

    1. 所有需要校验的表单,都必须在 <Form.Item> 中包含「每一个Item中正出现一个表单」
    2. 给每个ITEM设置 name 以及 基于 rules 设置校验的规则
      • 内置校验规则
      • 自定义校验「或者基于正则校验」
    3. 触发表单校验
      - 第一种: 获取到 formIns 实例,基于实例中的 validate… 方法去手动触发
      - 第二种:子自动触发
      • 例如:
        • 提交按钮的时候,自动触发
          • 按钮必须在 <Form>
          • 按钮的原生 type 类型必须是 submit
          • 只有这样,点击按钮的时候,才会自动触发Form表单校验
            • 成功:onFinish「参数中包含了Form收集的各个表单信息」
            • 失败:onFinishFailed
  • 校验规则:

    • 枚举:

      rules={[
        {
          type: "enum",
          enum: ["A", "B"],
          message: "不符合规则",
        },
      ]}
      
    • 自定义规则:

      rules={[
        {
          validator(_, value) {
            value = value.trim();
            if (value.length === 0) return Promise.reject("必填哈");
            return Promise.resolve();
          },
        },
      ]}
      
    • 正则规则:

      rules={[
        {
          pattern: /^.{1,5}$/,
          message: "呜呜呜呜",
        },
      ]}
      
    • 多个规则:

      rules={[
        {
          pattern: /^.{1,5}$/,
          message: "呜呜呜呜",
        },
        {
          required: true,
          message: "哈哈哈哈",
        },
      ]}
      
  • 表单相关的组件如何处理

    1. 结构样式如何调整
    2. 如何让from收集到各表单中信息
    3. 如何写校验规则,如果触发校验规则
    4. 如何拿到form实例,以及实例可以做什么

dayjs

服务端

  • 如果是用node的,那么一般先看package.json中的scripts。如果没有找到,就直接用node执行server.js之类的入口文件。

服务端窗口管理

  • 一般一个控制台开了,但里面的窗口可能在终端开了之后。一般用pm2来管理。
npm i pm2 -g //全局安装pm2,以便可以使用pm2命令。
pm2 start server.js --name TASK//开启一个的有名字的pm2服务。
pm2 stop TASK//停止执行一个有名字的pm2服务。
pm2 delete TASK//删除一个有名字的pm2服务。
pm2 restart TASK//重启一个有名字的pm2服务。
pm2 list //查看当前执行的pm2服务。

React的事件委托

因为React的事件绑定本身就是事件委托来做的,所以不必写成事件委托。

{["全部", "未完成", "已完成"].map((item, index) => {
  return (
    <Tag
      key={index}
      color={selectedIndex === index ? "#1677ff" : ""}
      onClick={() => {
        if(selectedIndex===index){
          return
        }
        setSelectedIndex(index)
      }}
    >
      全部
    </Tag>
  );
})}

axios的配置

  • 基于axios 发送请求,返回结果是一个 Promise 实例。
    1. 对于一个网络请求,用户及需求要求的是业务层成功。
    • 请求成功:

      1. 只是表示收到服务器返回的消息了,但服务器返回的消息会提示我们传的数据是否正确。
      2. 一般服务器表示是否成功,是通过一个字段code来约定的是否成功了。
      3. 一般到这一步,就算失败了,后端也会返回是因为什么原因导致的前后端交互失败,如给一个message字段。
      • 业务层成功(例如:返回数据中的 code===0)
      • 业务层失败
    • 请求失败「网络层失败 状态码不是以2开始的」promise实例是失败的:

      1. 这一步,就是网络的问题的。如服务器着火,请求地址不存在,请求地址写错了-不是后端给的那个地址。
      2. 到这一步,一般是根据状态码,如404与401或101之类的,由前端大概判断是因为什么导致的。
      3. 实际上,这一步大概率是前端或运维的问题。比如前端代码写错了,或者是运维服务器挂了。
      • 在axios二次封装的响应拦截器中,做了统一的处理!

        http.interceptors.response.use(response => {
          return response.data
        }, reason => {
          message.error('当前网络繁忙,请您稍后再试~')
          return Promise.reject(reason)
        })
        
    • 示例:

      // 从服务器获取指定状态的任务。
      const initData = async () => {
        setLoading(true);
        try {
          let { code, list } = await API.queryTaskList(selectedIndex);
      
          if (+code !== 0) {
            list = [];
          }
          setData(list);
        } catch (_) {
          console.log(_);
        }
        setLoading(false);
      };
      
      • 示例意思:

        // 从服务器获取指定状态的任务。
        
        // 对于一个网络请求,用户及需求要求的是业务层成功。
        const initData = async () => {
          //这里是请求发送前的代码。
          setLoading(true);
          try {
            let { code, list } = await API.queryTaskList(selectedIndex);
        
            //到这一步,就代表axios网络层成功了。运维基本上不用理了。
            //到这一步,需要前后端约定了,比如约定code等于0表示接口处理成功,即本次请求达成前端预期了。
        
            if (+code !== 0) {
              //这里处理axios网络层但业务层失败的任务。
              list = [];
            }
            
            //这里处理axios网络层并且业务层也成功的任务。
            setData(list);
          } catch (_) {
            //这里处理axios网络层失败的错误。
            console.log(_);
          }
        
          //这里是处理axios发送结束并且处理好各个请求错误之后的代码。
          setLoading(false);
        };
        

项目代码

  • /src/api/http.js axios实例默认配置:

    import axios from "axios"
    import qs from 'qs'
    import _ from '@/assets/utils'
    import { message } from 'antd'
    
    const http = axios.create({
        baseURL: '/api',
        timeout: 60000
    })
    
    http.defaults.transformRequest = data => {
        if (_.isPlainObject(data)) data = qs.stringify(data)
        return data
    }
    
    http.interceptors.response.use(response => {
        return response.data
    }, reason => {
        message.error('当前网络繁忙,请您稍后再试~')
        return Promise.reject(reason)
    })
    
    export default http
    
  • /src/api/index.js 使用axios实例的接口:

    import http from "./http";
    
    // 获取指定状态下的任务列表
    const queryTaskList = (state = 0) => {
      return http.get("/getTaskList", {
        params: {
          state,
        },
      });
    };
    
    // 新增任务
    const insertTaskInfo = (task, time) => {
      return http.post("/addTask", {
        task,
        time,
      });
    };
    
    // 删除任务
    const removeTaskById = (id) => {
      return http.get("/removeTask", {
        params: {
          id,
        },
      });
    };
    
    // 完成任务
    const updateTaskById = (id) => {
      return http.get("/completeTask", {
        params: {
          id,
        },
      });
    };
    
    /* 暴露API */
    const API = {
      queryTaskList,
      insertTaskInfo,
      removeTaskById,
      updateTaskById,
    };
    export default API;
    
  • /src/index.jsx 入口文件:

    import React from "react";
    import ReactDOM from "react-dom/client";
    /* ANTD */
    import { ConfigProvider } from "antd";
    import zhCN from "antd/locale/zh_CN";
    import dayjs from "dayjs";
    import "dayjs/locale/zh-cn";
    /* 组件&样式 */
    import "./index.less";
    // import Task from './views/Task'
    import Demo from "./views/Demo1";
    
    dayjs.locale("zh-cn");
    
    const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(
      <ConfigProvider locale={zhCN}>
        {/* <Task /> */}
        <Demo />
      </ConfigProvider>
    );
    
  • /src/views/Task.jsx 主要的代码:

    import { useState, useEffect } from "react";
    import styled from "styled-components";
    import {
      Button,
      Tag,
      Table,
      Popconfirm,
      Modal,
      Form,
      Input,
      DatePicker,
      message,
    } from "antd";
    import _ from "@/assets/utils";
    import dayjs from "dayjs";
    import API from "@/api";
    
    /* 组件样式 */
    const TaskStyle = styled.div`
      box-sizing: border-box;
      margin: 0 auto;
      width: 800px;
    
      .header-box {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 10px 0;
        border-bottom: 1px solid #ddd;
    
        .title {
          font-size: 20px;
          font-weight: normal;
        }
      }
    
      .tag-box {
        margin: 15px 0;
    
        .ant-tag {
          padding: 5px 15px;
          margin-right: 15px;
          font-size: 14px;
          cursor: pointer;
        }
      }
    
      .ant-btn-link {
        padding: 4px 5px;
      }
    
      .ant-modal-header {
        margin-bottom: 20px;
      }
    `;
    
    /* 组件视图 */
    export default function Task() {
      // 定义表格列
      const columns = [
        {
          title: "编号",
          dataIndex: "id",
          width: "6%",
          align: "center",
        },
        {
          title: "任务描述",
          dataIndex: "task",
          width: "50%",
          ellipsis: true,
        },
        {
          title: "状态",
          dataIndex: "state",
          width: "10%",
          align: "center",
          render: (text) => (+text === 1 ? "未完成" : "已完成"),
        },
        {
          title: "完成时间",
          width: "16%",
          align: "center",
          render(text, record) {
            let { state, time, complete } = record;
            time = +state === 1 ? time : complete;
            return _.formatTime(time, "{1}/{2} {3}:{4}");
          },
        },
        {
          title: "操作",
          width: "18%",
          render(text, record) {
            let { state, id } = record;
            return (
              <>
                <Popconfirm
                  title="您确定要删除此任务吗?"
                  onConfirm={handleRemove.bind(null, id)}
                >
                  <Button type="link" danger>
                    删除
                  </Button>
                </Popconfirm>
    
                {+state === 1 ? (
                  <Popconfirm
                    title="您确定要把此任务设置为已完成吗?"
                    onConfirm={handleUpdate.bind(null, id)}
                  >
                    <Button type="link">完成</Button>
                  </Popconfirm>
                ) : null}
              </>
            );
          },
        },
      ];
    
      // 定义状态
      let [data, setData] = useState([]),
        [loading, setLoading] = useState(false),
        [selectedIndex, setSelectedIndex] = useState(0);
      let [modalVisible, setModalVisible] = useState(false),
        [confirmLoading, setConfirmLoading] = useState(false);
      let [formIns] = Form.useForm();
    
      // 第一次渲染完&每一次选中状态改变,都要从服务器重新获取数据
      useEffect(() => {
        initData();
      }, [selectedIndex]);
    
      // 从服务器获取指定状态的任务
      const initData = async () => {
        setLoading(true);
        try {
          let { code, list } = await API.queryTaskList(selectedIndex);
          if (+code !== 0) list = [];
          setData(list);
        } catch (_) {}
        setLoading(false);
      };
    
      // 关闭Modal对话框
      const closeModal = () => {
        setModalVisible(false);
        setConfirmLoading(false);
        // 清除表单信息及校验信息
        formIns.resetFields();
      };
    
      // 确认新增任务
      const submit = async () => {
        try {
          await formIns.validateFields();
          let { task, time } = formIns.getFieldsValue();
          // time:是基于dayjs构建的日期对象
          time = time.format("YYYY-MM-DD HH:mm:ss");
          // 向服务器发送请求
          setConfirmLoading(true);
          let { code } = await API.insertTaskInfo(task, time);
          if (+code === 0) {
            message.success("恭喜您,新增成功~");
            closeModal();
            // 从服务器获取最新的任务列表
            initData();
          } else {
            message.error("很遗憾,新增失败,请稍后再试~");
          }
        } catch (_) {}
        setConfirmLoading(false);
      };
    
      // 删除任务
      const handleRemove = async (id) => {
        try {
          let { code } = await API.removeTaskById(id);
          if (+code === 0) {
            message.success("恭喜您,删除成功");
            // 让客户端页面中的数据也跟着删除
            // initData()
            let arr = data.filter((item) => +item.id !== +id);
            setData(arr);
            return;
          }
          message.error("很遗憾,删除失败,请稍后再试");
        } catch (_) {}
      };
    
      // 修改任务
      const handleUpdate = async (id) => {
        try {
          let { code } = await API.updateTaskById(id);
          if (+code === 0) {
            message.success("恭喜您,修改成功");
            // 让页面中的数据也会跟着变
            let arr = data.map((item) => {
              if (+item.id === +id) {
                item.state = 2;
                item.complete = dayjs().format("YYYY-MM-DD HH:mm:ss");
              }
              return item;
            });
            setData(arr);
            return;
          }
          message.error("很遗憾,修改失败,请稍后再试");
        } catch (_) {}
      };
    
      return (
        <TaskStyle>
          <div className="header-box">
            <h2 className="title">TASK OA 任务管理系统</h2>
            <Button type="primary" onClick={() => setModalVisible(true)}>
              新增任务
            </Button>
          </div>
    
          <div className="tag-box">
            {["全部", "未完成", "已完成"].map((item, index) => {
              return (
                <Tag
                  key={index}
                  color={selectedIndex === index ? "#1677ff" : ""}
                  onClick={() => {
                    if (selectedIndex === index) return;
                    setSelectedIndex(index);
                  }}
                >
                  {item}
                </Tag>
              );
            })}
          </div>
    
          <Table
            columns={columns}
            dataSource={data}
            loading={loading}
            pagination={false}
            rowKey="id"
            size="middle"
          />
    
          <Modal
            title="新增任务窗口"
            okText="确认提交"
            keyboard={false}
            maskClosable={false}
            getContainer={false}
            confirmLoading={confirmLoading}
            open={modalVisible}
            afterClose={closeModal}
            onCancel={closeModal}
            onOk={submit}
          >
            <Form
              colon={false}
              layout="vertical"
              validateTrigger="onBlur"
              form={formIns}
              initialValues={{
                task: "",
                time: dayjs().add(1, "day"),
              }}
            >
              <Form.Item
                name="task"
                label="任务描述"
                rules={[{ required: true, message: "任务描述是必填项" }]}
              >
                <Input.TextArea rows={4} />
              </Form.Item>
              <Form.Item
                name="time"
                label="预期完成时间"
                rules={[{ required: true, message: "请先选择预期完成时间" }]}
              >
                <DatePicker showTime />
              </Form.Item>
            </Form>
          </Modal>
        </TaskStyle>
      );
    }
    

项目总结

  1. 搭建结构:DOM骨架
    • antd组件库的运用
      • 按需加载与按需导入
      • 国际化-由英文化改为中文化
      • 核心组件
        • Table表格组件
          • 列的构建及格式化处理
          • 表格的分页
          • 列的选择
          • 列的排序
          • 列的筛选
        • Form表单
          • 数据的处理和收集
          • 规则校验及触发机制
            • 内置触发
            • 通过实例触发
            • 内置规则
            • 自定义规则
          • 实例及方法等
        • Modal与Dialog对话框
        • 文件上传等操作
    • 搭好骨架
  2. 写样式
    • 样式私有化的处理方案
    • UI组件库中样式的修改
    • 那些提取为全局样式
  3. 数据通信
    • 跨域配置
    • Axios的二次配置
    • API接口的管理

其它Hook

useMemo

  • 假设:z的值是依赖于x状态值计算出来的,而且非常消耗性能。

    import { Button } from "antd";
    import { useState, useMemo } from "react";
    import styled from "styled-components";
    
    // 一点破样式
    const DemoStyle = styled.div`
      box-sizing: border-box;
      margin: 20px auto;
      padding: 20px;
      width: 300px;
      border: 1px solid #ddd;
    
      p {
        font-size: 18px;
        line-height: 40px;
      }
    
      .ant-btn {
        margin-right: 10px;
      }
    `;
    
    export default function Demo() {
      let [x, setX] = useState(10),
        [y, setY] = useState(20);
    
      // 假设:z的值是依赖于x状态值计算出来的「而且非常消耗性能」
      console.time("AAA");
      let z = 0;
      new Array(9999999).fill(null).forEach(() => {
        // ...
        z += x / 10;
      });
      console.timeEnd("AAA");
      z = z.toFixed(2);
    
      return (
        <DemoStyle>
          <p>
            x:{x} -- z:{z} -- y:{y}
          </p>
          <Button type="primary" size="small" onClick={() => setX(x + 1)}>
            修改X
          </Button>
          <Button type="primary" size="small" onClick={() => setY(y + 1)}>
            修改Y
          </Button>
        </DemoStyle>
      );
    }
    
    • 所以就想要z只有在它所依赖的x变动之后才会重新进行计算。
  • useMemo就是在React中做优化的,类似于Vue中的computed计算属性;基于useMemo可以完成:

    • 组件第一次渲染,把useMemo中的callback执行,计算出一个值赋值给z。
      • 并且缓存这个值。
    • 以后组件的每一次更新,只有依赖的x状态发生改变,callback才会重新执行,计算出新的值;
      • 如果x状态没有变化,会使用之前缓存的值!
  • useMemo就是所谓的计算缓存。

    • 如果计算过程很简单,或者不需要缓存,就不必使用useMemo。

    • useMemo也比较耗性能。

    • 真实项目中,如果遇到依赖某个或者依赖某几个状态算出一个新的值这样的需求,并且计算过程有一些复杂,需要消耗很多时间或者性能,此时我们基于useMemo对其进行优化处理!

      import { Button } from "antd";
      import { useState, useMemo } from "react";
      import styled from "styled-components";
      
      // 一点破样式
      const DemoStyle = styled.div`
        box-sizing: border-box;
        margin: 20px auto;
        padding: 20px;
        width: 300px;
        border: 1px solid #ddd;
      
        p {
          font-size: 18px;
          line-height: 40px;
        }
      
        .ant-btn {
          margin-right: 10px;
        }
      `;
      
      export default function Demo() {
        let [x, setX] = useState(10),
          [y, setY] = useState(20);
      
        // 假设:z的值是依赖于x状态值计算出来的「而且非常消耗性能」
        // useMemo就是在React中做优化的,类似于Vue中的computed计算属性;基于useMemo可以完成:
        //  + 组件第一次渲染,把useMemo中的callback执行,计算出一个值赋值给z「并且缓存这个值」
        //  + 以后组件的每一次更新,只有依赖的x状态发生改变,callback才会重新执行,计算出新的值;如果x状态没有变化,会使用之前缓存的值!
        // ===> 计算缓存:真实项目中,如果遇到 “依赖某个或者某几个状态,算出一个新的值” 这样的需求,并且计算的过程有一些复杂,需要消耗很多时间或者性能,此时我们基于 useMemo 对其进行优化处理!
        let z = useMemo(() => {
          console.time("AAA");
          let z = 0;
          new Array(9999999).fill(null).forEach(() => {
            // ...
            z += x / 10;
          });
          console.timeEnd("AAA");
          return z.toFixed(2);
        }, [x]);
      
        return (
          <DemoStyle>
            <p>
            x:{x} -- z:{z} -- y:{y}
            </p>
            <Button type="primary" size="small" onClick={() => setX(x + 1)}>
              修改X
            </Button>
            <Button type="primary" size="small" onClick={() => setY(y + 1)}>
              修改Y
            </Button>
          </DemoStyle>
        );
      }
      

useCallback

父子组件处理流程
  • 父子组件处理流程:

    • 父组件第一次渲染:
      1. 父组件渲染之前 --> 父组件渲染 --> 遇到子组件 -->
      2. –> 子组件渲染之前 --> 子组件渲染 --> 子组件渲染完毕 -->
      3. –> 父组件结束子组件的渲染 --> 父组件渲染完毕。
    • 父组件后续更新:
      1. 父组件更新之前 --> 父组件更新 --> 遇到子组件 -->
      2. –> 子组件更新之前 --> 子组件更新 --> 子组件更新完毕 -->
      3. –> 父组件结束子组件的更新 --> 父组件更新完毕。
    • 和vue组件父子组件的更新
    • 处理的原则是:深度优先原则,只有把子组件先处理完毕,才能继续处理父组件。
      • 父组件第一次渲染,子组件也是第一次渲染。
      • 父组件进行更新,子组件也是进行更新。
  • 会发现父组件更新后,子组件必定也更新了。

    import { Button } from "antd";
    import { useState, useCallback, Component, PureComponent, memo } from "react";
    import styled from "styled-components";
    const DemoStyle = styled.div`
      box-sizing: border-box;
      margin: 20px auto;
      padding: 20px;
      width: 400px;
      border: 1px solid #ccc;
    
      p {
        font-size: 18px;
        line-height: 40px;
      }
    
      .ant-btn {
        margin-right: 10px;
      }
    `;
    
    class Child extends Component {
      render() {
        console.log(`子组件渲染与更新`);
        return <div>子组件</div>;
      }
    }
    export default function Demo() {
      let [x, setX] = useState(10);
      console.log(`父组件渲染与更新`);
      return (
        <DemoStyle className="demo-box">
          <Child></Child>
          <p>x:{x}</p>
          <Button type="primary" size="small" onClick={() => setX(x + 1)}>
            修改x
          </Button>
        </DemoStyle>
      );
    }
    
  • 会发现父组件如果更新,子组件却不更新。

    import { Button } from "antd";
    import { useState, useCallback, Component, PureComponent, memo } from "react";
    import styled from "styled-components";
    const DemoStyle = styled.div`
      box-sizing: border-box;
      margin: 20px auto;
      padding: 20px;
      width: 400px;
      border: 1px solid #ccc;
    
      p {
        font-size: 18px;
        line-height: 40px;
      }
    
      .ant-btn {
        margin-right: 10px;
      }
    `;
    class Child extends PureComponent {
      render() {
        console.log(`子组件渲染与更新`);
        return <div>子组件</div>;
      }
    }
    export default function Demo() {
      let [x, setX] = useState(10);
      console.log(`父组件渲染与更新`);
      return (
        <DemoStyle className="demo-box">
          <Child></Child>
          <p>x:{x}</p>
          <Button type="primary" size="small" onClick={() => setX(x + 1)}>
            修改x
          </Button>
        </DemoStyle>
      );
    }
    
    • 原因是因为子组件继承自React.PureComponent,React.PureComponent默认会比对前后的状态及父组件传递的props值。而由于父组件前后都没有传递props,而子组件的前后状态也都没变化,所以子组件不会重新生成虚拟DOM,进而不更新。

useCallback()钩子函数

useCallback()
  • 父组件每一次更新,都是把函数重新执行,产生一个新的闭包;闭包中的handle方法也会重新创建一个新的函数(新的堆内存);这样每一次传递给 Child 的属性值,都是新的堆内存地址;所以不论子组件继承的是 PureComponent 还是 Component ,每一次更新,其接收的属性值都是发生变化的,所以子组件也要更新!

    • 但是函数组件本应如此,只有每一次更新都创建新的函数出来,这样在函数中用的状态,才是当前闭包中的信息!

    • 如果每次更新,handle方法依然是初次渲染闭包一中的handle,那么在handle中,我们使用的状态,也依然是闭包一中的!

      import { Button } from "antd";
      import { useState, useCallback, PureComponent, memo } from "react";
      import styled from "styled-components";
      
      // 一点破样式
      const DemoStyle = styled.div`
        box-sizing: border-box;
        margin: 20px auto;
        padding: 20px;
        width: 300px;
        border: 1px solid #ddd;
      
        p {
          font-size: 18px;
          line-height: 40px;
        }
      
        .ant-btn {
          margin-right: 10px;
        }
      `;
      
      class Child extends PureComponent {
        render() {
          console.log("子组件渲染/更新");
          return <div>我是子组件</div>;
        }
      }
      
      export default function Demo() {
        console.log("父组件渲染/更新");
        let [x, setX] = useState(10);
        const handle = () => { }
      
        return (
          <DemoStyle>
            <Child handle={handle} />
            <p>{x}</p>
            <Button type="primary" size="small" onClick={() => setX(x + 1)}>
              修改X
            </Button>
          </DemoStyle>
        );
      }
      
  • useCallback:可以确保每一次组件更新,都拿到第一次创建函数的堆内存地址(也就是组件更新的时候,不会重新创建新的函数了,一直使用第一次创建的函数)。

    • useCallback(callback,[]) 只有第一次渲染创建新的函数,后期组件每次更新,获取的都是第一次创建的函数。

      import { Button } from "antd";
      import { useState, useCallback, PureComponent, memo } from "react";
      import styled from "styled-components";
      // 一点破样式
      const DemoStyle = styled.div`
        box-sizing: border-box;
        margin: 20px auto;
        padding: 20px;
        width: 300px;
        border: 1px solid #ddd;
      
        p {
          font-size: 18px;
          line-height: 40px;
        }
      
        .ant-btn {
          margin-right: 10px;
        }
      `;
      class Child extends PureComponent {
        render() {
          console.log("子组件渲染/更新");
          return <div>我是子组件</div>;
        }
      }
      export default function Demo() {
        console.log("父组件渲染/更新");
        let [x, setX] = useState(10);
        const handle = useCallback(() => {}, []);
        return (
          <DemoStyle>
            <Child handle={handle} />
            <p>{x}</p>
            <Button type="primary" size="small" onClick={() => setX(x + 1)}>
              修改X
            </Button>
          </DemoStyle>
        );
      }
      
      • 使用这种方式,会存在一个很严重的问题:函数中获取的状态值,也永远是闭包1中的。

      • 但是也会带来一种优化:如果我们把函数作为属性传递给子组件「前提:子组件内部也对新老属性做了比较,如果发现一样,则不允许更新(类组件继承PureComponent或者自己设置shouldComponentUpdate;函数组件基于React.memo处理即可)」,因为每一次获取的函数都是一致的,导致传递的属性也是一样的,则子组件不会再更新了。

        import { Button } from "antd";
        import { useState, useCallback, PureComponent, memo } from "react";
        import styled from "styled-components";
        // 一点破样式
        const DemoStyle = styled.div`
          box-sizing: border-box;
          margin: 20px auto;
          padding: 20px;
          width: 300px;
          border: 1px solid #ddd;
        
          p {
            font-size: 18px;
            line-height: 40px;
          }
        
          .ant-btn {
            margin-right: 10px;
          }
        `;
        // 函数组件中,如果想对新老属性做比较,从而控制视图更新或者不更新,需要使用 React.memo 方法
        const Child = memo(function Child(props) {
          console.log("子组件渲染/更新");
          return <div>我是子组件</div>;
        });
        export default function Demo() {
          console.log("父组件渲染/更新");
          let [x, setX] = useState(10);
          const handle = useCallback(() => {}, []);
          return (
            <DemoStyle>
              <Child handle={handle} />
              <p>{x}</p>
              <Button type="primary" size="small" onClick={() => setX(x + 1)}>
                修改X
              </Button>
            </DemoStyle>
          );
        }
        
        • 函数组件中,如果想对新老属性做比较,从而控制视图更新或者不更新,需要使用 React.memo 方法。
    • useCallback(callback,[x]) 只有第一次渲染和x状态更新,才会重新创建新的函数。

      • 这种写法没什么问题,就是用法不够简洁。
        • 同时如果如果改了钩子函数导致依赖了其它的状态,那么还要改useCallback的第二个参数中的依赖数组,如果不及时同步改,可能导致函数中使用的状态不是自己预期的,而是某次闭包中的值。
    • useCallback(callback) 和不设置useCallback一样,每一次渲染都会创建新的函数。

      • 这个一般没人使用,因为和不使用useCallback作用一样,但不写useCallback,会让代码更简洁。
测试经过
  • 如果给子组件传递一个在父组件中声明的变量,会发现子组件必定会在父组件更新的情况下触发更新,即便父组件的函数代码一直没变。

    import { Button } from "antd";
    import { useState, useCallback, Component, PureComponent, memo } from "react";
    import styled from "styled-components";
    const DemoStyle = styled.div`
      box-sizing: border-box;
      margin: 20px auto;
      padding: 20px;
      width: 400px;
      border: 1px solid #ccc;
    
      p {
        font-size: 18px;
        line-height: 40px;
      }
    
      .ant-btn {
        margin-right: 10px;
      }
    `;
    
    class Child extends PureComponent {
      render() {
        console.log(`子组件渲染与更新`);
        return <div>子组件</div>;
      }
    }
    export default function Demo() {
      let [x, setX] = useState(10);
      console.log(`父组件渲染与更新`);
    
      // 父组件的每一次更新,都是把函数重新执行,产生一个新的闭包,闭包中的handle方法也会重新创建一个新的函数,即新的堆内存。这样每一次传递给Child的属性值,都是新的堆内存地址。所以不论子组件继承的是PureComponent还是Component,每一次更新,其接收的属性值都是发生变化的,所以子组件也要更新。
      // 但是函数组件本应如此,只有每一次更新,都创建新的函数出来,这样在函数中所使用的状态,才是当前闭包中的信息!
      // 如果每次更新,handle方法依然是闭包一中的handle,那么在handle中,我们使用的状态,也依然是闭包一中的状态!
      const handle = () => {
        console.log(x);
      };
      handle();
      return (
        <DemoStyle className="demo-box">
          <Child handle={handle}></Child>
          <p>x:{x}</p>
          <Button type="primary" size="small" onClick={() => setX(x + 1)}>
            修改x
          </Button>
        </DemoStyle>
      );
    }
    
    • 父组件的每一次更新,都是把函数重新执行,产生一个新的闭包,闭包中的handle方法也会重新创建一个新的函数,即新的堆内存。这样每一次传递给Child的属性值,都是新的堆内存地址。所以不论子组件继承的是PureComponent还是Component,每一次更新,其接收的属性值都是发生变化的,所以子组件也要更新。
      • 但是函数组件本应如此,只有每一次更新,都创建新的函数出来,这样在函数中所使用的状态,才是当前闭包中的信息!
        • 如果每次更新,handle方法依然是闭包一中的handle,那么在handle中,我们使用的状态,也依然是闭包一中的状态!
  • useCallback:可以确保每一次组件更新,都拿到第一次创建函数的堆内存地址(也就是组件更新的时候,不会重新创建新的函数了,一直使用第一次创建的函数)

    • 当子组件为类组件时,且做了更新时比对优化:

      import { Button } from "antd";
      import { useState, useCallback, Component, PureComponent, memo } from "react";
      import styled from "styled-components";
      const DemoStyle = styled.div`
        box-sizing: border-box;
        margin: 20px auto;
        padding: 20px;
        width: 400px;
        border: 1px solid #ccc;
      
        p {
          font-size: 18px;
          line-height: 40px;
        }
      
        .ant-btn {
          margin-right: 10px;
        }
      `;
      class Child extends PureComponent {
        render() {
          console.log(`子组件渲染与更新`);
          return <div>子组件</div>;
        }
      }
      export default function Demo() {
        let [x, setX] = useState(10);
        console.log(`父组件渲染与更新`);
      
        // useCallback:可以确保每一次组件更新,都拿到第一次创建函数的堆内存地址(也就是组件更新的时候,不会重新创建新的函数了,一直使用第一次创建的函数)
        const handle = useCallback(() => {
          console.log(x);
        }, []);
        handle();
        return (
          <DemoStyle className="demo-box">
            <Child handle={handle}></Child>
            <p>x:{x}</p>
            <Button type="primary" size="small" onClick={() => setX(x + 1)}>
              修改x
            </Button>
          </DemoStyle>
        );
      }
      
      • 发现父组件渲染时,子组件可以不更新。
    • 当子组件为函数组件时,单纯使用useCallback没有什么用,如果父组件更新,则子组件必定更新:

      import { Button } from "antd";
      import { useState, useCallback, Component, PureComponent, memo } from "react";
      import styled from "styled-components";
      const DemoStyle = styled.div`
        box-sizing: border-box;
        margin: 20px auto;
        padding: 20px;
        width: 400px;
        border: 1px solid #ccc;
      
        p {
          font-size: 18px;
          line-height: 40px;
        }
      
        .ant-btn {
          margin-right: 10px;
        }
      `;
      const Child = function Child() {
        console.log(`子组件渲染与更新`);
        return <div>子组件</div>;
      };
      export default function Demo() {
        let [x, setX] = useState(10);
        console.log(`父组件渲染与更新`);
        const handle = useCallback(() => {
          console.log(`x:${x}`);
        }, []);
        return (
          <DemoStyle className="demo-box">
            <Child handle={handle}></Child>
            <p>x:{x}</p>
            <Button type="primary" size="small" onClick={() => setX(x + 1)}>
              修改x
            </Button>
          </DemoStyle>
        );
      }
      
      • 这个是因为函数组件,必定是会执行一遍,所以导致子组件必定更新。
    • 子组件为函数组件时,使用React.useCallback配合React.memo一起使用:

      • 子组件可以在父组件时不必强制被连带更新。

        import { Button } from "antd";
        import { useState, useCallback, Component, PureComponent, memo } from "react";
        import styled from "styled-components";
        const DemoStyle = styled.div`
          box-sizing: border-box;
          margin: 20px auto;
          padding: 20px;
          width: 400px;
          border: 1px solid #ccc;
        
          p {
            font-size: 18px;
            line-height: 40px;
          }
        
          .ant-btn {
            margin-right: 10px;
          }
        `;
        const Child = memo(function Child() {
          console.log(`子组件渲染与更新`);
          return <div>子组件</div>;
        });
        export default function Demo() {
          let [x, setX] = useState(10);
          console.log(`父组件渲染与更新`);
          const handle = useCallback(() => {
            console.log(`x:${x}`);
          }, []);
          return (
            <DemoStyle className="demo-box">
              <Child handle={handle}></Child>
              <p>x:{x}</p>
              <Button type="primary" size="small" onClick={() => setX(x + 1)}>
                修改x
              </Button>
            </DemoStyle>
          );
        }
        
      • 因为React.memo()可以对父组件前后传的props进行前后比对,以便判断是否应该强制更新实际的子组件函数的数据。

自定义hook简易说明

  • 自定义hook:
    • 表现来看,就是一个以use开头的函数组件。

      import { Button } from "antd";
      import { useState, useCallback,} from "react";
      import styled from "styled-components";
      const DemoStyle = styled.div`
        box-sizing: border-box;
        margin: 20px auto;
        padding: 20px;
        width: 400px;
        border: 1px solid #ccc;
      
        p {
          font-size: 18px;
          line-height: 40px;
        }
      
        .ant-btn {
          margin-right: 10px;
        }
      `;
      const useChaoDuan = function useChaoDuan() {
        return [10, 20];
      };
      export default function Demo() {
        let [x, setX] = useState(10);
        let [m, n] = useChaoDuan();
        console.log(`父组件渲染与更新`,m, n);
        return (
          <DemoStyle className="demo-box">
            <p>x:{x}</p>
            <Button type="primary" size="small" onClick={() => setX(x + 1)}>
              修改x
            </Button>
          </DemoStyle>
        );
      }
      

进阶参考

  1. Modal;
  2. Form.Item;
  3. Day.js官方中文文档
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值