react学习

news2024/11/19 19:17:43

1. React概述

1.1 什么是react?

React 是一个用于构建用户界面的 JavaScript 库
用户界面:HTML页面(前端)
React 主要用来写HTML页面,或构建Web应用如果从 MVC 的角度来看,React 仅仅是视图层(V),也就是只负责视图的染,而并非提供了完整的M和C的功能
React 起源于Facebook 的内部项目,后又用来架设Instagram 的网站,并于2013年5月开源

 1.1react特点

1.声明式

        你只需要描述 UI(HTML)看起来是什么样,就跟写HTML一样React负责渲染 UI,并在数据变化时更新 UI

const jsx = <div className="app">
    <h1>Hello React!动态变化数据:(countj</h1>
</div>

2.基于组件

  • 组件是 React 最重要的内容
  • 组件表示页面中的部分内容
  • 组合、复用多个组件,可以实现完整的页面功能

 3.学习一次 随处可用

  • 使用 React 可以开发Web应用
  • 使用 React 可以开发移动端原生应用(react-native)
  • 使用 React 可以开发VR(虚拟现实)应用(react 360)

2. React基本使用

2.1 React安装

安装命令:npm i react react-dom

  • react 包是核心,提供创建元素、组件等功能
  • react-dom 包提供 DOM 相关功能等

 2.2 React使用

1. 引入 react 和 react-dom 两个 js 文件

2. 创建 React 元素

3. 渲染 React 元素到页面中

<div id="root"></div>
<script>
    const title = React.createElement('h1',null,'hello React')
    ReactDOM.render(title, document.getElementById('root'))
</script>

2.2 方法说明

React.createElement() 说明(知道)

// 返回值:React元素

// 第一个参数:要创建的React元素名称

// 第二个参数:该React元素的属性

// 第三个及其以后的参数:该React元素的子节点 const el = React.createElement('h1', { title: '标题' }, 'Hello React')

ReactDOM.render() 说明 

// 第一个参数:要渲染的React元素

// 第二个参数:DOM对象,用于指定渲染到页面中的位置

ReactDOM.render(el, document.getElementById('root'))

2.3 CDN方式

    <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

<script type="text/babel">
    书写react内容
</script>

3. React 脚手架的使用

3.1 React 脚手架意义

  1. 脚手架是开发 现代Web 应用的必备。
  2. 充分利用 Webpack、Babel、ESLint 等工具辅助项目开发。
  3. 零配置,无需手动配置繁琐的工具即可使用。
  4. 关注业务,而不是工具配置。

3.2 使用 React 脚手架初始化项目

1. 初始化项目,命令:npx create-react-app my-app

2. 启动项目,在项目根目录执行命令:npm start

npx 命令介绍 

  • npm v5.2.0 引入的一条命令
  • 目的:提升包内提供的命令行工具的使用体验 
  • 原来:先安装脚手架包,再使用这个包中提供的命令
  • 现在:无需安装脚手架包,就可以直接使用这个包提供的命令

补充说明

1. 推荐使用:npx create-react-app my-app

2. npm init react-app my-app

3. yarn create react-app my-app

  • yarn 是 Facebook 发布的包管理器,可以看做是 npm 的替代品,功能与 npm 相同
  • yarn 具有快速、可靠和安全的特点
  • 初始化新项目:yarn init
  • 安装包: yarn add 包名称
  • 安装项目依赖项: yarn
  • 其他命令,请参考yarn文档

3.3 在脚手架中使用 React

1. 导入 react 和 react-dom 两个包。

import React from 'react'

import ReactDOM from 'react-dom'

2. 调用 React.createElement() 方法创建 react 元素。

3. 调用 ReactDOM.render() 方法渲染 react 元素到页面中。 

基础总结

React 基础

  1. React是构建用户界面的JavaScript库
  2. 使用 react 时,推荐使用脚手架方式。
  3. 初始化项目命令:npx create-react-app my-app 。
  4. 启动项目命令:yarn start(或 npm start)。
  5. React.createElement() 方法用于创建 react 元素(知道)。
  6. ReactDOM.render() 方法负责渲染 react 元素到页面中。

react(B站李立超-18)

1. hello world

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Hello World</title>
    <!--引入react的核心库-->
    <script src="script/react.development.js"></script>
    <!--引入react的DOM库-->
    <script src="script/react-dom.development.js"></script>
</head>
<body>

<div id="root"></div>

<script>
    /*
    *   React就是用来代替DOM的
    * */

    // 通过DOM向页面中添加一个div
    // 创建一个div
    // const div = document.createElement('div'); // 创建一个dom元素
    // // 向div中设置内容
    // div.innerText = '我是一个div';
    // // 获取root
    // const root = document.getElementById('root');
    // // 将div添加到页面中
    // root.appendChild(div);


    // 通过React向页面中添加一个div
    /*
    *   React.createElement()
    *       - 用来创建一个React元素
    *       - 参数:
    *           1. 元素名(组件名)
    *           2. 元素中的属性
    *           3. 元素的子元素(内容)
    * */
    const div = React.createElement('div', {}, '我是React创建的div'); // 创建一个React元素

    // 获取根元素对应的React元素
    //  ReactDOM.createRoot() 用来创建React根元素,需要一个DOM元素作为参数
    const root = ReactDOM.createRoot(document.getElementById('root'));

    // 将div渲染到根元素中
    root.render(div);



</script>
</body>
</html>

2. 3个API

  1. React.createElement
  2. ReactDOM.createRoot
  3. ReactDOM.createRoot 的render方法

    /*
    * React.createElement()
    *   - 用来创建一个React元素
    *   - 参数:
    *        1.元素的名称(html标签必须小写)
    *        2.标签中的属性
    *           - class属性需要使用className来设置
    *           - 在设置事件时,属性名需要修改为驼峰命名法
    *       3.元素的内容(子元素)
    *   - 注意点:
    *       React元素最终会通过虚拟DOM转换为真实的DOM元素
    *       React元素一旦创建就无法修改,只能通过新创建的元素进行替换
    * */

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>三个API</title>
    <script src="script/react.development.js"></script>
    <script src="script/react-dom.development.js"></script>
</head>
<body>
<button id="btn">我是按钮</button>
<div id="root"></div>

<script>
    // 创建一个React元素
    const button = React.createElement('button', {
        type: 'button',
        className: 'hello',
        onClick: () => {
            alert('你点我干嘛')
        }
    }, '点我一下');

    // 创建第一个div
    const div = React.createElement('div', {}, '我是一个div', button);

    // 获取根元素
    const root = ReactDOM.createRoot(document.getElementById('root'));

    // 将元素在根元素中显示
    root.render(div);

    // 获取按钮对象
    const btn = document.getElementById('btn');
    btn.addEventListener('click', ()=>{
        // 点击按钮后,修改div中button的文字为click me
        const button = React.createElement('button', {
            type: 'button',
            className: 'hello',
            onClick: () => {
                alert('你点我干嘛')
            }
        }, 'click me');

        // 创建一个div
        const div = React.createElement('div', {}, '我是一个div', button);

        // 修改React元素后,必须重新对根元素进行渲染
        // 当调用render渲染页面时,React会自动比较两次渲染的元素,只在真实DOM中更新发生变化的部分
        //      没发生变化的保持不变
        root.render(div);
    });
</script>

</body>
</html>

   /*
    *   root.render()
    *       - 用来将React元素渲染到根元素中
    *       - 根元素中所有的内容都会被删除,被React元素所替换
    *       - 当重复调用render()时,React会将两次的渲染结果进行比较,
    *           它会确保只修改那些发生变化的元素,对DOM做最少的修改
    * */

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>三个API</title>
    <script src="script/react.development.js"></script>
    <script src="script/react-dom.development.js"></script>
</head>
<body>
<div id="root">
</div>

<script>

    // 创建一个React元素
    const button = React.createElement('button', {
        type: 'button',
        className: 'hello',
        onClick: () => {
            alert('你点我干嘛')
        }
    }, '点我一下');

    // 创建第一个div
    const div = React.createElement('div', {}, '我是一个div2', button);


    // ReactDOM.render(div, document.getElementById('root')); // 老版本的React中使用方法

    // 获取根元素 根元素就是React元素要插入的位置
    const root = ReactDOM.createRoot(document.getElementById('root'));

    // 将元素在根元素中显示
 
    root.render(div);

</script>

</body>
</html>

3. JSX

<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <title>JSX</title>
    <script src="script/react.development.js"></script>
    <script src="script/react-dom.development.js"></script>
    <!-- 引入babel -->
    <script src="script/babel.min.js"></script>
</head>

<body>
    <div id="root"></div>

    <!--设置js代码被babel处理-->
    <script type="text/babel">

         创建一个React元素 <button>我是按钮</button>
         命令式编程
         const button = React.createElement('button', {}, '我是按钮');

         声明式编程,结果导向的编程
         在React中可以通过JSX(JS扩展)来创建React元素,JSX需要被翻译为JS代码,才能被React执行
         要在React中使用JSX,必须引入babel来完成“翻译”工作
         const button = <button>我是按钮</button>; // React.createElement('button', {}, '我是按钮');

        /*
        *   JSX就是React.createElement()的语法糖
        *       JSX在执行之前都会被babel转换为js代码
        * */
        const div = <div>
            我是一个div
            <button>我是按钮</button>
        </div>;

        const root = ReactDOM.createRoot(document.getElementById('root'));
        root.render(div);

    </script>
</body>

</html>

    /*

    *   JSX的注意事项

    *       1. JSX不是字符串,不要加引号

    *       2. JSX中html标签应该小写,React组件应该大写开头

    *       3. JSX中有且只有一个根标签

    *       4. JSX的标签必须正确结束(自结束标签必须写/)

    *       5. 在JSX中可以使用{}嵌入表达式

    *              - 有值的语句的就是表达式

    *       6. 如果表达式是空值、布尔值、undefined,将不会显示

    *       7. 在JSX中,属性可以直接在标签中设置

    *           注意:

    *               class需要使用className代替

    *               style中必须使用对象设置

    *                   style={{background:'red'}}

    * */

<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <title>JSX的注意</title>
    <script src="script/react.development.js"></script>
    <script src="script/react-dom.development.js"></script>
    <!-- 引入babel -->
    <script src="script/babel.min.js"></script>
</head>

<body>
    <div id="root"></div>

    <script type="text/babel">
        function fn() {
            return 'hello';
        }

        const name = '孙悟空';

        const div = <div
            id="box"

            onClick={() => {
                alert('哈哈')
            }} className="box1"

            style={{ backgroundColor: "yellowgreen", border: '10px red solid' }}
        >
            我是一个div
            <ul>
                <li>列表项</li>
            </ul>

            <input type="text" />

            <div>
                {name} <br />
                {1 + 1} <br />
                {fn()} <br />
                {NaN} <br />
            </div>
        </div>;

        // alert(div);

        const root = ReactDOM.createRoot(document.getElementById('root'));
        root.render(div);
    </script>
</body>

</html>

 4. 列表渲染

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>渲染列表</title>
    <script src="script/react.development.js"></script>
    <script src="script/react-dom.development.js"></script>
    <!-- 引入babel -->
    <script src="script/babel.min.js"></script>
</head>
<body>
<div id="root"></div>

<script type="text/babel">

    const name = '孙悟空';
    const lang = 'cn';

    /*
    *   {} 只能用来放js表达式,而不能放语句(if for)
    *       在语句中是可以去操作JSX
    * */
    // const div = <div>Hello {name}</div>;

    let div;

    if(lang === 'en'){
        div = <div>hello {name}</div>;
    }else if(lang === 'cn'){
        div = <div>你好 {name}</div>;
    }

    const data = ['孙悟空', '猪八戒', '沙和尚'];

    /*
        <ul>
             <li>孙悟空</li>
             <li>猪八戒</li>
            ...
        </ul>

        [<li>孙悟空</li>, <li>猪八戒</li>, <li>沙和尚</li>]
    * */

    // const arr = [];

    // 遍历data
    // for(let i=0; i<data.length; i++){
    //     arr.push(<li>{data[i]}</li>);
    // }

    // const arr = data.map(item => <li>{item}</li>);


    // 将arr渲染为一个列表在网页中显示
    // jsx中会自动将数组中的元素在页面中显示
    // const list = <ul>{arr}</ul>;

    const list = <ul>{data.map(item => <li>{item}</li>)}</ul>;

    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(list);

</script>
</body>
</html>

5. 虚拟DOM

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>虚拟DOM</title>
    <script src="script/react.development.js"></script>
    <script src="script/react-dom.development.js"></script>
    <script src="script/babel.min.js"></script>
</head>
<body>
<button id="btn">点我一下</button>
<hr>
<div id="root"></div>

<script type="text/babel">

    //创建一个数据
    const data = ['孙悟空', '猪八戒', '沙和尚'];

    // 创建列表
    const list = <ul>
        {/*data.map(item => <li key={item}>{item}</li>)*/}
        {data.map((item, index) => <li key={index}>{item}</li>)}
    </ul>;

    // 获取根元素
    const root = ReactDOM.createRoot(document.getElementById('root'));
    // 渲染元素
    root.render(list);

    /*
    *   在React我们操作的元素被称为React元素,并不是真正的原生DOM元素,
    *       React通过虚拟DOM 将React元素 和 原生DOM,进行映射,虽然操作的React元素,但是这些操作最终都会在真实DOM中体现出来
    *       虚拟DOM的好处:
    *           1.降低API复杂度
    *           2.解决兼容问题
    *           3.提升性能(减少DOM的不必要操作)
    *
    *   每当我们调用root.render()时,页面就会发生重新渲染
    *       React会通过diffing算法,将新的元素和旧的元素进行比较
    *       通过比较找到发生变化的元素,并且只对变化的元素进行修改,没有发生的变化不予处理
    * */
    document.getElementById('btn').onclick = function (){
        // 重新渲染页面
        //创建一个数据
        const data = ['唐僧', '孙悟空', '猪八戒', '沙和尚'];
        // 创建列表
        const list = <ul>
            {/*data.map(item => <li key={item}>{item}</li>)*/}
            {data.map((item, index) => <li key={index}>{item}</li>)}
        </ul>;
        // 渲染元素
        root.render(list);

        /*
        *   旧数据
        *       ul
        *           li>孙悟空
        *           li>猪八戒
        *           li>沙和尚
        *  新数据
        *       ul
        *           li>孙悟空
        *           li>猪八戒
        *           li>沙和尚
        *   比较两次数据时,React会先比较父元素,父元素如果不同,直接所有元素全部替换
        *       父元素一致,在去逐个比较子元素,直到找到所有发生变化的元素为止
        *   上例中,新旧两组数据完全一致,所以没有任何DOM对象被修改
        *
        *
        * 旧数据
        *       ul
        *           li>孙悟空
        *           li>猪八戒
        *           li>沙和尚
        *  新数据
        *       ul
        *           li>tom
        *           li>猪八戒
        *           li>沙和尚
        *
        *  上例中,只有第一个li发生变化,所以只有第一个li被修改,其他元素不变
        *
        *  当我们在JSX中显示数组中,数组中每一个元素都需要设置一个唯一key,否则控制台会显示红色警告
        *       重新渲染页面时,React会按照顺序依次比较对应的元素,当渲染一个列表时如果不指定key,同样也会按照顺序进行比较,
        *       如果列表的顺序永远不会发生变化,这么做当然没有问题,但是如果列表的顺序会发生变化,这可能会导致性能问题出现
        *
        *
        *   旧数据
        *       ul
        *           li>孙悟空
        *           li>猪八戒
        *           li>沙和尚
        *   新数据
        *       ul
        *           li>孙悟空
        *           li>猪八戒
        *           li>沙和尚
        *           li>唐僧
        *
        *   上例中,在列表的最后添加了一个新元素,并没有改变其他的元素的顺序,所以这种操作不会带来性能问题
        *
        *
        *
        *   旧数据
        *       ul
        *           li>孙悟空
        *           li>猪八戒
        *           li>沙和尚
        *   新数据
        *       ul
        *           li>唐僧
        *           li>孙悟空
        *           li>猪八戒
        *           li>沙和尚
        *
        *   上例中,在列表的最前边插入了一个新元素,其他元素内容并没有发生变化,
        *       但是由于新元素插入到了开始位置,其余元素的位置全都发生变化,而React默认是根据位置比较元素
        *       所以 此时,所有元素都会被修改
        *
        *   为了解决这个问题,React为列表设计了一个key属性,
        *       key的作用相当于ID,只是无法在页面中查看,当设置key以后,再比较元素时,
        *       就会比较相同key的元素,而不是按照顺序进行比较
        *   在渲染一个列表时,通常会给列表项设置一个唯一的key来避免上述问题
        *       (这个key在当前列表中唯一即可)
        *       注意:
        *           1.开发中一般会采用数据的id作为key
        *           2.尽量不要使用元素的index作为key
        *               索引会跟着元素顺序的改变而改变,所以使用索引做key跟没有key是一样的
        *                   唯一的不同就是,控制台的警告没了
        *               当元素的顺序不会发生变化时,用索引做key也没有什么问题
        *   旧数据
        *       ul
        *           li(key=孙悟空)>孙悟空
        *           li(key=猪八戒)>猪八戒
        *           li(key=沙和尚)>沙和尚
        *   新数据
        *       ul
        *           li(key=唐僧)>唐僧
        *           li(key=孙悟空)>孙悟空
        *           li(key=猪八戒)>猪八戒
        *           li(key=沙和尚)>沙和尚
        * */
    };



</script>
</body>
</html>

6. 手动创建React

项目结构


常规的React项目需要使用npm(或yarn) 作为包管理器来对项目进行管理。并且React官方为了方便我们的开发,为我们提供react-scripts包。包中提供了项目开发中的大部分依赖,大大的简化了项目的开发


开发步骤:

  • 创建目录结构

根元素 

        - public

                - index.html

        - src

                - App.js

                - index.js

...

  • 进入项目所在目录,并执行命令: npm init -yyarn init -y
  • 安装项目依赖: npm install readt react-dom react-scripts - S 或 yarn add reactreact-dom react-scripts
  • 运行npx react-scripts start 启动项目 (初次启动需要输入y确认)
  • 或者将react-scripts start 设置到package.json 的scripts选项中,然后通过npm start启动 (初次启动需要输入y确认)"scripts":[“start":“react-scripts start”了 

7. 练习 学习记录器


js部分:

import React from "react";
import ReactDOM from "react-dom/client";

// 引入css 
import "./index.css"

// 创建 JSX

const App = <div className="logs">
    {/* 日志项容器 */}
    <div className="item">
        {/* 日期容器 */}
        <div className="date">
            <div className="month">四月</div>
            <div className="day">19</div>
        </div>
        {/* 日志内容容器  */}
        <div className="content">
            <h2 className="desc">学习React</h2>
            <div className="time">40分钟</div>
        </div>
    </div>

</div>

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(App)

css部分:

/*设置基本样式*/
*{
    box-sizing: border-box;
}

/*设置body的样式*/
body{
    background-color: #DFDFDF;
    margin: 0;
}

/*设置外层容器logs的样式*/
.logs{
    width: 800px;
    margin: 50px auto;
    padding: 20px;
    background-color: #EAE2B7;
    border-radius: 10px;
    box-shadow: 0 0 10px rgba(0,0,0,.2);
}

/*设置item的样式*/
.item{
    /*开启弹性盒*/
    display: flex;
    margin: 16px 0;
    padding: 6px;
    background-color: #FCBF49;
    border-radius: 10px;
    box-shadow: 0 0 10px rgba(0,0,0,.2);
}

/*设置日期的样式*/
.date{
    width: 90px;
    background-color: #fff;
    border-radius: 10px;
    font-weight: bold;
    text-align: center;
    overflow: hidden;
}

/*设置月份效果*/
.month{
    height: 30px;
    line-height: 30px;
    font-size: 20px;
    color: #fff;
    background-color: #D62828;
}

/*设置日期的效果*/
.day{
    height: 60px;
    line-height: 60px;
    font-size: 50px;
}

/*设置日志内容的样式*/
.content{
    flex: auto;
    text-align: center;
    font-weight: bold;
}

/*设置描述内容*/
.desc{
    font-size: 16px;
    color: #194B49;
}

/*设置学习时间*/
.time{
    color: #D62828;
}

8. 组件

在React中网页被拆分为了一个一个组件,组件是独立可复用的代码片段。具体来说,组件可能是页面中的一个按钮,一个对话框,一个弹出层等。React中定义组件的方式有两种: 基于函数的组件和基于类的组件。本书我们先看看基于函数的组件。


基于函数的组件其实就是一个会返回JSX (React元素) 的普通的JS函数,你可以这样定义:

 函数组件

const App = () => {
  return <div>我是App组件!</div>
};

// 导出App
export default App;





*   组件

*       - React中组件有两种创建方式

*       - 函数式组件

*           - 函数组件就是一个返回JSX的普通

*           - 组件的首字母必须是大写

*       - 类组件

import React from 'react'
import ReactDOM from "react-dom/client";
import App from "./App";


const root = ReactDOM.createRoot(document.getElementById('root'));

// React组件可以直接通过JSX渲染
root.render(<App />);

类组件

import React from "react";


*   类组件必须要继承React.Component
*       相较于函数组件,类组件的编写要麻烦一下,
*           但是他俩的功能是一样的

class App extends React.Component{

    // 类组件中,必须添加一个render()方法,且方法的返回值要是一个jsx
    render() {
        return <div>我是一个类组件</div>;
    }
}

// 导出App
export default App;



import ReactDOM from "react-dom/client";
import App from "./App";


const root = ReactDOM.createRoot(document.getElementById('root'));

// React组件可以直接通过JSX渲染
root.render(<App/>);

9. 组件练习

组件化的方式 

10. 事件 

React中的事件处理类似于在HTML标签中通过属性来设置事件,像是这样:

<button οnclick="alert("你点我干嗨");">点我一下</button>

这是传统DOM中绑定事件的方式之一,onclick全都小写是事件的名字,它的值是一组JS代码,当事件触发时,JS代码便会执行。记住这一点,传统DOM中事件属性的JS代码在事件触发时执行!

React中的事件绑定是这样的: 

App.js:

const App = () => {

    const clickHandler = (event) => {
        event.preventDefault(); // 取消默认行为
        event.stopPropagation(); // 取消事件的冒泡

        alert('我是App中的clickHandler!');
        
        *     在React中,无法通过return false取消默认行为
        *     return false;
        *
        *     事件对象
        *         - React事件中同样会传递事件对象,可以在响应函数中定义参数来接收事件对象
        *         - React中的事件对象同样不是原生的事件对象,是经过React包装后的事件对象
        *         - 由于对象进行过包装,所以使用过程中我们无需再去考虑兼容性问题
        
    };


    return <div
        onClick={() => {
            alert('div');
        }}
        style={{width: 200, height: 200, margin: '100px auto', backgroundColor:'#bfa'}}>

        {/*
      在React中事件需要通过元素的属性来设置,
        和原生JS不同,在React中事件的属性需要使用驼峰命名法:
           onclick -> onClick
           onchange -> onChange
        属性值不能直接执行代码,而是需要一个回调函数:
          onclick="alert(123)"
          onClick={()=>{alert(123)}}
    */}
        <button onClick={() => {
            alert(123);
        }}>点我一下
        </button>
        <button onClick={clickHandler}>哈哈</button>
        <br/>
        <a href="https://www.baidu.com" onClick={clickHandler}>超链接</a>
    </div>
};

/*
*   <button onclick="alert(123)">点我一下</button>
*
*   <button id="btn01">点我一下</button>
*
*   document.getElementById('btn01').onclick = function(){};
*   document.getElementById('btn01').addEventListener('click', function(){}, false);
*
*
* */

// 导出App
export default App;

index.js 入口js:

import ReactDOM from "react-dom/client";
import App from "./App";
import './index.css';

const root = ReactDOM.createRoot(document.getElementById('root'));

// React组件可以直接通过JSX渲染
root.render(<App/>);

11. props

  1. 父组件在子组件标签上通过 属性={} 的方式传递给子组件
  2. 子组件通过 props 形参接收,通过 {props.属性名} 的方式使用
  3. props是只读,不能修改

初识 MDN - MDN 项目 | MDN

logs组件:

/* 日志的容器 */
import LogItem from "./LogItem/LogItem";
import './Logs.css';

const Logs = () => {

    // 模拟一组从服务器中加载的数据
    const logsData = [
        {
            id: '001',
            date: new Date(2021, 1, 20, 18, 30),
            desc: '学习九阳神功',
            time: 30
        },
        {
            id: '002',
            date: new Date(2022, 2, 10, 12, 30),
            desc: '学习降龙十八掌',
            time: 20
        },
        {
            id: '003',
            date: new Date(2022, 2, 11, 11, 30),
            desc: '学习JavaScript',
            time: 40
        },
        {
            id: '004',
            date: new Date(2022, 2, 15, 10, 30),
            desc: '学习React',
            time: 80
        }
    ];

    // 将数据放入JSX中
    const logItemDate = logsData.map(item =>
         <LogItem key={item.id} date={item.date} desc={item.desc} time={item.time}/>);

    return <div className="logs">
        {
            logItemDate
            // logsData.map(item => <LogItem {...item}/> )
        }
    </div>
};

export default Logs;

logItem组件:

import React from 'react';
import MyDate from "./MyDate/MyDate";
import './LogItem.css'

const LogItem = (props) => {

     在函数组件中,属性就相当于是函数的参数,可以通过参数来访问
     可以在函数组件的形参中定义一个props,props指向的是一个对象
     它包含了父组件中传递的所有参数
     console.log(props.date);

    return (
        <div className="item">
            <MyDate date={props.date}/>
            {/* 日志内容的容器 */}
            <div className="content">
                {/*
                  如果将组件中的数据全部写死,将会导致组件无法动态设置,不具有使用价值
                    我们希望组件数据可以由外部设置,在组件间,父组件可以通过props(属性)向子组件传递数据
                */}
                <h2 className="desc">{props.desc}</h2>
                <div className="time">{props.time}分钟</div>
            </div>
        </div>
    );
};


export default LogItem;

MyDate组件:

import React from 'react';
import './MyDate.css';

const MyDate = (props) => {
    // console.log(props.date.getDate());
    // 获取月份
    const month = props.date.toLocaleString('zh-CN', {month:'long'});
    // 获取日期
    const date = props.date.getDate();

    return (
        <div className="date">
            <div className="month">
                {month}
            </div>
            <div className="day">
                {date}
            </div>
        </div>
    );
};

export default MyDate;

12. state

  /*
  * 在React中,当组件渲染完毕后,再修改组件中的变量,不会使组件重新渲染
  *   要使得组件可以收到变量的影响,必须在变量修改后对组件进行重新渲染
  *   这里我们就需要一个特殊变量,当这个变量被修改使,组件会自动重新渲染
  *
  * state相当于一个变量,
  *   只是这个变量在React中进行了注册,
  *   React会监控这个变量的变化,当state发生变化时,会自动触发组件的重新渲染
  *   使得我们的修改可以在页面中呈现出来
  *
  * 在函数组件中,我们需要通过钩子函数,获取state
  *
  * 使用钩子 useState() 来创建state
  *   import {useState} from "react";
  *
  * 它需要一个值作为参数,这个值就是state的初始值
  *   该函数会返回一个数组
  *     数组中第一个元素,是初始值
  *       - 初始值只用来显示数据,直接修改不会触发组件的重新渲染
  *     数组中的第二个元素,是一个函数,通常会命名为setXxx
  *       - 这个函数用来修改state,调用其修改state后会触发组件的重新渲染,
  *           并且使用函数中的值作为新的state值
  *
  * */

import './App.css';
import { useState } from "react";

const App = () => {

  console.log('函数执行了 ---> 组件创建完毕!');


  const [counter, setCounter] = useState(1);
  // let counter = result[0];
  // let setCounter = result[1];
  // const [counter, setCounter] = result;
  /*
  *   当点击+时,数字增大
  *   点击-时,数字减少
  * */

  // 创建一个变量存储数字
  // let counter = 2;

  const addHandler = () => {
    // 点击后数字+1
    // alert('+1');
    // counter++;
    setCounter(counter + 1); // 将counter值修改为2
  };

  const lessHandler = () => {
    // 点击后数字-1
    // alert('-1');
    // counter--;

    setCounter(counter-1);

  };

  return <div className={'app'}>
    <h1>{counter}</h1>
    <button onClick={lessHandler}>-</button>
    <button onClick={addHandler}>+</button>
  </div>;
};

// 导出App
export default App;

   /*

    *   state

    *     - state实际就是一个被React管理的变量

    *         当我们通过setState()修改变量的值时,会触发组件的自动重新渲染

    *     - 只有state值发生变化时,组件才会重新渲染

    *     - 当state的值是一个对象时,修改时是使用新的对象去替换已有对象

    *     - 当通过setState去修改一个state时,并不表示修改当前的state

    *         它修改的是组件下一次渲染时state值

    *     - setState()会触发组件的重新渲染,它是异步的

    *           所以当调用setState()需要用旧state的值时,一定要注意

    *           有可能出现计算错误的情况

    *           为了避免这种情况,可以通过为setState()传递回调函数的形式来修改state值

    * */

import './App.css';
import { useState } from "react";

const App = () => {

    console.log('函数执行了 ---> 组件创建完毕!');

    const [counter, setCounter] = useState(1);
    const [user, setUser] = useState({ name: '孙悟空', age: 18 });

    const addHandler = () => {
        setTimeout(() => {
            // setCounter(counter + 1); // 将counter值修改为2
            setCounter((prevCounter) => {

                /*
                *   setState()中回调函数的返回值将会成为新的state值
                *       回调函数执行时,React会将最新的state值作为参数传递
                * */
                return prevCounter + 1;
            });

            // setCounter(prevState => prevState + 1);
        }, 1000);

        // setCounter(2);
        // setCounter(3);
        // setCounter(4);
        // setCounter(5);
        // setCounter(6);
    };

    const updateUserHandler = () => {
        // setUser({name:'猪八戒'});

        // 如果直接修改旧的state对象,由于对象还是那个对象,所以不会生效
        // user.name = '猪八戒';
        // console.log(user);
        // setUser(user);

        // const newUser = Object.assign({}, user);
        // newUser.name = '猪八戒';
        // setUser(newUser);

        setUser({ ...user, name: '猪八戒' });


    };

    return <div className={'app'}>
        <h1>{counter} -- {user.name} -- {user.age}</h1>
        <button onClick={addHandler}>1</button>
        <button onClick={updateUserHandler}>2</button>
    </div>;
};

// 导出App
export default App;

 13. DOM对象和ref 

ref:

React中所有的操作默认都是在React元素上进行,然后再通过虚拟DOM应用到真实页面上的.这样做的好处我们不在整述。虽然如此,在React中依然为我们提供了可以直接访问原生DOM对象的方式。ref就是干这个事的。
ref是reference的简写,换句话说就是用来获取真实DOM对象的引用。咱们丑话还是要说在前边,虽然可以获取到DOM对象,但是轻易不要这么做,如果必须要获取,也尽量是读取而不要修改,如果必需要修改也要尽量减少修改的次数,总之能不用就不用。

    /*

    *   获取原生的DOM对象

    *       1.可以使用传统的document来对DOM进行操作

    *       2.直接从React处获取DOM对象

    *           步骤:

    *               1.创建一个存储DOM对象的容器

    *                   - 使用 useRef() 钩子函数

    *                       钩子函数的注意事项:

    *                           ① React中的钩子函数只能用于函数组件或自定义钩子

    *                           ② 钩子函数只能直接在函数组件中调用

    *               2.将容器设置为想要获取DOM对象元素的ref属性

    *                   <h1 ref={xxx}>....</h1>

    *                   - React会自动将当前元素的DOM对象,设置为容器current属性

    *

    *   useRef()

    *       - 返回的就是一个普通的JS对象

    *       - {current:undefined}

    *       - 所以我们直接创建一个js对象,也可以代替useRef()

    *       - 区别:

    *           我们创建的对象,组件每次重新渲染都会创建一个新对象

    *           useRef()创建的对象,可以确保每次渲染获取到的都是同一个对象

    *

    *       - 当你需要一个对象不会因为组件的重新渲染而改变时,就可以使用useRef()

    *

    * */

import './App.css';
import { useRef, useState } from "react";

let temp;

const App = () => {

    const h1Ref = useRef(); // 创建一个容器
    const [count, setCount] = useState(1);
    // const h1Ref = {current:null};

    // console.log(temp === h1Ref);
    // temp = h1Ref;

    const clickHandler = () => {

        // 通过id获取h1
        const header = document.getElementById('header');
        // alert(header);
        // header.innerHTML = '哈哈';

        console.log(h1Ref);
        // alert(h1Ref.current === header);
        h1Ref.current.innerText = '嘻嘻!';
    };

    const countAddHandler = () => {
        setCount(prevState => prevState + 1);
    };

    return <div className={'app'}>
        <h1 id="header" ref={h1Ref}>我是标题{count}</h1>
        <button onClick={clickHandler}>1</button>
        <button onClick={countAddHandler}>2</button>
    </div>;
};

// 导出App
export default App;

14. 类组件

父组件App.js:

向子组件User中 传递name='猪八戒' age={28} gender={'男'}

/*
*   Webstrom中的快捷方式:
*       rsc --> 函数组件(不带props)
*       rsi --> 函数组件(带props)
*       rcc --> 类组件
* */

import React, {Component} from 'react';
import './App.css';
import User from "./components/User";

class App extends Component {
    render() {
        return (
            <div className="app">
                <User name='猪八戒' age={28} gender={'男'}/>
            </div>
        );
    }
}

export default App;


子组件User.js:

    /*
    *   类组件的props是存储到类的实例对象中,
    *       可以直接通过实例对象访问
    *       this.props
    *   类组件中state统一存储到了实例对象的state属性中
    *       可以通过 this.state来访问
    *       通过this.setState()对其进行修改
    *           当我们通过this.setState()修改state时,
    *               React只会修改设置了的属性
    *
    *   函数组件中,响应函数直接以函数的形式定义在组件中,
    *       但是在类组件中,响应函数是以类的方法来定义,之前的属性都会保留
    *       但是这你仅限于直接存储于state中的属性
    *
    *   获取DOM对象
    *       1.创建一个属性,用来存储DOM对象
    *           divRef = React.createRef();
    *       2.将这个属性设置为指定元素的ref值
    * */

import React, {Component} from 'react';

class User extends Component {

    // 创建属性存储DOM对象
    divRef = React.createRef();

    // 向state中添加属性
    state = {
        count: 0,
        test: '哈哈',
        obj: {name: '孙悟空', age: 18}
    };

    // 为了省事,在类组件中响应函数都应该以箭头函数的形式定义
    clickHandler = () => {
        // this.setState({count: 10});
        // this.setState(prevState => {
        //     return {
        //         count: prevState + 1
        //     }
        // });
        /*this.setState({
            obj:{...this.state.obj, name:'沙和尚'}
        });*/

        console.log(this.divRef);
    };

    render() {

        // console.log(this.props);
        // console.log(this.divRef);

        return (
            <div ref={this.divRef}>
                <h1>{this.state.count} --- {this.state.test}</h1>
                <h2>{this.state.obj.name} --- {this.state.obj.age}</h2>
                <button onClick={this.clickHandler}>点</button>
                <ul>
                    <li>姓名:{this.props.name}</li>
                    <li>年龄:{this.props.age}</li>
                    <li>性别:{this.props.gender}</li>
                </ul>
            </div>
        );
    }
}

export default User;

15. 添加card组件

logs组件:

再logs组件中引入 Card组件

Card组件:

        props.children 表示组件的标签体,

import React from 'react';
import './Card.css';

const Card = (props) => {


    /*
    *   props.children 表示组件的标签体
    * */
    // console.log(props.children);
    return <div className={`card ${props.className}`}>{props.children}</div>;
};

export default Card;

16. 添加表单、处理表单数据、双向绑定、存储到一个state中

logsForm组件:

import React, {useState} from 'react';
import Card from "../UI/Card/Card";
import './LogsForm.css';

const LogsForm = () => {

    /*
    *   当表单项发生变化时,获取用户输入的内容
    * */
    // 创建三个变量,用来存储表单中的数据
    // let inputDate = '';
    // let inputDesc = '';
    // let inputTime = 0;

    // const [inputDate, setInputDate] = useState('');
    // const [inputDesc, setInputDesc] = useState('');
    // const [inputTime, setInputTime] = useState('');

    // 将表单数据统一到一个state中
    const [formData, setFormData] = useState({
        inputDate:'',
        inputDesc:'',
        inputTime:''
    });

    // 创建一个响应函数,监听日期的变化
    const dateChangeHandler = (e) => {
        // 获取到当前触发事件的对象
        // 事件对象中保存了当前事件触发时的所有信息
        // event.target 执行的是触发事件的对象(DOM对象)
        //console.log(e.target.value);
        // setInputDate(e.target.value);
        setFormData({
            ...formData,
            inputDate: e.target.value
        });
    };

    // 监听内容的变化
    const descChangeHandler = (e) => {
        // 获取到当前触发事件的对象
        // 事件对象中保存了当前事件触发时的所有信息
        // event.target 执行的是触发事件的对象(DOM对象)
        //console.log(e.target.value);
        // setInputDesc(e.target.value);

        setFormData({
            ...formData,
            inputDesc: e.target.value
        });
    };

    //监听时长的变化
    const timeChangeHandler = (e) => {
        // 获取到当前触发事件的对象
        // 事件对象中保存了当前事件触发时的所有信息
        // event.target 执行的是触发事件的对象(DOM对象)
        //console.log(e.target.value);
        // setInputTime(e.target.value);
        setFormData({
            ...formData,
            inputTime: e.target.value
        });
    };

    // 当表单提交时,汇总表单中的数据
    /*
    *   在React中,通常表单不需要自行提交
    *       而是要通过React提交
    * */
    const formSubmitHandler = (e) => {
        // 取消表单的默认行为
        e.preventDefault();
        // 获取表单项中的数据日期、内容、时长
        // 将数据拼装为一个对象
        const newLog = {
            date: new Date(formData.inputDate),
            desc: formData.inputDesc,
            time: +formData.inputTime
        };

        // 清空表单项
        setFormData({
            inputDate: '',
            inputDesc: '',
            inputTime: ''
        });

        console.log(newLog);

        *   提交表单后如何清空表单中的旧数据
        *       现在这种表单,在React我们称为非受控组件
        *
        *   我们可以将表单中的数据存储到state中,
        *       然后将state设置为表单项value值,
        *       这样当表单项发生变化,state会随之变化,
        *       反之,state发生变化,表单项也会跟着改变,这种操作我们就称为双向绑定
        *       这样一来,表单就成为了一个受控组件

    };

    return (
        <Card className="logs-form">
            <form onSubmit={formSubmitHandler}>
                <div className="form-item">
                    <label htmlFor="date">日期</label>
                    <input onChange={dateChangeHandler} value={formData.inputDate} id="date" type="date"/>
                </div>
                <div className="form-item">
                    <label htmlFor="desc">内容</label>
                    <input onChange={descChangeHandler} value={formData.inputDesc} id="desc" type="text"/>
                </div>
                <div className="form-item">
                    <label htmlFor="time">时长</label>
                    <input onChange={timeChangeHandler} value={formData.inputTime} id="time" type="number"/>
                </div>
                <div className="form-btn">
                    <button>添加</button>
                </div>
            </form>
        </Card>
    );
};

export default LogsForm;

App组件:

17. 完成添加

子组件数据传递给父组件:

  1. 父组件内在子组件上传递一个方法
  2. 子组件使用这个方法传递数据给父组件
  3. 父组件可以通过形参拿到传递的数据

图示:

  

App组件:

import Logs from "./Components/Logs/Logs";
import LogsForm from "./Components/LogsForm/LogsForm";
import './App.css';
import {useState} from "react";

const App = () => {

    // 数据
    const [logsData, setLogsData] = useState([
        {
            id: '001',
            date: new Date(2021, 1, 20, 18, 30),
            desc: '学习九阳神功',
            time: 30
        },
        {
            id: '002',
            date: new Date(2022, 2, 10, 12, 30),
            desc: '学习降龙十八掌',
            time: 20
        },
        {
            id: '003',
            date: new Date(2022, 2, 11, 11, 30),
            desc: '学习JavaScript',
            time: 40
        },
        {
            id: '004',
            date: new Date(2022, 2, 15, 10, 30),
            desc: '学习React',
            time: 80
        }
    ]);

    /*
    *   下一步:
    *     将LogsForm中的数据传递给App组件,然后App组件,将新的日志添加到数组中!
    * */

    // 定义一个函数
    const saveLogHandler = (newLog) => {
        // 向新的日志中添加id
        newLog.id = Date.now() + '';

        // console.log('App.js -->',newLog);

        // 将新的数据添加到数组中
        // logsData.push(newLog);
        setLogsData([newLog, ...logsData]);

    };

    return <div className="app">
        {/*引入LogsFrom*/}
        <LogsForm onSaveLog={saveLogHandler}/>
        <Logs logsData={logsData}/>
    </div>;
};

// 导出App
export default App;

logs组件:

/* 日志的容器 */
import LogItem from "./LogItem/LogItem";
import Card from "../UI/Card/Card";
import './Logs.css';

const Logs = (props) => {

    
    *   logsDate 用来存储学习的日志,
    *       这个数据除了当前组件Logs需要使用外,LogsForm也需要使用
    *       当遇到一个数据需要被多个组件使用时,我们可以将数据放入到这些组件共同的祖先元素中
    *       这样就可以使得多个组件都能方便的访问到这个数据
    *
    *   state的提升

    // 模拟一组从服务器中加载的数据


    // 将数据放入JSX中
    const logItemDate = props.logsData.map(item => <LogItem key={item.id} date={item.date} desc={item.desc}
                                                      time={item.time}/>);

    return <Card className="logs">
        {
            logItemDate
            // logsData.map(item => <LogItem {...item}/> )
        }
    </Card>
};

export default Logs;

logsForm组件:

import React, {useState} from 'react';
import Card from "../UI/Card/Card";
import './LogsForm.css';

const LogsForm = (props) => {

    const [inputDate, setInputDate] = useState('');
    const [inputDesc, setInputDesc] = useState('');
    const [inputTime, setInputTime] = useState('');


    // 创建一个响应函数,监听日期的变化
    const dateChangeHandler = (e) => {
        setInputDate(e.target.value);
    };

    // 监听内容的变化
    const descChangeHandler = (e) => {

        setInputDesc(e.target.value);

    };

    //监听时长的变化
    const timeChangeHandler = (e) => {
        setInputTime(e.target.value);
    };

    // 当表单提交时,汇总表单中的数据
    const formSubmitHandler = (e) => {
        // 取消表单的默认行为
        e.preventDefault();
        // 获取表单项中的数据日期、内容、时长
        // 将数据拼装为一个对象
        const newLog = {
            date: new Date(inputDate),
            desc: inputDesc,
            time: +inputTime
        };

        // 当要添加新的日志时,调用父组件传递过来的函数
        props.onSaveLog(newLog);

        // 清空表单项
        setInputDate('');
        setInputDesc('');
        setInputTime('');

    };

    return (
        <Card className="logs-form">
            <form onSubmit={formSubmitHandler}>
                <div className="form-item">
                    <label htmlFor="date">日期</label>
                    <input onChange={dateChangeHandler} value={inputDate} id="date" type="date"/>
                </div>
                <div className="form-item">
                    <label htmlFor="desc">内容</label>
                    <input onChange={descChangeHandler} value={inputDesc} id="desc" type="text"/>
                </div>
                <div className="form-item">
                    <label htmlFor="time">时长</label>
                    <input onChange={timeChangeHandler} value={inputTime} id="time" type="number"/>
                </div>
                <div className="form-btn">
                    <button>添加</button>
                </div>
            </form>
        </Card>
    );
};

export default LogsForm;

18. 完成删除

logsItem组件:

监听删除方法 onDelLog

import React from 'react';
import MyDate from "./MyDate/MyDate";
import './LogItem.css'
import Card from "../../UI/Card/Card";

const LogItem = (props) => {

    /*
    *   props是只读的不能修改
    * */
    // props.desc = '嘻嘻'; // 不能修改props中的属性
    // console.log(props.desc);

    // 删除item的响应函数
    const deleteItemHandler = () => {
        // 临时性
        const isDel = window.confirm('该操作不可恢复,确认吗?');
        if (isDel){
            // 删除当前的item,要删除item,其实就是要从数据的state移除指定的数据
            // console.log(props.onDelLog);
            props.onDelLog();
        }
    };

    return (
        <Card className="item">
            <MyDate date={props.date}/>
            {/* 日志内容的容器 */}
            <div className="content">
                {/*
                  如果将组件中的数据全部写死,将会导致组件无法动态设置,不具有使用价值
                    我们希望组件数据可以由外部设置,在组件间,父组件可以通过props(属性)向子组件传递数据
                */}
                <h2 className="desc">{props.desc}</h2>
                <div className="time">{props.time}分钟</div>
            </div>

        {/*    添加一个删除按钮*/}
             <div>
                 <div onClick={deleteItemHandler} className='delete'>×</div>
             </div>
        </Card>
    );
};


export default LogItem;

App组件:

import Logs from "./components/Logs/Logs";
import LogsForm from "./components/LogsForm/LogsForm";
import './App.css';
import {useState} from "react";

const App = () => {

    // 数据
    const [logsData, setLogsData] = useState([
        {
            id: '001',
            date: new Date(2021, 1, 20, 18, 30),
            desc: '学习九阳神功',
            time: 30
        },
        {
            id: '002',
            date: new Date(2022, 2, 10, 12, 30),
            desc: '学习降龙十八掌',
            time: 20
        },
        {
            id: '003',
            date: new Date(2022, 2, 11, 11, 30),
            desc: '学习JavaScript',
            time: 40
        },
        {
            id: '004',
            date: new Date(2022, 2, 15, 10, 30),
            desc: '学习React',
            time: 80
        }
    ]);

    /*
    *   下一步:
    *     将LogsForm中的数据传递给App组件,然后App组件,将新的日志添加到数组中!
    * */

    // 定义一个函数
    const saveLogHandler = (newLog) => {
        // 向新的日志中添加id
        newLog.id = Date.now() + '';

        // console.log('App.js -->',newLog);

        // 将新的数据添加到数组中
        // logsData.push(newLog);
        setLogsData([newLog, ...logsData]);

    };

    // 定义一个函数,从数据中删除一条日志
    const delLogByIndex = (index) => {
        setLogsData(prevState => {
            const newLog = [...prevState];
            newLog.splice(index, 1);
            return newLog;
        });
    };

    return <div className="app">
        {/*引入LogsFrom*/}
        <LogsForm onSaveLog={saveLogHandler}/>
        <Logs logsData={logsData} onDelLog={delLogByIndex}/>
    </div>;
};

// 导出App
export default App;

logs组件:

把App组件中的删除方法传递给 losgItem

/* 日志的容器 */
import LogItem from "./LogItem/LogItem";
import Card from "../UI/Card/Card";
import './Logs.css';

const Logs = (props) => {

    /*
    *   logsDate 用来存储学习的日志,
    *       这个数据除了当前组件Logs需要使用外,LogsForm也需要使用
    *       当遇到一个数据需要被多个组件使用时,我们可以将数据放入到这些组件共同的祖先元素中
    *       这样就可以使得多个组件都能方便的访问到这个数据
    *
    *   state的提升
    *
    * */
    // 模拟一组从服务器中加载的数据


    // 将数据放入JSX中
    const logItemDate = props.logsData.map((item, index) => <LogItem
                                                                     onDelLog={()=>props.onDelLog(index)}
                                                                     key={item.id}
                                                                     date={item.date}
                                                                     desc={item.desc}
                                                                     time={item.time}/>);

    return <Card className="logs">
        {
            logItemDate
            // logsData.map(item => <LogItem {...item}/> )
        }
    </Card>
};

export default Logs;

19. 空列表提示

logs组件:

/* 日志的容器 */
import LogItem from "./LogItem/LogItem";
import Card from "../UI/Card/Card";
import './Logs.css';

const Logs = (props) => {

    /*
    *   logsDate 用来存储学习的日志,
    *       这个数据除了当前组件Logs需要使用外,LogsForm也需要使用
    *       当遇到一个数据需要被多个组件使用时,我们可以将数据放入到这些组件共同的祖先元素中
    *       这样就可以使得多个组件都能方便的访问到这个数据
    *
    *   state的提升
    *
    * */
    // 模拟一组从服务器中加载的数据


    // 将数据放入JSX中
    let logItemData = props.logsData.map(
        (item, index) => <LogItem
            onDelLog={() => props.onDelLog(index)}
            key={item.id}
            date={item.date}
            desc={item.desc}
            time={item.time}/>
    );

    if (logItemData.length === 0) {
        logItemData = <p className="no-logs">没要找到日志!</p>;
    }

    return <Card className="logs">
        {
            logItemData
            // logItemData.length !== 0 ? logItemData : <p className="no-logs">没要找到日志!</p>
            // logsData.map(item => <LogItem {...item}/> )
        }
    </Card>
};

export default Logs;

20. 添加 ConfirmModal

ConfirmModal组件:

import './ComfirmModal.css';
import Card from "../Card/Card";
import BackDrop from "../BackDrop/BackDrop";

const ConfirmModal = props => {

    return <BackDrop>
        <Card className="confirmModal">
            <div className="confirmText">
                <p>{props.confirmText}</p>
            </div>
            <div className="confirmButton">
                <button onClick={props.onOk} className="okButton">确认</button>
                <button onClick={props.onCancel}>取消</button>
            </div>
        </Card>;
    </BackDrop>

};

export default ConfirmModal;

logsItem组件:

import React, {useState} from 'react';
import MyDate from "./MyDate/MyDate";
import './LogItem.css'
import Card from "../../UI/Card/Card";
import ConfirmModal from "../../UI/ConfirmModal/ConfirmModal";

const LogItem = (props) => {

    // 添加一个state,记录是否显示确认窗口
    const [showConfirm, setShowConfirm] = useState(false);

    /*
    *   props是只读的不能修改
    * */
    // props.desc = '嘻嘻'; // 不能修改props中的属性
    // console.log(props.desc);

    // 删除item的响应函数
    const deleteItemHandler = () => {

        // 显示确认窗口
        setShowConfirm(true);
    };

    //取消函数
    const cancelHandler = () => {
        setShowConfirm(false);
    };

    // 确认函数
    const okHandler = () => {
        props.onDelLog();
    };

    return (
        <Card className="item">

            {showConfirm && <ConfirmModal
                confirmText="该操作不可恢复!请确认"
                onCancel={cancelHandler}
                onOk={okHandler}
            />}

            <MyDate date={props.date}/>
            {/* 日志内容的容器 */}
            <div className="content">
                {/*
                  如果将组件中的数据全部写死,将会导致组件无法动态设置,不具有使用价值
                    我们希望组件数据可以由外部设置,在组件间,父组件可以通过props(属性)向子组件传递数据
                */}
                <h2 className="desc">{props.desc}</h2>
                <div className="time">{props.time}分钟</div>
            </div>

        {/*    添加一个删除按钮*/}
             <div>
                 <div onClick={deleteItemHandler} className='delete'>×</div>
             </div>
        </Card>
    );
};


export default LogItem;

backDrop组件:

import React from 'react';
import './BackDrop.css';

const BackDrop = (props) => {
    return (
        <div className="backDrop">
            {props.children}
        </div>
    );
};

export default BackDrop;

21.  使用portal修改项目

createPortal方法

   /*

    *   portal

    *       - 组件默认会作为父组件的后代渲染到页面中

    *           但是有些情况下,这种方式会带来一些问题

    *       - 通过portal可以将组件渲染到页面中的指定位置

    *       - 使用方法:

    *           1.在index.html添加一个新的元素

    *           2.修改组件的渲染方式

    *               - 通过ReactDOM.createPortal()作为返回值创建元素

    *               - 参数:

    *                   1. jsx(修改前return后的代码)

    *                   2. 目标位置(DOM元素)

    * */

遮罩层组件:

import React from 'react';
import './Backdrop.css';
import ReactDOM from "react-dom";

// 获取backdrop的根元素
const backdropRoot = document.getElementById('backdrop-root');

const Backdrop = (props) => {
    return ReactDOM.createPortal(<div className="backdrop">
        {props.children}
    </div>, backdropRoot);
};

export default Backdrop;

logItem组件:

import React, {useState} from 'react';
import MyDate from "./MyDate/MyDate";
import './LogItem.css'
import Card from "../../UI/Card/Card";
import ConfirmModal from "../../UI/ConfirmModal/ConfirmModal";

const LogItem = (props) => {

    // 添加一个state,记录是否显示确认窗口
    const [showConfirm, setShowConfirm] = useState(false);

    // 删除item的响应函数
    const deleteItemHandler = () => {
        // 显示确认窗口
        setShowConfirm(true);
    };

    //取消函数
    const cancelHandler = () => {
        setShowConfirm(false);
    };

    // 确认函数
    const okHandler = () => {
        props.onDelLog();
    };

    return (
        <Card className="item">

            {showConfirm && <ConfirmModal
                confirmText="该操作不可恢复!请确认"
                onCancel={cancelHandler}
                onOk={okHandler}
            />}

            <MyDate date={props.date}/>
            {/* 日志内容的容器 */}
            <div className="content">
                {/*
                  如果将组件中的数据全部写死,将会导致组件无法动态设置,不具有使用价值
                    我们希望组件数据可以由外部设置,在组件间,父组件可以通过props(属性)向子组件传递数据
                */}
                <h2 className="desc">{props.desc}</h2>
                <div className="time">{props.time}分钟</div>
            </div>

        {/*    添加一个删除按钮*/}
             <div>
                 <div onClick={deleteItemHandler} className='delete'>×</div>
             </div>
        </Card>
    );
};


export default LogItem;

22.  过滤日志

logs组件:

/* 日志的容器 */
import LogItem from "./LogItem/LogItem";
import Card from "../UI/Card/Card";
import './Logs.css';
import LogFilter from "./LogFilter/LogFilter";
import {useState} from "react";

const Logs = (props) => {

    // 创建一个存储年份的state
    const [year, setYear] = useState(2022);

    // 过滤数据,只显示某一年的数据
    let filterData = props.logsData.filter(item => item.date.getFullYear() === year);

    // 创建一个修改年份的函数
    const changeYearHandler = (year) => {
        setYear(year);
    };


    // 将数据放入JSX中
    let logItemData = filterData.map(
        (item, index) => <LogItem
            onDelLog={() => props.onDelLog(item.id)}
            key={item.id}
            date={item.date}
            desc={item.desc}
            time={item.time}/>
    );

    if (logItemData.length === 0) {
        logItemData = <p className="no-logs">没要找到日志!</p>;
    }

    return <Card className="logs">
        {/*引入年份的选择组件*/}
        <LogFilter
            year={year}
            onYearChange={changeYearHandler}
        />
        {logItemData}
    </Card>
};

export default Logs;

过滤组件:

import React from 'react';

const LogFilter = props => {

    // 创建监听change事件的响应函数
    const changeHandler = e => {
        props.onYearChange(+e.target.value);
    };

    return (
        <div>
            年份:
            <select onChange={changeHandler} value={props.year}>
            <option value="2022">2022</option>
            <option value="2021">2021</option>
            <option value="2020">2020</option>
        </select>
        </div>
    );
};

export default LogFilter;

App组件:

更改为id删除

import Logs from "./components/Logs/Logs";
import LogsForm from "./components/LogsForm/LogsForm";
import './App.css';
import {useState} from "react";

const App = () => {

    // 数据
    const [logsData, setLogsData] = useState([
        {
            id: '001',
            date: new Date(2021, 1, 20, 18, 30),
            desc: '学习九阳神功',
            time: 30
        },
        {
            id: '002',
            date: new Date(2022, 2, 10, 12, 30),
            desc: '学习降龙十八掌',
            time: 20
        },
        {
            id: '003',
            date: new Date(2022, 2, 11, 11, 30),
            desc: '学习JavaScript',
            time: 40
        },
        {
            id: '004',
            date: new Date(2022, 2, 15, 10, 30),
            desc: '学习React',
            time: 80
        }
    ]);

    /*
    *   下一步:
    *     将LogsForm中的数据传递给App组件,然后App组件,将新的日志添加到数组中!
    * */

    // 定义一个函数
    const saveLogHandler = (newLog) => {
        // 向新的日志中添加id
        newLog.id = Date.now() + '';

        // console.log('App.js -->',newLog);

        // 将新的数据添加到数组中
        // logsData.push(newLog);
        setLogsData([newLog, ...logsData]);

    };

    // 定义一个函数,从数据中删除一条日志
    const delLogByIndex = (index) => {
        setLogsData(prevState => {
            const newLog = [...prevState];
            newLog.splice(index, 1);
            return newLog;
        });
    };

    // 定义一个函数,从数据中删除一条日志
    const delLogById = (id) => {
        setLogsData(prevState => {
            return prevState.filter(item => item.id !== id);
        });
    };

    return <div className="app">
        {/*引入LogsFrom*/}
        <LogsForm onSaveLog={saveLogHandler}/>
        <Logs logsData={logsData} onDelLog={delLogById}/>
    </div>;
};

// 导出App
export default App;

23. create-react-app  

为了使我们创建项目更加方便React为我们提供了一个工具 create-react-app 光看名字你应该就已经知道它的作用了
使用 create-react-app 可以快速的创建一个React项目的目录结构,并且它会自动帮助我们安装React中所有的依赖,换句话说,之前我们手动做的工作现在只需要一个命令就可以搞定了!

npx create-react-app 项目名

 24. 内联样式和样式表

  1. 内联样式编写一个style对象,在DOM元素的style属性进行赋值
  2. 结合 state动态设置样式
  3. 通过 import 引入外部样式表

25.  CSS_Module

    /*

    *   CSS模块

    *       使用步骤:

    *           1.创建一个xxx.module.css

    *           2.在组件中引入css

    *               import classes from './App.module.css';

    *           3.通过classes来设置类

    *               className={classes.p1}

    *       CSS模块可以动态的设置唯一的class值

    *           App_p1__9v2n6

    * */

import React, {useState} from 'react';
import classes from './App.module.css';
import A from "./A";

const App = () => {

    const [showBorder, setShowBorder] = useState(false);

    const clickHandler = () => {
        setShowBorder(true);
    };

    return (
        <div>
            <A/>
            <p className={`${classes.p1} ${showBorder ? classes.Border : ''}`}>我是一个段落</p>
            <button onClick={clickHandler}>点我一下</button>
        </div>
    );
};

export default App;

26. Fragment

    /*

    *   React.Fragment

    *       - 是一个专门用来作为父容器的组件

    *           它只会将它里边的子元素直接返回,不会创建任何多余的元素

    *       - 当我们希望有一个父容器

    *           但同时又不希望父容器在网页中产生多余的结构时

    *           就可以使用Fragment

    * */

方式一:

 方式二:

方式三:

使用空标签

    return (
        <>
           <div>第一个组件</div>
           <div>第二个组件</div>
           <div>第三个组件</div>
        </>
    );

27. 汉堡到家 - demo练习

Font Awesome 中文网 – | 字体图标

在需要使用的组件内引入这两个包

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

import { faPlus, faMinus } from "@fortawesome/free-solid-svg-icons";

使用示例:

<button className={classes.Sub}><FontAwesomeIcon icon={faMinus} /></button>

        - 引入组件
               import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
        - 引入图标
                import {faPlus} from "@fortawesome/free-solid-svg-icons";
        - 使用组件
                <FontAwesomeIcon icon={faPlus}/>

Context

Context官网地址: Context – React

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

创建Context:

import React from 'react';

*   Context相当于一个公共的存储空间,
*       我们可以将多个组件中都需要访问的数据统一存储到一个Context中,
*       这样无需通过props逐层传递,即可使组件访问到这些数据
*
*   通过React.createContext()创建context
*


const TestContext = React.createContext({
    name:'孙悟空',
    age:18
});

export default TestContext;

演示Context:

A组件:

import React from 'react';
import TestContext from "../store/testContext";


*   使用方式一:
*       1.引入context
*       2.使用 Xxx.Consumer 组件来创建元素
*           Consumer 的标签体需要一个回调函数
*           它会将context设置为回调函数的参数,通过参数就可以访问到context中存储的数据
*


const A = () => {
    return (
        <TestContext.Consumer>
            {(ctx)=>{
               return <div>
                   {ctx.name} - {ctx.age}
               </div>
            }}
        </TestContext.Consumer>
    );
};

export default A;

B组件:

import React, {useContext} from 'react';
import TestContext from "../store/testContext";


*   使用Context方式二:
*       1.导入Context
*       2.使用钩子函数useContext()获取到context
*           useContext() 需要一个Context作为参数
*               它会将Context中数据获取并作为返回值返回
*
*   Xxx.Provider
*       - 表示数据的生产者,可以使用它来指定Context中的数据
*       - 通过value来指定Context中存储的数据,
*           这样一来,在该组件的所有的子组件中都可以通过Context来访问它所指定数据
*
*   当我们通过Context访问数据时,他会读取离他最近的Provider中的数据,
*       如果没有Provider,则读取Context中的默认数据
*


const B = () => {

    // 使用钩子函数获取Context
    const ctx = useContext(TestContext);

    return (
        <div>
            {ctx.name} -- {ctx.age}
        </div>
    );
};

export default B;

仓库源码:https://gitee.com/wei-shuai-lei/order-cart-react.git


28.  Hooks

1. Effcet 

React组件有部分逻辑都可以直接编写到组件的函数体中的,像是对数组调用filter、map等方法,像是判断某个组件是否显示等。但是有一部分逻辑如果直接写在函数体中,会影响到组件的渲染,这部分会产生“副作用”的代码,是一定不能直接写在函数体中。
例如,如果直接将修改state的逻辑编写到了组件之中,就会导致组件不断的循环渲染,直至调用次数过多内存溢出。


React.StrictMode
编写React组件时,我们要极力的避免组件中出现那些会产生"副作用”的代码。同时,如果你的React使用了严格模式,也就是在React中使用了 React.strictMode 标签,那么React会非常"智能”的去检查你的组件中是否写有副作用的代码,当然这个智能是加了引号的,我们来看看React官网的文档是如何说明的:

useEffect

    
     *  useEffect是一个钩子函数,需要一个函数作为参数
     *      这个作为参数的函数,将会在组件渲染完毕后执行
     *  在开发中,可以将那些会产生副作用的代码编写到useEffect的回调函数中
     *  这样可以避免这些代码影响带组件的渲染
     *   
     *  在useEffect() 可以传递一个第二个参数
     *      第二个参数是一个数组,在数组中可以指定Effect的依赖项
     *      指定后,只有当依赖项发生变化的时候,Effect才会被触发
     *      通常会将Effect中使用的所有的局部变量都设置为它的依赖项
     *          这样一来可以确保这些值发生变化时,可以触发 Effect
     *      像setState是由钩子函数 useState() 生成的
     *          useState() 会确保组件的每次渲染都会获得到相同 setState() 对象
     *          所以setState() 方法可以不设置到依赖项中
     *      如果依赖项设置了一个空数组,则意味着Effect只会在组件初始化时触发执行一次
  
    useEffect(() => {
        // 降低数据过滤的次数,提高用户体验
        // 用户输入完了你再过滤,用户输入过程中,不要过滤
        // 当用户停止输入动作1秒后, 再查询
        // 再开启一个定时器的同时,应该关掉上一个
        let timer = setTimeout(() => {
            console.log('Effect执行了');
            props.onFilter(keyword);

        }, 1000)

       

        // 在Effect的回调函数中,可以指定一个函数作为返回值
        // 这个函数可以称其为清理函数,他会在下次Effect执行前调用
        // 可以在这个函数中,做一些工作来清除上次Effect执行所带来的影响

        return () => {
            clearTimeout(timer)
        }

    }, [keyword])

2. setState的执行流程

当我们直接在函数体中调用setState时,就会触发上述错误问题:

不是说过,当新的state 值和旧值相同时,它是不会触发组件的重新渲染的setState()的执行流程(函数组件)

setCount() --> dispatchSetDate()
            --> 会先判断,组件当前处于什么阶段
            如果是渲染阶段 --> 不会检查state 值是否相同
            如果不是渲染阶段 --> 会检查state的值是否相同
                - 如果值不相同,则对组件进行重新渲染
                - 如果值相同,则不对值进行重新渲染
                    如果值相同,React在一些情况下会继续执行当前组件的渲染
                    但是这个渲染不会触发其子组件的渲染,这次渲染不会产生实际的效果

3. useReducer 

   /**

     * 参数:

     *  reducer :整合函数

     *      对于我们当前state的所有操作都应该在该函数中定义

     *      该函数的返回值,会成为state的新值

     *      reducer 在执行时,会受到两个参数

     *          1. 当前最新的state

     *          2. action 它需要一个对象,在对象中会存储dispatch所发送的指令

     *  initialArg : state初始值,作用和useState中的值是一样的

     *  init :

     * 返回值:

     *      数组:

     *          第一个参数,state 用来获取state的值

     *          第二个参数,state 修改的派发器

     *              通过派发器可以发送操作state的命令

     *              具体的修改行为将会由另外一个函数执行

     *  

     *  */

// 为了避免reducer会重复创建,通常reducer会定义到组件的外部

代码示例:

import React, { useEffect, useReducer, useState } from 'react';
import './App.css';


// 为了避免reducer会重复创建,通常reducer会定义到组件的外部
const countHander = (state, action) => {


    switch (action.type) {
        case 'ADD':
            return state + 1
        case 'SUB':
            return state - 1
        default:
            return state
    }
}


const App = () => {

    // const [count, setCount] = useState(1)

    // const addHander = () => {
    //     setCount(prevState => prevState + 1)
    // }

    // const subHander = () => {
    //     setCount(prevState => prevState - 1)
    // }

    // useReducer(reducer, initialArg, init)
   
    const [count, countDispatch] = useReducer(countHander, 1)
    const addHander = () => {
        countDispatch({ type: 'ADD' })
    }

    const subHander = () => {
        countDispatch({ type: 'SUB' })
    }
    return (
        <div style={{ fontSize: 30 }}>
            <button onClick={subHander}>减少</button>
            {count}
            <button onClick={addHander}>增加</button>

        </div>
    );
};

export default App;

4.React.memo

React组件会在两种情况下发生重新渲染。第一种,当组件自身的state发生变化时。第二种当组件的父组件重新渲染时。第一种情况下的重新渲染无可厚非,state都变了,组件自然应该重新进行渲染。

但是第二种情况似乎并不是总那么的必要。

React.memo() 是一个高阶组件
    它接收另一个组件作为参数,并且返回一个包装后的新组件
    包装过的新组件就会具有缓存功能
        包装过后,只有组件的props发生变化
        才会触发组件的重新渲染,否则总是返回缓存中的结果

示例: (导出一个B组件)
export default React.memo(B)

5. useCallback()

useCallback

  • useCallback 是一个钩子函数,用来创建React 中的回调函数
  • useCallback 创建的回调函数不会总在组件重新渲染时重新创建

参数:

  1. 回调函数
  2. 依赖数组
    1. 当依赖数组中的变量发生变化时,回调函数才会重新执行
    2. 如果不指定依赖数组,回调函数每次都会重新创建
    3. 一定要将所使用到的局部变量放到依赖数组中(除了 setState)

示例:

const B = () => {
    console.log('B组件执行了');
    const clickHandler = useCallback(() => {
        setCount(prevState => prevState + num)
        setNum(prevState => num + 1)
    }, [num])
}

6.Strapi 

欢迎来到 Strapi 开发人员文档!

React83_Strapi的使用_哔哩哔哩_bilibili

Strapi是什么? 官网是这么描述的“Strapi是完全使用JavaScript开发的,开源无头内容管理系统”,对于第一次接触它的同学会感觉莫名其妙,“无头内容管理系统”,什么玩意? 简单来说,strapi就是一个APl的管理系统,通过Strapi我们可以直接以网页的形式去定义自己的API、包括设置模型、权限等功能。有了strapi我们无需编写代码便可开发出功能强大的API。

创建项目:

使用npx会卡在一个压缩包,建议使用yarn

npx create-strapi-app 项目名 --quickstart

yarn create strapi-app 项目名 --quickstart

7.fetch

React的主要作用是取代了原生DOM,让我们操作网页的方式变得更加简单。但是React中并没有为我们提供向服务器中发送请求的方法 (因为这本来就不是它所关注的事情)。所以在React中发送请求的方式和传统项目其实是一致的,无非就是使用浏览器自带的Ajax、Fetch或者是类似于Axios的第三方框架。这也就意味着在React中发送请求的方式其实是非常灵活的,你完全可以根据自己的习惯选择一种你喜欢的方式。

8. 数据加载提示 + 错误处理

可以添加一个 布尔值 来判断是否在加载


创建一个state 来记录错误消息

 9. 自定义钩子

个人理解 所谓自定义钩子就是封装一个方法

/**

 * React中的钩子函数只能在函数组件或自定钩子中调用

 *      当我们需要将React 中钩子函数提取到一个公共区域时,就可以使用自定义钩子

 *

 * 自定义钩子其实就是一个普通函数,只是它的名字需要使用use 开头

 */


自定义hook useFetch.js 
自定义的useFetch.js 钩子

import React, { useState } from "react";
const useEftch = () => {

    const [stuData, setStuData] = useState([])

    const fetchData = async () => {
        let res = await fetch('http://localhost:1337/api/data')
        if (res.ok) {
            const data = await res.json()
            setStuData(data)
        }

    }
    return {
        stuData,
        setStuData,
        fetchData
    }
}

export default useEftch

App.js

        导入自定义的钩子

import React, { useCallback, useEffect, useState } from 'react';
import './App.css';
import Table from "./Table";

import useFetch from './hooks/useFetch'

const App = () => {
    const { stuData, setStuData, fetchData } = useFetch()
    
    useEffect(() => {
        fetchData()
    }, [])

    const loadDataHandler = () => {
        fetchData()
    }

    return (
        <div>
            <button onClick={loadDataHandler}>加载数据</button>
            <Table slectMethod={fetchData} list={stuData}></Table>
        </div>
    );
};

export default App;

10.  Redux学习

A Predictable State Container for JS Apps是Redux官方对于Redux的描述,这句话可以这样翻译“一个专为JS应用设计的可预期的状态容器”,简单来说Redux是一个可预测的状态容器什么玩意?


状态(State)

state直译过来就是状态,使用React这么久了,对于state我们已经是非常的熟悉了。state不过就是一个变量,一个用来记录(组件)状态的变量。组件可以根据不同的状态值切换为不同的显示,比如,用户登录和没登录看到页面应该是不同的,那么用户的登录与否就应该是一个状态。再比如,数据加载与否,显示的界面也应该不同,那么数据本身就是一个状态。换句话说,状态控制了页面的如何显示。

但是需要注意的是,状态并不是React中或其他类似框架中独有的。所有的编程语言,都有状态,所有的编程语言都会根据不同的状态去执行不同的逻辑,这是一定的。所以状态是什么,状态就是一个变量,用以记录程序执行的情况。

容器(Container)

容器当然是用来装东西的,状态容器即用来存储状态的容器。状态多了,自然需要一个东西来存储,但是容器的功能却不是仅仅能存储状态,它实则是一个状态的管理器,除了存储状态外,它还可以用来对state进行查询、修改等所有操作。(编程语言中容器几乎都是这个意思,其作用无非就是对某个东西进行增删改查)

可预测(Predictable)

可预测指我们在对state进行各种操作时,其结果是一定的。即以相同的顺序对state执行相同的操作会得到相同的结果。简单来说,Redux中对状态所有的操作都封装到了容器内部,外部只能通过调用容器提供的方法来操作state,而不能直接修改state。这就意味着外部对state的操作都被容器所限制,对state的操作都在容器的掌控之中,也就是可预测。

总的来说,Redux是一个稳定、安全的状态管理器

为什么是Redux?

问:不对啊?React中不是已经有state了吗?为什么还要整出一个Redux来作为状态管理器呢?

答:state应付简单值还可以,如果值比较复杂的话并不是很方便。

问:复杂值可以用useReducer嘛!

答:的确可以啊!但无论是state还是useReducer,state在传递起来还是不方便,自上至下一层一层的传递并不方便啊!

问:那不是还有context吗?

答:的确使用context可以解决state的传递的问题,但依然是简单的数据尚可,如果数据结构过于复杂会使得context变得异常的庞大,不方便维护。

Redux可以理解为是reducer和context的结合体,使用Redux即可管理复杂的state,又可以在不同的组件间方便的共享传递state。当然,Redux主要使用场景依然是大型应用,大型应用中状态比较复杂,如果只是使用reducer和context,开发起来并不是那么的便利,此时一个有一个功能强大的状态管理器就变得尤为的重要。

使用

使用Redux之前,你需要先明确一点Redux是JS应用的状态容器,它并不是只能在React使用,而是可以应用到任意的JS应用中(包括前端JS,和服务器中Node.js)。总之,凡是JS中需要管理的状态的Redux都可以胜任。

在网页中直接使用

我们先来在网页中使用以下Redux,在网页中使用Redux就像使用jQuery似的,直接在网页中引入Redux的库文件即可:

<script src="https://unpkg.com/redux@4.2.0/dist/redux.js"></script>

网页中我们实现一个简单的计数器功能,页面长成这样:

<button id="btn01">减少</button>
<span id="counter">1</span>
<button id="btn02">增加</button>

我们要实现的功能很简单,点击减少数字变小,点击增加数字变大。如果用传统的DOM编写,可以创建一个变量用以记录数量,点击不同的按钮对变量做不同的修改并设置到span之中,代码像是这样:

不使用Redux:

const btn01 = document.getElementById('btn01');
const btn02 = document.getElementById('btn02');
const counterSpan = document.getElementById('counter');

let count = 1;

btn01.addEventListener('click', ()=>{
   count--;
   counterSpan.innerText = count;
});

btn02.addEventListener('click', ()=>{
   count++;
   counterSpan.innerText = count;
});

上述代码中count就是一个状态,只是这个状态没有专门的管理器,它的所有操作都在事件的响应函数中进行处理,这种状态就是不可预测的状态,因为在任何的函数中都可以对这个状态进行修改,没有任何安全限制。不过就这个功能而言,这可能已经是最简单的代码了。一会我们使用了Redux,代码会变得复杂一些,但是还是那句话,这里我们只是找一个简单的场景做一个演示,Redux的真实使用场景依然是大型应用中的复杂state。

Redux是一个状态容器,所以使用Redux必须先创建容器对象,它的所有操作都是通过容器对象来进行的,创建容器的方式有多种,我们先说一种好理解的:

Redux.createStore(reducer, [preloadedState], [enhancer])

createStore用来创建一个Redux中的容器对象,它需要三个参数:reducer、preloadedState、enhancer。

reducer是一个函数,是state操作的整合函数,每次修改state时都会触发该函数,它的返回值会成为新的state。

preloadedState就是state的初始值,可以在这里指定也可以在reducer中指定。

enhancer增强函数用来对state的功能进行扩展,暂时先不理它。

三个参数中,只有reducer是必须的,来看一个Reducer的示例:

const countReducer = (state = {count:0}, action) => {
    switch (action.type){
        case 'ADD':
            return {count:state.count+1};
        case 'SUB':
            return {count:state.count-1};
        default:
            return state
    }
};

reducer用来整合关于state的所有操作,容器修改state时会自动调用该函数,函数调用时会接收到两个参数:state和action,state表示当前的state,可以通过该state来计算新的state。state = {count:0}这是在指定state的默认值,如果不指定,第一次调用时state的值会是undefined。也可以将该值指定为createStore()的第二个参数。action是一个普通对象,用来存储操作信息。

将reducer传递进createStore后,我们会得到一个store对象:

const store = Redux.createStore(countReducer);

store对象创建后,对state的所有操作都需要通过它来进行:

读取state:

store.getState()

修改state:

store.dispatch({type:'ADD'})

dipatch用来触发state的操作,可以将其理解为是想reducer发送任务的工具。它需要一个对象作为参数,这个对象将会成为reducer的第二个参数action,需要将操作信息设置到对象中传递给reducer。action中最重要的属性是type,type用来识别对state的不同的操作,上例中’ADD’表示增加操作,’SUB’表示减少的操作。

除了这些方法外,store还拥有一个subscribe方法,这个方法用来订阅state变化的信息。该方法需要一个回调函数作为参数,当store中存储的state发生变化时,回调函数会自动调用,我们可以在回调函数中定义state发生变化时所要触发的操作:

store.subscribe(()=>{
    // store中state发生变化时触发
});

如此一来,刚刚的代码被修改成了这个样子:

const btn01 = document.getElementById('btn01');
const btn02 = document.getElementById('btn02');
const counterSpan = document.getElementById('counter');


const countReducer = (state = {count:0}, action) => {
    switch (action.type){
        case 'ADD':
            return {count:state.count+1};
        case 'SUB':
            return {count:state.count-1};
        default:
            return state
    }
};

const store = Redux.createStore(countReducer);

store.subscribe(()=>{
    counterSpan.innerText = store.getState().count;
});

btn01.addEventListener('click', ()=>{
    store.dispatch({type:'SUB'});
});

btn02.addEventListener('click', ()=>{
    store.dispatch({type:'ADD'});
});

修改后的代码相较于第一个版本要复杂一些,同时也解决了之前代码中存在的一些问题:

  1. 前一个版本的代码state就是一个变量,可以任意被修改。state不可预测,容易被修改为错误的值。新代码中使用了Redux,Redux中的对state的所有操作都封装到了reducer函数中,可以限制state的修改使state可预测,有效的避免了错误的state值。
  2. 前一个版本的代码,每次点击按钮修改state,就要手动的修改counterSpan的innerText,非常麻烦,这样一来我们如果再添加新的功能,依然不能忘记对其进行修改。新代码中,counterSpan的修改是在store.subscribe()的回调函数中进行的,state每次发生变化其值就会随之变化,不需要再手动修改。换句话说,state和DOM元素通过Redux绑定到了一起。

通过上例也不难看出,Redux中最最核心的东西就是这个store,只要拿到了这个store对象就相当于拿到了Redux中存储的数据。在加上Redux的核心思想中有一条叫做“单一数据源”,也就是所有的state都会存储到一课对象树中,并且这个对象树会存储到一个store中。所以到了React中,组件只需获取到store即可获取到Redux中存储的所有state。

React中使用Redux(旧的方式)

当我们需要在React中使用Redux时,我们除了需要引入Redux核心库外,还需要引入react-redux库,以使React和redux适配,可以通过npm或yarn安装:

npm install -S redux react-redux

yarn add redux react-redux

接下来我们尝试在Redux,添加一些复杂的state,比如一个学生的信息:

{name:'孙悟空', age:18, gender:'男', address:'花果山'}

代码:

创建reducer:

const reducer = (state = {
    name: '孙悟空',
    age: 18,
    gender: '男',
    address: '花果山'
}, action) => {
    switch (action.type) {
        case 'SET_NAME':
            return {
                ...state,
                name: action.payload
            };
        case 'SET_AGE':
            return {
                ...state,
                age: action.payload
            };
        case 'SET_ADDRESS':
            return {
                ...state,
                address: action.payload
            };
        case 'SET_GENDER':
            return {
                ...state,
                gender: action.payload
            };
        default :
            return state
    }

};

reducer的编写和之前的案例并没有本质的区别,只是这次的数据和操作方法变得复杂了一些。以SET_NAME为例,当需要修改name属性时,dispatch需要传递一个有两个属性的action,action的type应该是字符串”SET_NAME”,payload应该是要修改的新名字,比如要将名字修改为猪八戒,则dispatch需要传递这样一个对象{type:'SET_NAME',payload:'猪八戒'}

创建store:

const store = createStore(reducer);

创建store和前例并无差异,传递reducer进行构建即可。

设置provider:

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <Provider store={store}>
        <App/>
    </Provider>
);

创建store后,需要引入react-redux中提供的Provider组件,将其设置到所有组件的最外层,并且将刚刚创建的store设置为组件的store属性,只有这样才能使得Redux中的数据能被所有的组件访问到。

访问数据:

const stu = useSelector(state => state);

react-redux还为我们提供一个钩子函数useSelector,用于获取Redux中存储的数据,它需要一个回调函数作为参数,回调函数的第一个参数就是当前的state,回调函数的返回值,会作为useSelector的返回值返回,所以state => state表示直接将整个state作为返回值返回。现在就可以通过stu来读取state中的数据了:

<p>
    {stu.name} -- {stu.age} -- {stu.gender} -- {stu.address}
</p>

操作数据:

const dispatch = useDispatch();

useDispatch同样是react-redux提供的钩子函数,用来获取redux的派发器,对state的所有操作都需要通过派发器来进行。

通过派发器修改state:

dispatch({type:'SET_NAME', payload:'猪八戒'})
dispatch({type:'SET_AGE', payload:28})
dispatch({type:'SET_GENDER', payload:'女'})
dispatch({type:'SET_ADDRESS', payload:'高老庄'})

完整代码:

import ReactDOM from 'react-dom/client';
import {Provider, useDispatch, useSelector} from "react-redux";
import {createStore} from "redux";

const reducer = (state = {
    name: '孙悟空',
    age: 18,
    gender: '男',
    address: '花果山'
}, action) => {
    switch (action.type) {
        case 'SET_NAME':
            return {
                ...state,
                name: action.payload
            };
        case 'SET_AGE':
            return {
                ...state,
                age: action.payload
            };
        case 'SET_ADDRESS':
            return {
                ...state,
                address: action.payload
            };
        case 'SET_GENDER':
            return {
                ...state,
                gender: action.payload
            };
        default :
            return state
    }

};

const store = createStore(reducer);

const App = () =>{
    const stu = useSelector(state => state);
    const dispatch = useDispatch();
    return  <div>
        <p>
            {stu.name} -- {stu.age} -- {stu.gender} -- {stu.address}
        </p>
        <div>
            <button onClick={()=>{dispatch({type:'SET_NAME', payload:'猪八戒'})}}>改name</button>
            <button onClick={()=>{dispatch({type:'SET_AGE', payload:28})}}>改age</button>
            <button onClick={()=>{dispatch({type:'SET_GENDER', payload:'女'})}}>改gender</button>
            <button onClick={()=>{dispatch({type:'SET_ADDRESS', payload:'高老庄'})}}>改address</button>
        </div>
  </div>
};



const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <div>
        <Provider store={store}>
            <App/>
        </Provider>
    </div>

);

复杂的State

上例中的数据结构已经变得复杂,但是距离真实项目还有一定的差距。因为Redux的核心思想是所有的state都应该存储到同一个仓库中,所以只有一个学生数据确实显得有点单薄,现在将数据变得复杂一些,出来学生数据外,还增加了一个学校的信息,于是state的结构变成了这样:

{
    stu:{
        name: '孙悟空',
        age: 18,
        gender: '男',
        address: '花果山' 
    },
    school:{
        name:'花果山一小',
        address:'花果山大街1号'
    }
}

数据结构变得复杂了,我们需要对代码进行修改,首先看reducer:

const reducer = (state = {
    stu: {
        name: '孙悟空',
        age: 18,
        gender: '男',
        address: '花果山'
    },
    school: {
        name: '花果山一小',
        address: '花果山大街1号'
    }

}, action) => {
    switch (action.type) {
        case 'SET_NAME':
            return {
                ...state,
                stu: {
                    ...state.stu,
                    name: action.payload
                }
            };
        case 'SET_AGE':
            return {
                ...state,
                stu: {
                    ...state.stu,
                    age: action.payload
                }
            };
        case 'SET_ADDRESS':
            return {
                ...state,
                stu: {
                    ...state.stu,
                    address: action.payload
                }
            };
        case 'SET_GENDER':
            return {
                ...state,
                stu: {
                    ...state.stu,
                    gender: action.payload
                }
            };
        case 'SET_SCHOOL_NAME':
            return {
                ...state,
                school: {
                    ...state.school,
                    name:action.payload
                }
            };
        case 'SET_SCHOOL_ADDRESS':
            return {
                ...state,
                school: {
                    ...state.school,
                    address: action.payload
                }
            }
        default :
            return state;
    }

};

数据层次变多了,我们在操作数据时也变得复杂了,比如修改name的逻辑变成了这样:

case 'SET_NAME':
    return {
         ...state,
        stu: {
            ...state.stu,
            name: action.payload
    }
};

同时数据加载的逻辑也要修改,之前我们是将整个state返回,现在我们需要根据不同情况获取state,比如获取学生信息要这么写:

const stu = useSelector(state => state.stu);

获取学校信息:

const school = useSelector(state => state.school);

完整代码:

import ReactDOM from 'react-dom/client';
import {Provider, useDispatch, useSelector} from "react-redux";
import {createStore} from "redux";

const reducer = (state = {
    stu: {
        name: '孙悟空',
        age: 18,
        gender: '男',
        address: '花果山'
    },
    school: {
        name: '花果山一小',
        address: '花果山大街1号'
    }

}, action) => {
    switch (action.type) {
        case 'SET_NAME':
            return {
                ...state,
                stu: {
                    ...state.stu,
                    name: action.payload
                }
            };
        case 'SET_AGE':
            return {
                ...state,
                stu: {
                    ...state.stu,
                    age: action.payload
                }
            };
        case 'SET_ADDRESS':
            return {
                ...state,
                stu: {
                    ...state.stu,
                    address: action.payload
                }
            };
        case 'SET_GENDER':
            return {
                ...state,
                stu: {
                    ...state.stu,
                    gender: action.payload
                }
            };
        case 'SET_SCHOOL_NAME':
            return {
                ...state,
                school: {
                    ...state.school,
                    name:action.payload
                }
            };
        case 'SET_SCHOOL_ADDRESS':
            return {
                ...state,
                school: {
                    ...state.school,
                    address: action.payload
                }
            }
        default :
            return state;
    }

};

const store = createStore(reducer);

const App = () => {
    const stu = useSelector(state => state.stu);
    const school = useSelector(state => state.school);
    const dispatch = useDispatch();
    return <div>
        <p>
            {stu.name} -- {stu.age} -- {stu.gender} -- {stu.address}
        </p>
        <div>
            <button onClick={() => {
                dispatch({type: 'SET_NAME', payload: '猪八戒'});
            }}>改name
            </button>
            <button onClick={() => {
                dispatch({type: 'SET_AGE', payload: 28});
            }}>改age
            </button>
            <button onClick={() => {
                dispatch({type: 'SET_GENDER', payload: '女'});
            }}>改gender
            </button>
            <button onClick={() => {
                dispatch({type: 'SET_ADDRESS', payload: '高老庄'});
            }}>改address
            </button>
        </div>

        <hr/>

        <p>
            {school.name} -- {school.address}
        </p>
        <div>
            <button onClick={()=>{dispatch({type:'SET_SCHOOL_NAME', payload:'高老庄小学'})}}>改学校name</button>
            <button onClick={()=>{dispatch({type:'SET_SCHOOL_ADDRESS', payload:'高老庄中心大街15号'})}}>改学校address</button>
        </div>
    </div>;
};


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <div>
        <Provider store={store}>
            <App/>
        </Provider>
    </div>
);

麻烦确实是麻烦了一些,但是还好功能实现了。

多个Reducer

上边的案例的写法存在一个非常严重的问题!将所有的代码都写到一个reducer中,会使得这个reducer变得无比庞大,现在只有学生和学校两个信息。如果数据在多一些,操作方法也会随之增多,reducer会越来越庞大变得难以维护。

Redux中是允许我们创建多个reducer的,所以上例中的reducer我们可以根据它的数据和功能进行拆分,拆分为两个reducer,像是这样:

const stuReducer = (state = {
    name: '孙悟空',
    age: 18,
    gender: '男',
    address: '花果山'
}, action) => {
    switch (action.type) {
        case 'SET_NAME':
            return {
                ...state,
                name: action.payload
            };
        case 'SET_AGE':
            return {
                ...state,
                age: action.payload
            };
        case 'SET_ADDRESS':
            return {
                ...state,
                address: action.payload
            };
        case 'SET_GENDER':
            return {
                ...state,
                gender: action.payload
            };
        default :
            return state;
    }

};

const schoolReducer = (state = {
    name: '花果山一小',
    address: '花果山大街1号'
}, action) => {
    switch (action.type) {
        case 'SET_SCHOOL_NAME':
            return {
                ...state,
                name: action.payload
            };
        case 'SET_SCHOOL_ADDRESS':
            return {
                ...state,
                address: action.payload
            };
        default :
            return state;
    }

};

修改后reducer被拆分为了stuReducer和schoolReducer,拆分后在编写每个reducer时,只需要考虑当前的state数据,不再需要对无关的数据进行复制等操作,简化了reducer的编写。于此同时将不同的功能编写到了不同的reducer中,降低了代码间的耦合,方便对代码进行维护。

拆分后,还需要使用Redux为我们提供的函数combineReducer将多个reducer进行合并,合并后才能传递进createStore来创建store。

const reducer = combineReducers({
    stu:stuReducer,
    school:schoolReducer
});

const store = createStore(reducer);

combineReducer需要一个对象作为参数,对象的属性名可以根据需要指定,比如我们有两种数据stu和school,属性名就命名为stu和school,stu指向stuReducer,school指向schoolReducer。读取数据时,直接通过state.stu读取学生数据,通过state.school读取学校数据。

完整代码:

import ReactDOM from 'react-dom/client';
import {Provider, useDispatch, useSelector} from "react-redux";
import {combineReducers, createStore} from "redux";

const stuReducer = (state = {
    name: '孙悟空',
    age: 18,
    gender: '男',
    address: '花果山'
}, action) => {
    switch (action.type) {
        case 'SET_NAME':
            return {
                ...state,
                name: action.payload
            };
        case 'SET_AGE':
            return {
                ...state,
                age: action.payload
            };
        case 'SET_ADDRESS':
            return {
                ...state,
                address: action.payload
            };
        case 'SET_GENDER':
            return {
                ...state,
                gender: action.payload
            };
        default :
            return state;
    }

};

const schoolReducer = (state = {

    name: '花果山一小',
    address: '花果山大街1号'

}, action) => {
    switch (action.type) {
        case 'SET_SCHOOL_NAME':
            return {
                ...state,
                name: action.payload
            };
        case 'SET_SCHOOL_ADDRESS':
            return {
                ...state,
                address: action.payload
            };
        default :
            return state;
    }

};

const reducer = combineReducers({
    stu:stuReducer,
    school:schoolReducer
});

const store = createStore(reducer);

const App = () => {
    const stu = useSelector(state => state.stu);
    const school = useSelector(state => state.school);
    const dispatch = useDispatch();
    return <div>
        <p>
            {stu.name} -- {stu.age} -- {stu.gender} -- {stu.address}
        </p>
        <div>
            <button onClick={() => {
                dispatch({type: 'SET_NAME', payload: '猪八戒'});
            }}>改name
            </button>
            <button onClick={() => {
                dispatch({type: 'SET_AGE', payload: 28});
            }}>改age
            </button>
            <button onClick={() => {
                dispatch({type: 'SET_GENDER', payload: '女'});
            }}>改gender
            </button>
            <button onClick={() => {
                dispatch({type: 'SET_ADDRESS', payload: '高老庄'});
            }}>改address
            </button>
        </div>

        <hr/>

        <p>
            {school.name} -- {school.address}
        </p>
        <div>
            <button onClick={() => {
                dispatch({type: 'SET_SCHOOL_NAME', payload: '高老庄小学'});
            }}>改学校name
            </button>
            <button onClick={() => {
                dispatch({type: 'SET_SCHOOL_ADDRESS', payload: '高老庄中心大街15号'});
            }}>改学校address
            </button>
        </div>
    </div>;
};


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <div>
        <Provider store={store}>
            <App/>
        </Provider>
    </div>

11. Redux Toolkit(RTK) 

上边的案例我们一直在使用Redux核心库来使用Redux,除了Redux核心库外Redux还为我们提供了一种使用Redux的方式——Redux Toolkit。它的名字起的非常直白,Redux工具包,简称RTK。RTK可以帮助我们处理使用Redux过程中的重复性工作,简化Redux中的各种操作。

在React中使用RTK

安装,无论是RTK还是Redux,在React中使用时react-redux都是必不可少,所以使用RTK依然需要安装两个包:react-redux和@reduxjs/toolkit。

npm

npm install react-redux @reduxjs/toolkit -S

yarn

yarn add react-redux @reduxjs/toolkit

修改上边的例子

使用RTK时,reducer依然可以使用之前的创建方式不变,但是不在需要合并reducer。RTK为我们提供了一个configureStore方法,它直接接收一个对象作为参数,可以将reducer的相关配置直接通过该对象传递,而不再需要单独合并reducer。

上例中代码:

const reducer = combineReducers({
    stu:stuReducer,
    school:schoolReducer
});

const store = createStore(reducer);

修改为:

const store = configureStore({
    reducer:{
        stu:stuReducer,
        school:schoolReducer
    }
});

configureStore需要一个对象作为参数,在这个对象中可以通过不同的属性来对store进行设置,比如:reducer属性用来设置store中关联到的reducer,preloadedState用来指定state的初始值等,还有一些值我们会放到后边讲解。

reducer属性可以直接传递一个reducer,也可以传递一个对象作为值。如果只传递一个reducer,则意味着store中只有一个reducer。若传递一个对象作为参数,对象的每个属性都可以执行一个reducer,在方法内部它会自动对这些reducer进行合并。

RTK的API

CreateAction(一般不直接用)

action是reducer中的第二个参数,当我们通过dispatch向reducer发送指令时需要手动创建action对象并传递。action中常见的属性有两个一个是type用来指定操作的类型,一个是payload用来指定要传递的数据。

RTK为我们提供了一个方法createAction,用来帮助我们创建action。

createAction(type, prepareAction?)

它的第一个参数为type,用来指定action中的type属性。第二个参数可选先忽略它。它的返回值是一个函数。我们可以这么调用:

conconst setName= createAction('ADD');

setName(); // {type: 'ADD', payload: undefined}
setName('猪八戒'); // {type: 'ADD', payload: '猪八戒'}

返回值的函数我们可以调用,调用该函数后会得到一个对象,这个对象有两个属性type和payload,type属性值就是我们调用createAction传递的第一个参数,上例中type就是’ADD’。而payload属性就是我们调用该函数时传递的参数。

const add = createAction('SET_NAME');
add(); // {type: 'SET_NAME', payload: undefined}
add('猪八戒'); // {type: 'SET_NAME', payload: '猪八戒'}

简单说,createAction会返回一个函数,这个函数可以用来创建固定type属性值的对象,并且这个函数的第一个参数会成为新建对象的payload属性值。

可以通过creatAction修改之前的项目:

先创建四个action函数

const setName = createAction('SET_NAME');
const setAge = createAction('SET_AGE');
const setAddress = createAction('SET_ADDRESS');
const setGender = createAction('SET_GENDER');

修改dispatch

dispatch(setName('猪八戒'));
dispatch(setAge(28));
dispatch(setGender('女'));
dispatch(setAddress('高老庄'));

createAction返回函数所创建的对象结构是固定的{type:'xxx', payload:...},我们也可以通过向createAction传递第二个参数来指定payload的格式:

const add = createAction('ADD', (name, age, gender, address) => {
    return {
        payload:{
            name,
            age,
            gender,
            address
        }
    }
});

add('沙和尚', 38, '男', '流沙河'); // {"type":"ADD","payload":{"name":"沙和尚","age":38,"gender":"男","address":"流沙河"}}

CreateReucer(一般不用)

该方法用来是创建reducer的工具方法。

createReducer(initialState, builderCallback)

参数:

initialState —— state的初始值

builderCallback —— 带有builer的回调函数,可以同builer来设置reducer的逻辑

回调函数中会传递一个builder作为参数,通过通过builder可以将action和函数进行绑定,使用时可以通过传递指定的action来触发函数的调用。

builder有一个常用的方法addCase,addCase需要两个参数,第一个参数为action,第二个参数为回调函数。action直接传递通过createAction所创建的函数即可,第二个参数是一个回调函数,回调函数类似于reducer,第一个参数为state,第二个参数为action。但又和reducer不同,该回调函数中返回的state是一个代理对象,可以直接对该对象修改,RTK会自动完成其余操作。

示例:

// 创建action
const setName = createAction('setName');

// 创建reducer
const stuReducer = createReducer({
        name: '孙悟空',
        age: 18,
        gender: '男',
        address: '花果山'
    }, builder => {
        // 通过builder将action和回调函数进行绑定
        builder.addCase(setName, (state, action) => {
            // 这里的state是代理对象,可以直接对其进行修改
            state.name = action.payload;
        });
    }
);

// 配置reducer
const store = configureStore({
    reducer: {
        stu: stuReducer,
        school: schoolReducer
    }
});

// 发送指令修改name属性
dispatch(setName('猪八戒'));

无论是createAction和createReducer都不是RTK中的常用方式(要是这么写代码,可能得疯)。介绍他们只是希望你能了解一下RTK的运行方式。对于我们来创建reducer时最最常用的方式是:createSlice。

CreateSlice

createSlice是一个全自动的创建reducer切片的方法,在它的内部调用就是createAction和createReducer,之所以先介绍那两个也是这个原因。createSlice需要一个对象作为参数,对象中通过不同的属性来指定reducer的配置信息。

createSlice(configuration object)

配置对象中的属性:

initialState —— state的初始值

name —— reducer的名字,会作为action中type属性的前缀,不要重复

reducers —— reducer的具体方法,需要一个对象作为参数,可以以方法的形式添加reducer,RTK会自动生成action对象。

示例:

const stuSlice= createSlice({
    name:'stu',
    initialState:{
        name: '孙悟空',
        age: 18,
        gender: '男',
        address: '花果山'
    },
    reducers:{
        setName(state, action){
            state.name = action.payload
        }
    }
});

createSlice返回的并不是一个reducer对象而是一个slice对象(切片对象)。这个对象中我们需要使用的属性现在有两个一个叫做actions,一个叫做reducer。

Actions

切片对象会根据我们对象中的reducers方法来自动创建action对象,这些action对象会存储到切片对象actions属性中:

stuSlice.actions; // {setName: ƒ}

上例中,我们仅仅指定一个reducer,所以actions中只有一个方法setName,可以通过解构赋值获取到切片中的action。

const {setName} = stuSlice.actions;

开发中可以将这些取出的action对象作为组件向外部导出,导出其他组件就可以直接导入这些action,然后即可通过action来触发reducer。

Reducer

切片的reducer属性是切片根据我们传递的方法自动创建生成的reducer,需要将其作为reducer传递进configureStore的配置对象中以使其生效:

const store = configureStore({
    reducer: {
        stu: stuSlice.reducer,
        school: schoolReducer
    }
});

总的来说,使用createSlice创建切片后,切片会自动根据配置对象生成action和reducer,action需要导出给调用处,调用处可以使用action作为dispatch的参数触发state的修改。reducer需要传递给configureStore以使其在仓库中生效。

完整代码:

import ReactDOM from 'react-dom/client';
import {Provider, useDispatch, useSelector} from "react-redux";
import {configureStore, createSlice} from "@reduxjs/toolkit";

const stuSlice = createSlice({
    name: 'stu',
    initialState: {
        name: '孙悟空',
        age: 18,
        gender: '男',
        address: '花果山'
    },
    reducers: {
        setName(state, action) {
            state.name = action.payload;
        },
        setAge(state, action) {
            state.age = action.payload;
        },
        setGender(state, action) {
            state.gender = action.payload;
        },
        setAddress(state, action) {
            state.gender = action.payload;
        }
    }
});

const {setName, setAge, setGender, setAddress} = stuSlice.actions;

const schoolSlice = createSlice({
    name: 'school',
    initialState: {
        name: '花果山一小',
        address: '花果山大街1号'
    },
    reducers: {
        setSchoolName(state, action) {
            state.name = action.payload;
        },
        setSchoolAddress(state, action) {
            state.address = action.payload;
        }
    }
});

const {setSchoolName, setSchoolAddress} = schoolSlice.actions;

const store = configureStore({
    reducer: {
        stu: stuSlice.reducer,
        school: schoolSlice.reducer
    }
});

const App = () => {
    const stu = useSelector(state => state.stu);
    const school = useSelector(state => state.school);
    const dispatch = useDispatch();
    return <div>
        <p>
            {stu.name} -- {stu.age} -- {stu.gender} -- {stu.address}
        </p>
        <div>
            <button onClick={() => {
                dispatch(setName('猪八戒'));
            }}>改name
            </button>
            <button onClick={() => {
                dispatch(setAge(28));
            }}>改age
            </button>
            <button onClick={() => {
                dispatch(setGender('女'));
            }}>改gender
            </button>
            <button onClick={() => {
                dispatch(setAddress('高老庄'));
            }}>改address
            </button>
        </div>

        <hr/>

        <p>
            {school.name} -- {school.address}
        </p>
        <div>
            <button onClick={() => {
                dispatch(setSchoolName('高老庄中心小学'));
            }}>改学校name
            </button>
            <button onClick={() => {
                dispatch(setSchoolAddress('高老庄中心大街15号'));
            }}>改学校address
            </button>
        </div>
    </div>;
};


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <div>
        <Provider store={store}>
            <App/>
        </Provider>
    </div>
);

目前我们的代码都是编写在同一个文件中的,真实开发中还需要对代码进行拆分,不写了你们自己试一试吧,具体内容看视频。

自己尝试:

使用RTK来构建store

在store/index.js中:

自己写的使用RTK来 构建store

import { configureStore, createSlice } from "@reduxjs/toolkit";

// 

/** 
 *  createSlice 创建 reducer 切片   ,要求传一个配置对象
 *      
 *  它需要一个配置对象作为参数,通过对象的不同属性来指定它的配置
 *  
 * 
  */
const stuSlice = createSlice({
    name: "stu", // 用来自动生成action中的type属性
    initialState: { // 当前切片state的初始值
        name: "孙悟空",
        age: 18,
        gender: "男",
        address: "花果山"
    },
    reducers: {  // 指定state的各种操作
        setName(state, action) {
            /** 
             * 可以通过不同的方法来指定对state的不同操作
             * 两个参数: state  这个state 是一个代理对象,可以直接修改
             *      state.name = "猪八戒" 
             *   
             * */
            state.name = "猪八戒"
        },
        setAge(state, action) {
            state.age = 20
        }
    }
})

// 切片对象会自动的帮助我们生成 action , stuSlice内部会自动帮助我们生成actions , console.log(stuSlice.actions);
// actions中存储的是slice自动生成的action创建器(函数),调用函数后会自动创建action对象
// action对象的结构 {type:name/函数名, payload} -> {type:stu/setName, payload}

export const { setName, setAge } = stuSlice.actions
/* const nameSet = setName('哈哈')
const ageSet = setAge(30)
console.log(nameSet); // {type: 'stu/setName', payload: '哈哈'}
console.log(ageSet); // {type: 'stu/setAge', payload: 30} */

// 创建store  用来创建store对象, 需要一个配置对象作为参数
const store = configureStore({
    /**
     *  1. 单个直接给值  // reducer: stuSlice.reducer
     *  2. 多个要写成对象 
     *      reducer: {
                student: stuSlice.reducer
            }
     * 
     *  */

    reducer: {
        student: stuSlice.reducer
    }
})

export default store

在入口文件 index.jszhong :

使用 react-redux 中提供的 Provider, 使用 Provider包裹App 并书写 store={store}

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from "react-redux";
import store from './store';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>


);


再需要使用的store的组件中:这里是App组件

  • 使用useSekector 来加载state中的数据
  • 通过useDispatch() 来获取派发器对象
  • 需要引入 action 里提供的方法 ,获取action构建器
import { useDispatch, useSelector } from "react-redux";
import { setName, setAge } from "./store";


function App() {
  // useSelector() 用来加载state中的数据
  const student = useSelector((state) => {
    return state.student
  })

  // 通过useDispatch() 来获取派发器对象
  const dispatch = useDispatch()

  // 获取action构建器


  const editName = () => {
    dispatch(setName("哈哈"))
  }

  const editAge = () => {
    console.log(dispatch(setAge(33)));
  }

  return (
    <div className="App">
      <p>{student.name}</p>
      <p>{student.age}</p>
      <p>{student.gender}</p>
      <p>{student.address}</p>
      <button onClick={editName}>修改name</button>
      <button onClick={editAge}>修改age</button>

    </div>
  );
}

export default App;

拆分RTK

创建学生和学校的切片

import { createSlice } from "@reduxjs/toolkit";

const stuSlice = createSlice({
    name: "stu",
    initialState: {
        name: "孙悟空",
        age: 18,
        gender: "男",
        address: "花果山"
    },
    reducers: {
        setName(state, action) {

            state.name = action.payload
        },
        setAge(state, action) {
            state.age = action.payload
        }
    }
})


export const { setName: stuSetName, setAge: stuSetAge } = stuSlice.actions
export const { reducer: stuReducer } = stuSlice


import { createSlice } from "@reduxjs/toolkit";

// 创建学校的slice 
const schoolSlice = createSlice({
    name: 'school',
    initialState: {
        name: "花果山一小",
        address: "花果山28号"
    },
    reducers: {
        setName(state, action) {
            state.name = action.payload
        },
        setAddress(state, action) {
            state.address = action.payload
        }
    }
})

export const { setName: schoolSetName, setAddress: schoolSetAddress } = schoolSlice.actions
export const { reducer: schoolReducer } = schoolSlice

在index中引入

import { configureStore } from "@reduxjs/toolkit";
import { stuReducer } from "./stuSlice";
import { schoolReducer } from "./schoolSlice";

// 创建store  用来创建store对象, 需要一个配置对象作为参数
const store = configureStore({

    reducer: {
        student: stuReducer,
        school: schoolReducer
    }

})

export default store

在需要使用的组件中引入: 这里以App。js为例

import { useDispatch, useSelector } from "react-redux";

// 导入action 构造器
import { stuSetName, stuSetAge } from "./store/stuSlice";
import { schoolSetName, schoolSetAddress } from "./store/schoolSlice"


function App() {
  // useSelector() 用来加载state中的数据
  const student = useSelector((state) => {
    return state.student
  })
  const school = useSelector((state) => {
    return state.school
  })

  // 通过useDispatch() 来获取派发器对象
  const dispatch = useDispatch()

  // 获取action构建器
  // 修改学生
  const editName = () => {
    dispatch(stuSetName("哈哈"))
  }

  const editAge = () => {
    dispatch(stuSetAge(33))
  }
  // 修改学校
  const editSchoolName = () => {
    dispatch(schoolSetName("水帘洞"))
  }

  const editSchoolAddress = () => {
    dispatch(schoolSetAddress("水帘洞33号"))
  }

  return (
    <div className="App">
      <p>{student.name}</p>
      <p>{student.age}</p>
      <p>{student.gender}</p>
      <p>{student.address}</p>
      <button onClick={editName}>修改学生name</button>
      <button onClick={editAge}>修改修改age</button>

      <hr></hr>

      <p>{school.name}</p>
      <p>{school.address}</p>
      <button onClick={editSchoolName}>修改 学校 name</button>
      <button onClick={editSchoolAddress}>修改 学校 地址</button>
    </div>
  );
}

export default App;

12.RTKQ 

RTK不仅帮助我们解决了state的问题,同时,它还为我们提供了RTK Query用来帮助我们处理数据加载的问题。RTK Query是一个强大的数据获取和缓存工具。在它的帮助下,Web应用中的加载变得十分简单,它使我们不再需要自己编写获取数据和缓存数据的逻辑。

Web应用中加载数据时需要处理的问题:

  1. 根据不同的加载状态显示不同UI组件
  2. 减少对相同数据重复发送请求
  3. 使用乐观更新,提升用户体验
  4. 在用户与UI交互时,管理缓存的生命周期

这些问题,RTKQ都可以帮助我们处理。首先,可以直接通过RTKQ向服务器发送请求加载数据,并且RTKQ会自动对数据进行缓存,避免重复发送不必要的请求。其次,RTKQ在发送请求时会根据请求不同的状态返回不同的值,我们可以通过这些值来监视请求发送的过程并随时中止。

使用

RTKQ已经集成在了RTK中,如果我们已经在项目中引入了RTK则无需再引入其余的模块。如果你不想使用RTKQ给我们提供的发送请求的方式(简单封装过的fetch),你还需要引入一下你要使用的发送请求的工具。

创建Api切片

RTKQ中将一组相关功能统一封装到一个Api对象中,比如:都是学生相关操作统一封装到StudentApi中,关于班级的相关操作封装到ClassApi中。接下来,我们尝试创建一个简单的Api,至于数据还是我们之前所熟悉的学生数据:

studentApi.js

import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/dist/query/react";

export const studentApi = createApi({
    reducerPath:'studentApi',
    baseQuery:fetchBaseQuery({
        baseUrl:'http://localhost:1337/api/'
    }),
    endpoints(build) {
        return {
            getStudents: build.query({
                query() {
                    return 'students'
                }
            }),
        }
    }
});

export const {useGetStudentsQuery} = studentApi;

上例是一个比较简单的Api对象的例子,我们来分析一下,首先我们需要调用createApi()来创建Api对象。这个方法在RTK中存在两个版本,一个位于@reduxjs/toolkit/dist/query下,一个位于@reduxjs/toolkit/dist/query/react下。react目录下的版本会自动生成一个钩子,方便我们使用Api。如果不要钩子,可以引入query下的版本,当然我不建议你这么做。

createApi()需要一个配置对象作为参数,配置对象中的属性繁多,我们暂时介绍案例中用到的属性:

reducerPath

用来设置reducer的唯一标识,主要用来在创建store时指定action的type属性,如果不指定默认为api。

baseQuery

用来设置发送请求的工具,就是你是用什么发请求,RTKQ为我们提供了fetchBaseQuery作为查询工具,它对fetch进行了简单的封装,很方便,如果你不喜欢可以改用其他工具,这里暂时不做讨论。

fetchBaseQuery

简单封装过的fetch调用后会返回一个封装后的工具函数。需要一个配置对象作为参数,baseUrl表示Api请求的基本路径,指定后请求将会以该路径为基本路径。配置对象中其他属性暂不讨论。

endpoints

Api对象封装了一类功能,比如学生的增删改查,我们会统一封装到一个对象中。一类功能中的每一个具体功能我们可以称它是一个端点。endpoints用来对请求中的端点进行配置。

endpoints是一个回调函数,可以用普通方法的形式指定,也可以用箭头函数。回调函数中会收到一个build对象,使用build对象对点进行映射。回调函数的返回值是一个对象,Api对象中的所有端点都要在该对象中进行配置。

对象中属性名就是要实现的功能名,比如获取所有学生可以命名为getStudents,根据id获取学生可以命名为getStudentById。属性值要通过build对象创建,分两种情况:

查询:build.query({})

增删改:build.mutation({})

例如:

getStudents: build.query({
    query() {
        return 'students'
    }
}),

先说query,query也需要一个配置对象作为参数(又他喵的是配置对象)。配置对象里同样有n多个属性,现在直说一个,query方法。注意不要搞混两个query,一个是build的query方法,一个是query方法配置对象中的属性,这个方法需要返回一个子路径,这个子路径将会和baseUrl拼接为一个完整的请求路径。比如:getStudets的最终请求地址是:

http://localhost:1337/api/+students=http://localhost:1337/api/students

可算是介绍完了,但是注意了这个只是最基本的配置。RTKQ功能非常强大,但是配置也比较麻烦。不过,熟了就好了。

上例中,我们创建一个Api对象studentApi,并且在对象中定义了一个getStudents方法用来查询所有的学生信息。如果我们使用react下的createApi,则其创建的Api对象中会自动生成钩子函数,钩子函数名字为useXxxQuery或useXxxMutation,中间的Xxx就是方法名,查询方法的后缀为Query,修改方法的后缀为Mutation。所以上例中,Api对象中会自动生成一个名为useGetStudentsQuery的钩子,我们可以获取并将钩子向外部暴露。

export const {useGetStudentsQuery} = studentApi;

创建Store对象

Api对象的使用有两种方式,一种是直接使用,一种是作为store中的一个reducer使用。store是我们比较熟悉的,所以先从store入手。

import {configureStore} from "@reduxjs/toolkit";
import {studentApi} from "./studentApi";

export const store = configureStore({
    reducer:{
        [studentApi.reducerPath]:studentApi.reducer
    },
    middleware:getDefaultMiddleware =>
        getDefaultMiddleware().concat(studentApi.middleware),
});

创建store并没有什么特别,只是注意需要添加一个中间件,这个中间件已自动生成了我们直接引入即可,中间件用来处理Api的缓存。

store创建完毕同样要设置Provider标签,这里不再展示。接下来,我们来看看如果通过studentApi发送请求。由于我们已经将studentApi中的钩子函数向外部导出了,所以我们只需通过钩子函数即可自动加载到所有的学生信息。比如,现在在App.js中加载信息可以这样编写代码:

import React from 'react';
import {useGetStudentsQuery} from './store/studentApi';

const App = () => {
    const {data, isFetching, isSuccess} = useGetStudentsQuery();

    return (
        <div>
            {isFetching && <p>数据正在加载...</p>}
            {isSuccess && data.data.map(item => <p key={item.id}>
                {item.attributes.name} --
                {item.attributes.age} --
                {item.attributes.gender} --
                {item.attributes.address}
            </p>)}
        </div>
    );
};

export default App;

直接调用useGetStudentsQuery()它会自动向服务器发送请求加载数据,并返回一个对象。这个对象中包括了很多属性:

  1. data – 最新返回的数据
  2. currentData – 当前的数据
  3. error – 错误信息
  4. isUninitialized – 如果为true则表示查询还没开始
  5. isLoading – 为true时,表示请求正在第一次加载
  6. isFetching 为true时,表示请求正在加载
  7. isSuccess 为true时,表示请求发送成功
  8. isError 为true时,表示请求有错误
  9. refetch 函数,用来重新加载数据

使用中可以根据需要,选择要获取到的属性值。写了这么多,也只写了一个Hello World。但是,良好的开端是成功的一半,这个理解了,后边的东西也就简单了!

自己尝试:

表格增删改查

student.js :

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react"
import qs from 'qs'

 * 创建API对象 
 * 
 * createApi() 用来创建RTKQ中的API对象
 * RTKQ的所有功能都需要通过对象来机芯
 * createApi() 需要一个对象作为参数
 * 

const studentApi = createApi({
    reducerPath: "studentApi", // API的标识,不能和其他的API 或者 reducer 重复
    baseQuery: fetchBaseQuery({
        baseUrl: "http://localhost:1337/api/",
    }), // 指定查询的基础信息,发送请求使用的工具
    tagTypes: ['student'], // 用来指定API的标签类型
    endpoints(build) {
        /**
         * build 是请求的构建器,通过build来设置请求的相关信息
         *  
         * */
        return {
            getStudents: build.query({
                query(id) {
                    // 查询接口地址是 http://localhost:1337/api/data
                    // 请求的子路径
                    return 'data'
                },
                // 用来转换响应式数据
                transformResponse(baseQueryReturnValue) {
                    // console.log(baseQueryReturnValue.data);
                    return baseQueryReturnValue
                    // return baseQueryReturnValue.data 可以返回其中的data数据
                },
                // 设置数据缓存的时间
                keepUnusedDataFor: 60, // 设置数据缓存的时间,单位秒 默认60s
                providesTags: (
                    (result, error, id) => {
                        return [{ type: 'student', id: 'LIST' }]
                    }
                )
            }), // 查询使用 query()
            // getStudentID:build.query(),
            // updateStudent:build.mutaion() 删除 添加 修改 都使用 mutaion()
            getStudentId: build.query({
                query(sId) {
                    console.log(sId);
                    return `data?${qs.stringify({ id: sId })}`
                }
            }),
            delStudent: build.mutation({
                query(sId) {
                    return {
                        url: `del?${qs.stringify({ id: sId })}`,
                        method: 'delete'
                    }
                },

            }),
            addStudent: build.mutation({
                query(obj) {
                    return {
                        url: 'add',
                        method: 'post',
                        body: obj
                        // 默认会转JSON
                    }
                },
                invalidatesTags: [{ type: 'student', id: 'LIST' }]
            }),
            updateStudent: build.mutation({
                query(obj) {
                    return {
                        url: 'edit',
                        method: 'post',
                        body: obj
                    }
                },
                invalidatesTags: ((obj) => {
                    return [{ type: 'student', id: obj.id }]
                })
            }),

        }
    },// endpoints 用来指定API中的各种功能,是一个方法

})

// API对象创建后, 对象中会根据各种方法自动生成对应的钩子函数
// 通过钩子函数可以向服务器发送请求
// 钩子函数的命名规则 getStudents --> useGetStudentsQuery

export const {
    useGetStudentsQuery,
    useGetStudentIdQuery,
    useDelStudentMutation,
    useAddStudentMutation,
    useUpdateStudentMutation
} = studentApi

export default studentApi

index:.js

import { configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/dist/query";
import studentApi from "./studentApi"

const store = configureStore({
    reducer: {
        [studentApi.reducerPath]: studentApi.reducer
    },
    middleware: getDefaultMiddleware => {
        return getDefaultMiddleware().concat(studentApi.middleware)
    }
})

setupListeners(store.dispatch)  设置以后,将会支持 refetchOnFocus  refetchOnReconnect
export default store

App.js

import React, { useState, useEffect } from 'react'
import { useGetStudentsQuery } from "./store/studentApi"
import Table from "./components/Table"


function App() {
  const [arr, setArr] = useState([])

   调用API查询数据
   这个钩子函数它会返回一个对象作为返回值,请求过程中相关数据都在该对象中存储

  const { isSuccess, data } = useGetStudentsQuery()
  useEffect(() => {

    if (isSuccess) setArr(data)

  }, [data])

  useGetStudentsQuery(null, {})
 
  * useQuery可以接收一个对象作为第二个参数,通过该对象可以对请求进行配置
  * selectFromResult: result => {
  *   if (result.data){
  *     result.data = result.data.filter(item => item.attributes.age < 18)
  *   }
  *   return result;
  * } // 用来指定useQuery返回的结果
  * 
  * pollingInterval: 0 // 设置轮询的间隔,单位是毫秒,如果为0 表示不轮询
  * skip:false // 设置是否跳过当前请求, 默认为false
  * refetchOnMountOrArgChange:false // 设置是否每次都重新加载数据 ,false正常使用缓存 ,true 每次都重载数据,数字 数据缓存的时间(秒)
  * refetchOnFocus:false  // 是否在重新获取焦点时重载数据 需要在入口进行设置
  * refetchOnReconnect:false // 是否在重新连接后重载数据 
 

  return (
    <div className="App">
      {isSuccess && <Table setArr={setArr} stuData={arr}></Table>}

    </div>
  );
}

export default App;

13. React-Router

React Router最新版本为6,版本6和版本5之间的变化跨度比较大,讲解两个版本。


版本5

安装:

npm

npm install react-router-dom@5 -S

yarn

yarn add react-router-dom@5

react-router使用步骤

  1. 引入react-router-dom 包

  2. 在index.js中引入BrowserRouter组件

  3. 将BrowserRouter设置为根组件

 react-router 可以将url地址和组件进行映射,当用户访问某个地址时,与其对应的组件会自动挂载

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { BrowserRouter as Router } from 'react-router-dom';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Router>
    <App />
  </Router>
);

地址和组件建立映射:

import { Route } from "react-router-dom";
import Home from './components/Home'
import About from './components/About'

function App() {
  return (
    <div >

       * 将路由和组件进行映射
       *    是有Route 来映射地址和组件
       *      属性: 
       *        1. path 映射的url地址; 
       *        2. component 映射的组件
       *        3. exact 路径是完整匹配,默认值为false ( 不允许访问子路径 )
       * 
       *    当Route的路径被访问时,其对应组件就会自动挂载
       *      注意: 默认情况下Route并不是严格匹配
       *        只要url地址的头部和path一致,组件就会挂载,不会检查子路径 
  
      <Route exact path="/" component={Home}></Route >
      <Route exact path="/about" component={About}></Route >

    </div>
  );
}

export default App;

Link 和 NavLink

     * 在使用react-router时,一定不要使用 a标签来创建超链接

     *      因为a标签创建的超链接,会自动向服务器发送请求重新加载页面

     *      而我们不希望这种情况发生

     *

     * 可以使用Link组件来创建超链接

     * NavLink和Link作用相似,只是可以指定链接激活后的样式

menu.js : 

import React from 'react'
import { Link, NavLink } from 'react-router-dom'
import classes from './Menu.module.css'

export default function Menu() {

    return (
        <div>
            <ul>
                {/* <li><a href='/'>主页</a></li>
                <li><a href='/about'>关于</a></li> */}

                {/* <Link to="/">主页</Link>
                <Link to="/about">关于</Link> */}

                <NavLink
                    exact
                    // activeClassName={classes.active}
                    activeStyle={{ textDecoration: "underline" }}
                    to="/">主页</NavLink>
                <NavLink
                    exact
                    // activeClassName={classes.active}
                    activeStyle={{ textDecoration: "underline" }}
                    to="/about">关于</NavLink>
            </ul>

        </div>
    )
}

Menu.module.css:

a:link,
a:visited {
    color: burlywood;
    text-decoration: none;
}

a:hover {
    color: black;
}

a:active {
    text-decoration: underline;
}

两种Router

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/112392.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Day44——Dp专题

文章目录子序列问题27.最长递增子序列28、最长连续递增序列29、最长重复子数组30、最长公共子序列31、不相交的线32、最大子序和33、判断子序列34、不同的子序列35、两个字符串的删除操作36、编辑距离37、回文子串38、最长回文子序列动态规划总结篇背包问题系列股票系列子序列系…

java论坛贴子网站ssm论坛项目发帖子网站论坛系统论坛源码

ssm开发的论坛系统&#xff0c;用户注册后可以发布帖子&#xff0c;其他人可以评论回复点赞评论和点赞回复&#xff0c;用户可以在个人中心管理自己的帖子&#xff0c;以及查看自己对他人的回复&#xff0c;和他人对自己的回复。 演示视频&#xff1a; https://www.bilibili.c…

图(Graph)的定义

图(Graph)的定义 文章目录图(Graph)的定义●图的形式化定义:G (V,E)●无向图和有向图的表示形式:● 有向图和无向图的定义●抽象数据类型定义ADT●图形结构属于复杂的非线性结构● 图由顶点的集合和边的集合构成 ●图的形式化定义:G (V,E) • 集合V(vertex):顶点的有限集合,…

多线程基础入门

文章目录前言一、认识线程&#xff08;一&#xff09;概念1.线程是什么2.为啥要有线程&#xff08;轻量级进程&#xff09;为什么线程比进程更轻量经典面试题&#xff1a;谈谈进程和线程的区别和联系3.线程的结构&#xff08;二&#xff09;第一个多线程程序&#xff08;三&…

Java中的自旋锁,手动实现一个自旋锁

自旋锁 CAS是实现自旋锁的基础&#xff0c;CAS利用CPU指令保证了操作的原子性&#xff0c;已达到锁的效果。自旋是指尝试获取锁的线程不会立即阻塞&#xff0c;而是采用循环的方式去尝试获取锁&#xff0c; 当线程发现锁被占用时&#xff0c;会不断循环判断锁的状态&#xff0…

计算机网络最新复习【太原理工大学】

课后题 Word 版&#xff0c;提取码&#xff1a;5201https://pan.baidu.com/s/13xzx8qr8Mnh4TWTS_dEYxA 目录 一、题型 二、考点 一、题型 1. 选择题 40 个&#xff0c;每个 1 分&#xff0c;共 40 分。&#xff08;大部分可一眼看出答案&#xff09; 2. 填空题 15 个&…

最长上升子序列优化(贪心+二分)(超级详细的讲解)

最长上升子序列优化&#xff08;贪心二分一、回顾1、问题描述2、动规代码弊端二、优化1、算法优化2、代码实现一、回顾 1、问题描述 2、动规代码弊端 我们之前的动规代码的时间复杂度是O(n2)O(n^2)O(n2)。如果大家还不知道动态规划的逻辑的话&#xff0c;建议大家先去看一下动…

我国用电信息采集系统行业应用需求及市场容量分析 现6省上线运行

用户用电信息采集系统是通过对配电变压器和终端用户的用电数据的采集和分析&#xff0c;实现用电监控、推行阶梯定价、负荷管理、线损分析&#xff0c;最终达到自动抄表、错峰用电、用电检查&#xff08;防窃电&#xff09;、负荷预测和节约用电成本等目的。建立全面的用户用电…

VuePress初学之利用模板theme创建一个个人博客网站

目录前言官方文档创建项目创建目录安装VuePress初始化项目创建文档修改package.json运行项目修改README.md的编码显示官方默认主题创建.vuepress文件创建config.js修改README.md补充logo资源运行效果更多默认主题配置开源主题vuepress-theme-reco安装脚手架初始化项目安装npm运…

为什么说L2毫秒接口的应用比较广泛?

和其它的数据接口一样&#xff0c;这L2毫秒接口也起到了一个媒介的作用&#xff0c;将所有的股票信息都传递到了另外一个软件上。因为市场上的股票种类很多&#xff0c;所以每一种股票都具有其特有的信息&#xff0c;是进行股票买卖的基础。 为什么说L2毫秒接口的应用比较广泛…

MCU-51:矩阵键盘

题目一、矩阵键盘介绍1.1 矩阵键盘1.2 扫描的概念二、编程2.1 LCD屏显示“HelloWord”2.2 矩阵键盘程序三、矩阵键盘应用一、矩阵键盘介绍 1.1 矩阵键盘 在键盘中按键数量较多时&#xff0c;为了减少I/O口的占用&#xff0c;通常将按键排列成矩阵形式 采用逐行或逐列的“扫描…

Flowable学习笔记(一):SpringBoot项目整合Flowable

1.基于k8s部署Mysql 参考&#xff1a;k8s部署mysql 我安装是去掉了卷挂载。安装过程可能出现磁盘容量不够&#xff0c;可以通过df -h查看。 镜像下载得比较慢&#xff0c;可以先用docker拉取镜像。 2.搭建SpringBoot项目 (1)搭建maven项目&#xff0c;pom.xml依赖如下&am…

一文带你深入理解【Java基础】· 网络编程(下)

写在前面 Hello大家好&#xff0c; 我是【麟-小白】&#xff0c;一位软件工程专业的学生&#xff0c;喜好计算机知识。希望大家能够一起学习进步呀&#xff01;本人是一名在读大学生&#xff0c;专业水平有限&#xff0c;如发现错误或不足之处&#xff0c;请多多指正&#xff0…

我国农村水利水电行业项目投资、水电站建设及装机容量情况分析

一、农村水利水电行业投资情况 根据水利部数据显示&#xff0c;2020年农村水利水电累计下达投资428.63亿元&#xff08;其中中央投资214.84亿元&#xff0c;地方投资213.79亿元&#xff09;。其中&#xff0c;中央预算内投资97.75亿元&#xff0c;中央财政水利发展资金100.96亿…

【MindStudio训练营第一季】基于U-Net网络的图像分割的MindStudio实践

前情说明 本作业基于Windows版MindStudio 5.0.RC3&#xff0c;远程连接ECS服务器使用&#xff0c;ECS是基于官方分享的CANN6.0.RC1_MindX_Vision3.0.RC3镜像创建的。 基于ECS&#xff08;Ascend310&#xff09;的U-Net网络的图像分割 1. U-Net网络介绍&#xff1a; U-Net模…

第12章_数据库其它调优策略

第12章_数据库其它调优策略 1.数据库调优的措施 1.1调优的目标 尽可能节省系统资源&#xff0c;以便系统可以提供更大负荷的服务。(吞吐量更大)。合理的结构设计和参数调整&#xff0c;以提高用户操作响应的速度。(响应速度更快)减少系统的瓶颈&#xff0c;提高MySQL数据库整…

PCL 基于最小生成树(MST)获取单木骨架(粗)

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 提取的过程大体上分为两个部分:生成单木MST(最小生成树)以及基于该MST获取大致的骨架结构(线条)。 具体的计算过程如下所述: 1、首先应用Delaunay三角剖分来构造初始图。Delaunay三角剖分是MST计算的基础,因…

【SQL优化】union 与 union all 的区别

先说结论&#xff1a; union all:合并查询结果 union:合并查询结果 && 去重 && 排序 &#xff08;所以 union 比union all 功能多&#xff0c;性能就会稍微差一点&#xff09; 推导过程&#xff1a; 假设有这样一张表&#xff0c;里面有4条数据 union操作…

win环境mysql版本升级到5.7过程

win环境mysql版本升级到5.7过程&#xff0c;我win电脑里mysql版本是5.0&#xff0c;版本太老了&#xff0c;也不支持和nacos集成&#xff08;nacos至少需要5.6版本的mysql&#xff09;&#xff0c;思来想去还是要升级一下自己电脑的mysql版本&#xff0c;保守点升级到5.7吧&…

Apache Hive DML语句与函数使用

Hive SQL 加载数据 之前我们加载数据是&#xff0c;创建一张表&#xff0c;将结构化文件放到hadoop对应表路径下。这样我们就将表和文件进行映射了。但是这样比较暴力&#xff0c;官方不推荐。 这样的操作是直接跳过了Hive Load加载数据 语法&#xff1a; load data [local]…