30分钟学会redux,react,react-router,快速入手react开发

86 阅读34分钟

大家好,我是无问,今天给大家分享一下react相关的知识,帮助大家快速上手react开发,这篇文章主要是讲解了react redux router相关的知识点,讲解还是比较细致的,如果帮助到你的话,请点个赞吧!👍👍👍

redux

概念

Action

action 是一个具有 type 字段的普通 JavaScript 对象。你可以将 action 视为描述应用程序中发生了什么的事件.

type 字段是一个字符串,给这个 action 一个描述性的名字,比如"todos/todoAdded"。我们通常把那个类型的字符串写成“域/事件名称”,其中第一部分是这个 action 所属的特征或类别,第二部分是发生的具体事情。

action 对象可以有其他字段,其中包含有关发生的事情的附加信息。按照惯例,我们将该信息放在名为 payload 的字段中。

一个典型的 action 对象可能如下所示:

const addTodoAction = {
  type: "todos/todoAdded",
  payload: "Buy milk",
};

Action Creator

action creator 是一个创建并返回一个 action 对象的函数。它的作用是让你不必每次都手动编写 action 对象:

const addTodo = (text) => {
  return {
    type: "todos/todoAdded",
    payload: text,
  };
};

Reducer

reducer 是一个函数,接收当前的 state 和一个 action 对象,必要时决定如何更新状态,并返回新状态。函数签名是:(state, action) => newState。 你可以将 reducer 视为一个事件监听器,它根据接收到的 action(事件)类型处理事件。

Store

当前 Redux 应用的 state 存在于一个名为 store 的对象中。

store 是通过传入一个 reducer 来创建的,并且有一个名为 getState 的方法,它返回当前状态值:

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

const store = configureStore({ reducer: counterReducer });

console.log(store.getState());
// {value: 0}

Dispatch

Redux store 有一个方法叫 dispatch更新 state 的唯一方法是调用 store.dispatch() 并传入一个 action 对象。 store 将执行所有 reducer 函数并计算出更新后的 state,调用 getState() 可以获取新 state。

dispatch 一个 action 可以形象的理解为 "触发一个事件" 。发生了一些事情,我们希望 store 知道这件事。 Reducer 就像事件监听器一样,当它们收到关注的 action 后,它就会更新 state 作为响应。

由于我们无法访问 store 本身,因此我们需要某种方式来访问 dispatch 方法。 useDispatch hooks 为我们完成了这项工作,并从 Redux store 中为我们提供了实际的 dispatch 方法:

const dispatch = useDispatch();

从那里,我们可以在用户执行诸如单击按钮之类的操作时 dispatch 对应 action:

<button
  className={styles.button}
  aria-label="Increment value"
  onClick={() => dispatch(increment())}
>
  +
</button>

Selector

Selector 函数可以从 store 状态树中提取指定的片段。随着应用变得越来越大,会遇到应用程序的不同部分需要读取相同的数据,selector 可以避免重复这样的读取逻辑:

const selectCounterValue = (state) => state.value;

const currentValue = selectCounterValue(store.getState());
console.log(currentValue);
// 2

我们的组件不能直接与 Redux store 对话,因为组件文件中不能引入 store。但是,useSelector 负责为我们在幕后与 Redux store 对话。 如果我们传入一个 selector 函数,它会为我们调用 someSelector(store.getState()),并返回结果。

基本使用

通过以下代码了解redux基本使用

import store from './store';
import ThemeContext from './ThemeContext';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <ConfigProvider locale={zhCN}>
    //通过ThemeContext.Provider 传入value={{store}} 这样通过useContext去拿到store 访问其中的方法
        <ThemeContext.Provider
            value={{
                store
            }}>
            <Vote />
        </ThemeContext.Provider>
    </ConfigProvider>
);
在函数组件中
const { store } = useContext(ThemeContext);
    return <div className="footer">
        <Button type="primary"
            onClick={() => {
                store.dispatch({
                    type: 'VOTE_SUP'
                });
            }}>
            支持
        </Button>
 在类组件中
 static contextType = ThemeContext;
    render() {
        const { store } = this.context;
        let { supNum, oppNum } = store.getState();

        return <div className="main">
            <p>支持人数:{supNum}人</p>
            <p>反对人数:{oppNum}人</p>
        </div>;
    }
import { createStore } from '../myredux';

/* 管理员:修改STORE容器中的公共状态 */
let initial = {
    supNum: 10,
    oppNum: 5
};
const reducer = function reducer(state = initial, action) {
    // state:存储STORE容器中的公共状态「最开始没有的时候,赋值初始状态值initial」
    // action:每一次基于dispatch派发的时候,传递进来的行为对象「要求必须具备type属性,存储派发的行为标识」
    // 为了接下来的操作中,我们操作state,不会直接修改容器中的状态「要等到最后return的时候」,我们需要先克隆
    state = { ...state };
    // 接下来我们需要基于派发的行为标识,修改STORE容器中的公共状态信息
    switch (action.type) {
        case 'VOTE_SUP':
            state.supNum++;
            break;
        case 'VOTE_OPP':
            state.oppNum++;
            break;
        default:
    }
    // return的内容,会整体替换STORE容器中的状态信息
    return state;
};

/* 创建STORE公共容器 */
const store = createStore(reducer);
export default store;

管理reducer action

reducer.js

/* 
合并各个模块的reducer,最后创建出一个总的reducer 
    const reducer = combineReducers({
        vote: voteReducer,
        personal: personalReducer
    });
  + reducer:是最后合并的总的reducer
  + 此时容器中的公共状态,会按照我们设置的成员名字,分模块进来管理
    state = {
       vote:{
          supNum: 10,
          oppNum: 5,
          num: 0
       },
       personal:{
          num: 100,
          info: null
       }
    }
*/
import { combineReducers } from 'redux';
import voteReducer from './voteReducer';
import personalReducer from './personalReducer';

const reducer = combineReducers({
    vote: voteReducer,
    personal: personalReducer
});
export default reducer;

store.js

import { createStore } from 'redux';
import reducer from './reducers';

const store = createStore(reducer);
export default store;

vote.jsx和votemain.jsx

export default connect((state) => state.vote)(Vote);

votefooter.jsx

export default connect(null,action.vote)(VoteFooter);

connect是什么?有什么用?

//第一个参数
export default connect(state => {
    return {
        supNum: state.vote.supNum,
        oppNum: state.vote.oppNum,
        num: state.vote.num
    }
})(Vote);
/*
 connect(mapStateToProps,mapDispatchToProps)(我们要渲染的组件)
   1. mapStateToProps:可以获取到redux中的公共状态,把需要的信息作为属性,传递组件即可
    connect(state=>{
        // state:存储redux容器中,所有模块的公共状态信息
        // 返回对象中的信息,就是要作为属性,传递给组件的信息
        return {
            supNum:state.vote.supNum,
            info:state.personal.info
        };
    })(Vote);
 */
 //所以 组件能通过props结构出来传入的属性并使用
 let { supNum, oppNum } = props;
  return (
    <div className="vote-box">
      <div className="header">
        <h2 className="title">React是很棒的前端框架</h2>
        <span className="num">{supNum + oppNum}</span>
        
 //第二个参数       
 export default connect(
    null,
    dispatch => {
        return {
            support() {
                dispatch(action.vote.support());
            },
            oppose() {
                dispatch(action.vote.oppose());
            }
        };
    }
)(VoteFooter);
/*
 connect(mapStateToProps,mapDispatchToProps)(我们要渲染的组件)
   2. mapDispatchToProps:把需要派发的任务,当做属性传递给组件
    connect(
        null,
        dispatch=>{
            // dispatch:store.dispatch 派发任务的方法

            // 返回对象中的信息,会作为**属性传递给组件**
            return {
                ...
            };
        }
    )(Vote);
 */
//所以 组件能通过props结构出来传入的属性并使用
let { support, oppose } = props;
    return <div className="footer">
        <Button type="primary" onClick={support}>支持</Button>
        <Button type="primary" danger onClick={oppose}>反对</Button>
    </div >;

image.png

工程化管理

const { store } = useContext(ThemeContext);

    return <div className="footer">
        <Button type="primary"
            onClick={() => {
                /* store.dispatch({
                    type: TYPES.VOTE_SUP
                }); */
                store.dispatch(action.vote.support());
            }}>
            支持
        </Button>
let { supNum, oppNum } = store.getState().vote;

React

语法

设置默认属性和属性校验

import PropTypes from 'prop-types';

const DemoOne = function DemoOne(props) {
    let { title } = props;
    let x = props.x;
    x = 1000;

    return <div className="demo-box">
        <h2 className="title">{title}</h2>
        <span>{x}</span>
    </div>;
};
/* 通过把函数当做对象,设置静态的私有属性方法,来给其设置属性的校验规则 */
DemoOne.defaultProps = {
    x: 0
};
DemoOne.propTypes = {
    title: PropTypes.string.isRequired,
    x: PropTypes.number,
    y: PropTypes.oneOfType([
        PropTypes.number,
        PropTypes.bool,
    ])
};

export default DemoOne;

渲染规则

image.png

插槽实现

import PropTypes from 'prop-types';
import React from 'react';
const DemoOne = function DemoOne(props) {
    let { title, x, children } = props;
    // 要对children的类型做处理
    // 可以基于 React.Children 对象中提供的方法,对props.children做处理:count\forEach\map\toArray...
    // 好处:在这些方法的内部,已经对children的各种形式做了处理
    /* if (!children) {
        children = [];
    } else if (!Array.isArray(children)) {
        children = [children];
    } */
    children = React.Children.toArray(children);
    let headerSlot = [],
        footerSlot = [],
        defaultSlot = [];
    children.forEach(child => {
        // 传递进来的插槽信息,都是编译为virtualDOM后传递进来的「而不是传递的标签」
        let { slot } = child.props;
        if (slot === 'header') {
            headerSlot.push(child);
        } else if (slot === 'footer') {
            footerSlot.push(child);
        } else {
            defaultSlot.push(child);
        }
    });

    return <div className="demo-box">
        {headerSlot}
        <br />

        <h2 className="title">{title}</h2>
        <span>{x}</span>

        <br />
        {footerSlot}
    </div>;
};
/* 设置属性的校验规则 */
DemoOne.defaultProps = {
    x: 0
};
DemoOne.propTypes = {
    title: PropTypes.string.isRequired,
    x: PropTypes.number
};
export default DemoOne;
//使用
<DemoOne title="REACT好好玩哦" x={10}>
            <span slot="footer">我是页脚</span>
            <span>哈哈哈哈</span>
            <span slot="header">我是页眉</span>
        </DemoOne>

         <DemoOne title="哇卡卡卡">
            <span>嘿嘿嘿</span>
        </DemoOne>

PureComponent

import React from "react";

// 检测是否为对象
const isObject = function isObject(obj) {
    return obj !== null && /^(object|function)$/.test(typeof obj);
};
// 对象浅比较的方法
const shallowEqual = function shallowEqual(objA, objB) {
    if (!isObject(objA) || !isObject(objB)) return false;
    if (objA === objB) return true;
    // 先比较成员的数量
    let keysA = Reflect.ownKeys(objA),
        keysB = Reflect.ownKeys(objB);
    if (keysA.length !== keysB.length) return false;
    // 数量一致,再逐一比较内部的成员「只比较第一级:浅比较」
    for (let i = 0; i < keysA.length; i++) {
        let key = keysA[i];
        // 如果一个对象中有这个成员,一个对象中没有;或者,都有这个成员,但是成员值不一样;都应该被判定为不相同!!
        if (!objB.hasOwnProperty(key) || !Object.is(objA[key], objB[key])) {
            return false;
        }
    }
    // 以上都处理完,发现没有不相同的成员,则认为两个对象是相等的
    return true;
};
class Demo extends React.PureComponent {
    state = {
        arr: [10, 20, 30] //0x001
    };
    render() {
        let { arr } = this.state; //arr->0x001
        return <div>
            {arr.map((item, index) => {
                return <span key={index} style={{
                    display: 'inline-block',
                    width: 100,
                    height: 100,
                    background: 'pink',
                    marginRight: 10
                }}>
                    {item}
                </span>;
            })}
            <br />
            <button onClick={() => {
                arr.push(40); //给0x001堆中新增一个40
                //此处push以后 地址没有变化  Reflect.ownKeys(objA) (例如:[1,2]-->[1,2,3]) 拿到的都是['0', '1', '2', 'length'] 所以比较后没有变化

                /* 
                // 无法更新的
                console.log(this.state.arr); //[10,20,30,40]
                //setState() 第一个参数是一个对象或者函数  第二个参数是一个callback
                this.setState({ arr }); //最新修改的转态地址,还是0x001「状态地址没有改」 
                */

                // this.forceUpdate(); //跳过默认加的shouldComponentUpdate,直接更新
                this.setState({
                    arr: [...arr] //我们是让arr状态值改为一个新的数组「堆地址」
                })
            }}>新增SPAN</button>
        </div >;
    }

    /* shouldComponentUpdate(nextProps, nextState) {
        let { props, state } = this;
        // props/state:修改之前的属性状态
        // nextProps/nextState:将要修改的属性状态
        //如果手动添加了shouldComponentUpdate 那么需要在手动进行比较shallowEqual

        return !shallowEqual(props, nextProps) || !shallowEqual(state, nextState);
    } */
}
export default Demo;
/* 
 PureComponent和Component的区别:
   PureComponent会给类组件默认加一个shouldComponentUpdate周期函数
     + 在此周期函数中,它对新老的属性/状态 会做一个钱浅比较
     + 如果经过浅比较,发现属性和状态并没有改变,则返回false「也就是不继续更新组建」;有变化才会去更新!!
*/

受控组件/非受控组件 ref

class Demo extends React.Component {
    box3 = React.createRef(); //this.box3=xxx

    render() {
        return <div>
            <h2 className="title" ref="titleBox">温馨提示</h2>
            <h2 className="title" ref={x => this.box2 = x}>友情提示</h2>
            <h2 className="title" ref={this.box3}>郑重提示</h2>
        </div>;
    }
    componentDidMount() {
        // 第一次渲染完毕「virtualDOM已经变为真实DOM」:此时我们可以获取需要操作的DOM元素
        console.log(this.refs.titleBox);
        console.log(this.box2);
        console.log(this.box3.current);
    }
}

export default Demo;

/*
  受控组件:基于修改数据/状态,让视图更新,达到需要的效果 「推荐」
  受控组件(Controlled Components):

-   受控组件是由 React 组件的状态(state)来管理的组件。
-   当用户输入数据或进行其他操作时,通过事件处理函数更新组件的状态,并将新的状态值传递给表单元素的 `value` 属性。
-   每次状态更新时,React 会重新渲染组件,并将新的值反映到表单元素上。
-   当需要获取表单数据时,可以通过读取组件的状态来获取最新的数据。
  非受控组件:基于ref获取DOM元素,我们操作DOM元素,来实现需求和效果「偶尔」
  -   非受控组件是由 DOM 元素自身管理的组件,React 并不直接控制其值。
-   在非受控组件中,通过使用 `ref` 来获取表单元素的值。
-   当需要获取表单数据时,可以通过访问 `ref` 对象的 `current` 属性来获取表单元素的值。

    基于ref获取DOM元素的语法
    1. 给需要获取的元素设置ref='xxx',后期基于this.refs.xxx去获取相应的DOM元素「不推荐使用:在React.StrictMode模式下会报错」
       <h2 ref="titleBox">...</h2>
       获取:this.refs.titleBox

    2. 把ref属性值设置为一个函数
       ref={x=>this.xxx=x}
         + x是函数的形参:存储的就是当前DOM元素
         + 然后我们获取的DOM元素“x”直接挂在到实例的某个属性上(例如:box2)
       获取:this.xxx

    3. 基于React.createRef()方法创建一个REF对象
       this.xxx=React.createRef();  //=> this.xxx={current:null}
       ref={REF对象(this.xxx)}
       获取:this.xxx.current

    原理:在render渲染的时候,会获取virtualDOM的ref属性
      + 如果属性值是一个  字符串,则会给this.refs增加这样的一个成员,成员值就是当前的DOM元素
      + 如果属性值是一个  函数,则会把函数执行,把当前DOM元素传递给这个函数「x->DOM元素」,而在函数执行的内部,我们一般都会把DOM元素直接挂在到实例的某个属性上
      + 如果属性值是一个  REF对象,则会把DOM元素赋值给对象的**current**属性
 */
 在函数组件中获取 `ref` 的常用方法是使用 React 的 `useRef` 钩子函数。`useRef` 允许你创建一个可变的引用,并在组件的生命周期中保持引用的一致性。

下面是一个示例,展示了如何在函数组件中使用 `useRef` 来获取 `ref`import React, { useRef } from 'react';

function MyComponent() {
  const myRef = useRef(null);

  // 在组件挂载后,可以通过 myRef.current 来访问引用的 DOM 节点

  return (
    <div ref={myRef}>
      {/* 组件的内容 */}
    </div>
  );
}

在上面的示例中,我们使用 useRef 创建了一个名为 myRef 的引用。然后,我们将 myRef 作为 ref 属性赋值给要引用的 DOM 元素(在这里是 <div>)。最终,我们可以通过 myRef.current 来访问该 DOM 元素。

this.setState

this.setState([partialState],[callback])
    [partialState]:支持部分状态更改
        this.setState({
            x:100 //不论总共有多少状态,我们只修改了x,其余的状态不动
        });
    [callback]:在状态更改/视图更新完毕后触发执行「也可以说只要执行了setState,callback一定会执行」
        + 发生在componentDidUpdate周期函数之后「DidUpdate会在任何状态更改后都触发执行;而回调函数方式,可以在指定状态更新后处理一些事情;」
        + 特殊:即便我们基于shouldComponentUpdate阻止了状态/视图的更新,DidUpdate周期函数肯定不会执行了,但是我们设置的这个callback回调函数依然会被触发执行!!
        + 类似于Vue框架中的$nextTick!!

 在React18中,setState操作都是异步的「不论是在哪执行,例如:合成事件、周期函数、定时器...」
   目的:实现状态的批处理「统一处理」
     + 有效减少更新次数,降低性能消耗
     + 有效管理代码执行的逻辑顺序
     + ...
   原理:利用了更新队列「updater」机制来处理的
     + 在当前相同的时间段内「浏览器此时可以处理的事情中」,遇到setState会立即放入到更新队列中!
     + 此时状态/视图还未更新
     + 当所有的代码操作结束,会“刷新队列”「通知更新队列中的任务执行」:把所有放入的setState合并在一起执行,只触发一次视图更新「批处理操作」
     
     handle = () => {
    let { x, y, z } = this.state;
    //触发handle后  页面渲染一次
    this.setState({ x: x + 1 });
    this.setState({ y: y + 1 });
    console.log(this.state);
    //一秒后  页面渲染一次
    setTimeout(() => {
      this.setState({ z: z + 1 });
      this.setState({ x: 100 }, () => {
        console.log(1);
      });
      console.log(this.state);
    }, 1000);
  };
  
  在React18React16中,关于setState是同步还是异步,是有一些区别的!
    React18中:不论在什么地方执行setState,它都是异步的「都是基于updater更新队列机制,实现的批处理」
    React16中:如果在合成事件「jsx元素中基于onXxx绑定的事件」、周期函数中,setState的操作是异步的!!但是如果setState出现在其他异步操作中「例如:定时器、手动获取DOM元素做的事件绑定等」,它将变为同步的操作「立即更新状态和让视图渲染」!!
    
    for (let i = 0; i < 20; i++) {
      /* this.setState({
                x: this.state.x + 1
            }); */
      // 只触发一次渲染
      this.setState((prevState) => {
        return {
          x: prevState.x + 1,
        };
      });
    }

类组件执行/更新/销毁过程


创建类组件
  创建一个构造函数(类)
    + 要求必须继承React.Component/PureComponent这个类
    + 我们习惯于使用ES6中的class创建类「因为方便」
    + 必须给当前类设置一个render的方法「放在其原型上」:在render方法中,返回需要渲染的视图

  从调用类组件「new Vote({...})」开始,类组件内部发生的事情:
    1. 初始化属性 && 规则校验
      先规则校验,校验完毕后,再处理属性的其他操作!!
      方案一: 
      constructor(props) {
        super(props); //会把传递进来的属性挂载到this实例上
        console.log(this.props); //获取到传递的属性
      }
      方案二:即便我们自己不再constructor中处理「或者constructor都没写」,在constructor处理完毕后,React内部也会把传递的props挂载到实例上;所以在其他的函数中,只要保证this是实例,就可以基于this.props获取传递的属性!
        + 同样this.props获取的属性对象也是被冻结的{只读的}  Object.isFrozen(this.props)->true

    2. 初始化状态
      状态:后期修改状态,可以触发视图的更新
      需要手动初始化,如果我们没有去做相关的处理,则默认会往实例上挂载一个state,初始值是null => this.state=null
      手动处理:
      state = {
        ...
      };
      ---------修改状态,控制视图更新
      this.state.xxx=xxx :这种操作仅仅是修改了状态值,但是无法让视图更新
      想让视图更新,我们需要基于React.Component.prototype提供的方法操作:
        @1 this.setState(partialState) 既可以修改状态,也可以让视图更新 「推荐」
          + partialState:部分状态
          this.setState({
            xxx:xxx
          });
        @2 this.forceUpdate() 强制更新

    3. 触发 componentWillMount 周期函数(钩子函数):组件第一次渲染之前
      钩子函数:在程序运行到某个阶段,我们可以基于提供一个处理函数,让开发者在这个阶段做一些自定义的事情
      + 此周期函数,目前是不安全的「虽然可以用,但是未来可能要被移除了,所以不建议使用」
        + 控制会抛出黄色警告「为了不抛出警告,我们可以暂时用 UNSAFE_componentWillMount」
      + 如果开启了React.StrictModeReact的严格模式」,则我们使用 UNSAFE_componentWillMount 这样的周期函数,控制台会直接抛出红色警告错误!!
        React.StrictMode VS "use strict"
        + "use strict"JS的严格模式
        + React.StrictModeReact的严格模式,它会去检查React中一些不规范的语法、或者是一些不建议使用的API等!!

    4. 触发 render 周期函数:渲染
    5. 触发 componentDidMount 周期函数:第一次渲染完毕
      + 已经把virtualDOM变为真实DOM了「所以我们可以获取真实DOM了」
      + ...

  组件更新的逻辑「第一种:组件内部的状态被修改,组件会更新」
    1. 触发 shouldComponentUpdate 周期函数:是否允许更新
       shouldComponentUpdate(nextProps, nextState) {
         // nextState:存储要修改的最新状态
         // this.state:存储的还是修改前的状态「此时状态还没有改变」
         console.log(this.state, nextState);

         // 此周期函数需要返回true/false
         //   返回true:允许更新,会继续执行下一个操作
         //   返回false:不允许更新,接下来啥都不处理
         return true;
       }
    2. 触发 componentWillUpdate 周期函数:更新之前
      + 此周期函数也是不安全的
      + 在这个阶段,状态/属性还没有被修改
    3. 修改状态值/属性值「让this.state.xxx改为最新的值」
    4. 触发 render 周期函数:组件更新
      + 按照最新的状态/属性,把返回的JSX编译为virtualDOM
      + 和上一次渲染出来的virtualDOM进行对比「DOM-DIFF」
      + 把差异的部分进行渲染「渲染为真实的DOM5. 触发 componentDidUpdate 周期函数:组件更新完毕
    特殊说明:如果我们是基于 this.forceUpdate() 强制更新视图,会跳过 shouldComponentUpdate 周期函数的校验,直接从 WillUpdate 开始进行更新「也就是:视图一定会触发更新」!

  组件更新的逻辑「第二种:父组件更新,触发的子组件更新」
    1. 触发 componentWillReceiveProps 周期函数:接收最新属性之前
      + 周期函数是不安全的
      UNSAFE_componentWillReceiveProps(nextProps) {
        // this.props:存储之前的属性
        // nextProps:传递进来的最新属性值
        console.log('componentWillReceiveProps:', this.props, nextProps);
      }
    2. 触发 shouldComponentUpdate 周期函数
    ......

  组件卸载的逻辑
    1. 触发 componentWillUnmount 周期函数:组件销毁之前
    2. 销毁

  父子组件嵌套,处理机制上遵循深度优先原则:父组件在操作中,遇到子组件,一定是把子组件处理完,父组件才能继续处理
    + 父组件第一次渲染
      父 willMount -> 父 render「子 willMount -> 子 render -> 子didMount」 -> 父didMount 
    + 父组件更新:
      父 shouldUpdate -> 父willUpdate -> 父 render 「子willReceiveProps -> 子 shouldUpdate -> 子willUpdate -> 子 render -> 子 didUpdate」-> 父 didUpdate
    + 父组件销毁:
      父 willUnmount -> 处理中「子willUnmount -> 子销毁」-> 父销毁
      
      函数组件是“静态组件”:
   + 组件第一次渲染完毕后,无法基于“内部的某些操作”让组件更新「无法实现“自更新”」;但是,如果调用它的父组件更新了,那么相关的子组件也一定会更新「可能传递最新的属性值进来」;
   + 函数组件具备:属性...「其他状态等内容几乎没有」
   + 优势:比类组件处理的机制简单,这样导致函数组件渲染速度更快!!
 类组件是“动态组件”:
   + 组件在第一渲染完毕后,除了父组件更新可以触发其更新外,我们还可以通过:this.setState修改状态 或者 this.forceUpdate 等方式,让组件实现“自更新”!!
   + 类组件具备:属性、状态、周期函数、ref...「几乎组件应该有的东西它都具备」
   + 优势:功能强大!!

 ===>Hooks组件「推荐」:具备了函数组件和类组件的各自优势,在函数组件的基础上,基于hooks函数,让函数组件也可以拥有状态、周期函数等,让函数组件也可以实现自更新「动态化」!!

类组件绑定方法

class Demo extends React.Component {
  /* 
    基于React内部的处理,如果我们给合成事件绑定一个“普通函数”,当事件行为触发,绑定的函数执行;方法中的this会是undefined「不好」!! 解决方案:this->实例
      + 我们可以基于JS中的bind方法:预先处理函数中的this和实参的
      + 推荐:当然也可以把绑定的函数设置为“箭头函数”,让其使用上下文中的this「也就是我们的实例」

    合成事件对象SyntheticBaseEvent:我们在React合成事件触发的时候,也可以获取到事件对象,只不过此对象是合成事件对象「React内部经过特殊处理,把各个浏览器的事件对象统一化后,构建的一个事件对象」
      合成事件对象中,也包含了浏览器内置事件对象中的一些属性和方法「常用的基本都有」
      + clientX/clientY
      + pageX/pageY
      + target
      + type
      + preventDefault
      + stopPropagation
      + ...
      + nativeEvent:基于这个属性,可以获取浏览器内置『原生』的事件对象
      + ...
    */
  handle1() {
    //Demo.prototype => Demo.prototype.handle=function handle(){}
    console.log(this); //undefined
  }
  handle2(x, y, ev) {
    // 只要方法经过bind处理了,那么最后一个实参,就是传递的合成事件对象!!
    console.log(this, x, y, ev); //实例 10 20 合成事件对象
  }
  handle3 = (ev) => {
    //实例.handle3=()=>{....}
    console.log(this); //实例
    console.log(ev); //SyntheticBaseEvent 合成事件对象「React内部经过特殊处理,把各个浏览器的事件对象统一化后,构建的一个事件对象」
  };
  handle4 = (x, ev) => {
    console.log(this);
    console.log(x, ev); //10 合成事件对象
  };

  render() {
    /*
         bind在React事件绑定的中运用
           + 绑定的方法是一个普通函数,需要改变函数中的this是实例,此时需要用到bind「一般都是绑定箭头函数」
           + 想给函数传递指定的实参,可以基于bind预先处理「bind会把事件对象以最后一个实参传递给函数」 
         */
    return (
      <div>
        <button onClick={this.handle1}>按钮1</button>
        //handle2不是箭头函数 但是可以使用bind绑定this 并传递参数
        <button onClick={this.handle2.bind(this, 10, 20)}>按钮2</button>
        //与handle1的区别是handle3是箭头函数 使用上下文中的this
        <button onClick={this.handle3}>按钮3</button>
        <button onClick={this.handle4.bind(null, 10)}>按钮4</button>
      </div>
    );
  }
}

事件合成

合成事件1.jpg


合成事件2.jpg

useState

函数组件的每一次渲染(或者是更新),都是把函数(重新)执行,产生一个全新的“私有上下文”!
   + 内部的代码也需要重新执行
   + 涉及的函数需要重新的构建{这些函数的作用域(函数执行的上级上下文),是每一次执行DEMO产生的闭包}
   + 每一次执行DEMO函数,也会把useState重新执行,但是:
     + 执行useState,只有第一次,设置的初始值会生效,其余以后再执行,获取的状态都是最新的状态值「而不是初始值」
     + 返回的修改状态的方法,每一次都是返回一个新的
     
执行一次useState:把需要的状态信息都放在对象中统一管理
    + 执行setState方法的时候,传递的是啥值,就把状态“整体”改为啥值 
      setState({
        supNum: state.supNum + 1
      }) 
      => 把状态值修改为 {supNum:11} ,oppNum成员就丢失了
      => 并不会像类组件中的this.setState一样,不支持部分状态的更新
    + 应该改为以下的处理方案
      setState({
        ...state, //在修改值之前,先把原有的所有状态,都展开赋值给新对象,再去修改要改动的那一项值即可
        supNum: state.supNum + 1
      });
      
      useState自带了性能优化的机制:
    + 每一次修改状态值的时候,会拿最新要修改的值和之前的状态值做比较「基于Object.is作比较」
    + 如果发现两次的值是一样的,则不会修改状态,也不会让视图更新「可以理解为:类似于PureComponent,在shouldComponentUpdate中做了浅比较和优化」
    
    让函数只更新一次,但是最后的结果是20
const Demo = function Demo() {
    console.log('RENDER渲染');
    let [x, setX] = useState(10);

    const handle = () => {
        for (let i = 0; i < 10; i++) {
            setX(prev => {
                // prev:存储上一次的状态值
                console.log(prev);
                return prev + 1; //返回的信息是我们要修改的状态值
            });
        }
    };
    
    将它 **作为初始化函数传递给** `useState`function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos);
}


请注意,你传递的是 `createInitialTodos` **函数本身**,
而不是 `createInitialTodos()` 调用该函数的结果。如果将函数传递给 `useState`React 仅在初始化期间调用它。

useEffect

// 第一次渲染完毕后,从服务器异步获取数据
    /* // useEffect如果设置返回值,则返回值必须是一个函数「代表组件销毁时触发」;
    //下面案例中,callback经过async的修饰,返回的是一个promise实例,不符合要求!!
    useEffect(async () => {
        let data = await queryData();
        console.log('成功:', data);
    }, []); */
    /* useEffect(() => {
        queryData()
            .then(data => {
                console.log('成功:', data);
            });
    }, []); */
    useEffect(() => {
        const next = async () => {
            let data = await queryData();
            console.log('成功:', data);
        };
        next();
    }, []);
    
    
useEffect:在函数组件中,使用生命周期函数
   useEffect(callback):没设置依赖
     + 第一次渲染完毕后,执行callback,等价于 componentDidMount
     + 在组件每一次更新完毕后,也会执行callback,等价于 componentDidUpdate

   useEffect(callback,[]):设置了,但是无依赖
     + 只有第一次渲染完毕后,才会执行callback,每一次视图更新完毕后,callback不再执行
     + 类似于 componentDidMount

   useEffect(callback,[依赖的状态(多个状态)]):
     + 第一次渲染完毕会执行callback
     + 当依赖的状态值(或者多个依赖状态中的一个)发生改变,也会触发callback执行
     + 但是依赖的状态如果没有变化,在组件更新的时候,callback是不会执行的

   useEffect(()=>{
      return ()=>{
        // 返回的小函数,会在组件释放的时候执行
        // 如果组件更新,会把上一次返回的小函数执行「可以“理解为”上一次渲染的组件释放了」
      };
   });
const Demo = function Demo() {
    let [num, setNum] = useState(0),
        [x, setX] = useState(100);

    useEffect(() => {
        // 获取最新的状态值
        console.log('@1', num);
    });
    
    useEffect(() => {
        console.log('@2', num);
    }, []);

    useEffect(() => {
        console.log('@3', num);
    }, [num]);

//参数是函数 再返回一个函数
    useEffect(() => {
        return () => {
            // 获取的是上一次的状态值
            console.log('@4', num);
        };
    });

    const handle = () => {
        setNum(num + 1);
    };
    return <div className="demo">
        <span className="num">{num}</span>
        <Button type="primary"
            size="small"
            onClick={handle}>
            新增
        </Button>
    </div>;
};

image.png

useLayoutEffect

useLayoutEffect会阻塞浏览器渲染真实DOM,优先执行Effect链表中的callback;
     useEffect不会阻塞浏览器渲染真实DOM,在渲染真实DOM的同时,去执行Effect链表中的callback;
       + useLayoutEffect设置的callback要优先于useEffect去执行!!
       + 在两者设置的callback中,依然可以获取DOM元素「原因:真实DOM对象已经创建了,区别只是浏览器是否渲染」
       + 如果在callback函数中又修改了状态值「视图又要更新」
         + useEffect:浏览器肯定是把第一次的真实已经绘制了,再去渲染第二次真实DOM
         + useLayoutEffect:浏览器是把两次真实DOM的渲染,合并在一起渲染的

     视图更新的步骤:
       第一步:基于babel-preset-react-app把JSX编译为createElement格式
       第二步:把createElement执行,创建出virtualDOM
       第三步:基于root.render方法把virtualDOM变为真实DOM对象「DOM-DIFF」
         useLayoutEffect阻塞第四步操作,先去执行Effect链表中的方法「同步操作」
         useEffect第四步操作和Effect链表中的方法执行,是同时进行的「异步操作」
       第四步:浏览器渲染和绘制真实DOM对象

creatRef和useRef

let box1 = useRef(null),
        box2 = React.createRef();
    if (!prev1) {
        // 第一次DEMO执行,把第一次创建的REF对象赋值给变量
        prev1 = box1;
        prev2 = box2;
    } else {
        // 第二次DEMO执行,我们验证一下,新创建的REF对象,和之前第一次创建的REF对象,是否一致?
        console.log(prev1 === box1); //true  useRef再每一次组件更新的时候(函数重新执行),再次执行useRef方法的时候,不会创建新的REF对象了,获取到的还是第一次创建的那个REF对象!!
        console.log(prev2 === box2); //false createRef在每一次组件更新的时候,都会创建一个全新的REF对象出来,比较浪费性能!!
        // 总结:在类组件中,创建REF对象,我们基于 React.createRef 处理;但是在函数组件中,为了保证性能,我们应该使用专属的 useRef 处理!!
    }

useImperativeHandle

useImperativeHandle – React 中文文档

// 函数子组件内部,可以有自己的状态和方法了;如何实现:基于forwardRef实现ref转发的同时,获取函数子组件内部的状态或者方法呢? => useImperativeHandle
const Child = React.forwardRef(function Child(props, ref) {
  let [text, setText] = useState("你好世界");
  const submit = () => {
    return span.current;
  };
  let span = useRef(null);
  useImperativeHandle(ref, () => {
    // 在这里返回的内容,都可以被父组件的REF对象获取到
    return {
      text,
      submit,
    };
  });
  return (
    <div className="child-box">
      <span ref={span}>哈哈哈</span>
    </div>
  );
});
const Demo = function Demo() {
  let x = useRef(null);
  useEffect(() => {
    console.log(x.current);
    console.log(x.current.submit());
  }, []);
  let click = () => {
    console.log(x.current.submit());
  };
  return (
    <div className="demo">
      <Child ref={x} />
      <div onClick={click}>11111</div>
    </div>
  );

useMemo

let xxx = useMemo(callback,[dependencies])
       + 第一次渲染组件的时候,callback会执行
       + 后期只有依赖的状态值发生改变,callback才会再执行
       + 每一次会把callback执行的返回结果赋值给xxx
       + useMemo具备“计算缓存”,在依赖的状态值没有发生改变,callback没有触发执行的时候,xxx获取的是上一次计算出来的结果
       和Vue中的计算属性非常的类似!!
       xxx直接在模板中使用

useCallback

const xxx = useCallback(callback,[dependencies])
       + 组件第一次渲染,useCallback执行,创建一个函数“callback”,赋值给xxx
       + 组件后续每一次更新,判断依赖的状态值是否改变,如果改变,则重新创建新的函数堆,赋值给xxx;但是如果,依赖的状态没有更新「或者没有设置依赖“[]”」则xxx获取的一直是第一次创建的函数堆,不会创建新的函数出来!!
       + 或者说,基于useCallback,可以始终获取第一次创建函数的堆内存地址(或者说函数的引用)
       

const Child = React.memo(function Child(props) {
    console.log('Child Render');
    return <div>
        我是子组件
    </div>;
});

/* 父组件 */
// 诉求:当父组件更新的时候,因为传递给子组件的属性仅仅是一个函数「特点:基本应该算是不变的」,所以不想再让子组件也跟着更新了!
//   + 第一条:传递给子组件的属性(函数),每一次需要是相同的堆内存地址(是一致的) . 基于useCallback处理!!
//   + 第二条:在子组件内部也要做一个处理,验证父组件传递的属性是否发生改变,如果没有变化,则让子组件不能更新,有变化才需要更新 . 继承React.PureComponent即可「在shouldComponentUpdate中对新老属性做了浅比较」!! 函数组件是基于 React.memo 函数,对新老传递的属性做比较,如果不一致,才会把函数组件执行,如果一致,则不让子组件更新!!
const Demo = function Demo() {
    let [x, setX] = useState(0);

    // const handle = () => { };  //第一次:0x001  第二次:0x101  第三次:0x201 ...
    const handle = useCallback(() => { }, []); //第一次:0x001  第二次:0x001  第三次:0x001 ...

    return <div className="vote-box">
        <Child handle={handle} />
        <div className="main">
            <p>{x}</p>
        </div>
        <div className="footer">
            <Button type="primary" onClick={() => setX(x + 1)}>累加</Button>
        </div>
    </div>;
};

自定义Hook

/* 
自定义Hook 
  作用:提取封装一些公共的处理逻辑
  玩法:创建一个函数,名字需要是 useXxx ,后期就可以在组件中调用这个方法!
*/
const usePartialState = function usePartialState(initialValue) {
    let [state, setState] = useState(initialValue);
    // setState:不支持部分状态更改的
    // setPartial:我们期望这个方法可以支持部分状态的更改
    const setPartial = function setPartial(partialState) {
        setState({
            ...state,
            ...partialState
        });
    };
    return [state, setPartial];
};

// 自定义Hook,在组件第一次渲染完毕后,统一干点啥事
const useDidMount = function useDidMount(title) {
    if (!title) title = 'React系统课';
    // 基于React内置的Hook函数,实现需求即可
    useEffect(() => {
        document.title = title;
    }, []);
};

高阶组件HOC

/* 
React高阶组件:利用JS中的闭包「柯理化函数」实现的组件代理
   我们可以在代理组件中,经过业务逻辑的处理,获取一些信息,最后基于属性等方案,传递给我们最终要渲染的组件!!
*/
import React from "react";
const Demo = function Demo(props) {
  console.log("Demo中的属性:", props);
  return <div className="demo">我是DEMO</div>;
};
// 执行ProxyTest方法,传递一个组件进来「Component」
const ProxyTest = function ProxyTest(Component) {
  // Component->Demo
  return function HOC(props) {
    let isUse = false;
    // console.log(props); //=>{x:10,y:20,enable:true}
    // 真实要渲染的是Demo组件:把获取的props要传递给Demo
    /* let { x, y, enable } = props;
        return <Component x={x} y={y} enable={enable} />; */
    return <Component {...props} isUse={isUse} />;
  };
};
export default ProxyTest(Demo);
// 返回的是一个函数(组件) 通过使用组件,传递参数  正确渲染ProxyTest(Demo)中传入的参数(组件)
// 把函数执行的返回结果「应该是一个组件」,基于ES6Module规范导出,供App导入使用!!
// 当前案例中,我们导出的是HOC「HOC:higher-order-components」

createContext

使用 createContext 创建组件能够提供与读取的 上下文(context)

const SomeContext = createContext(defaultValue)

SomeContext.Provider 

用上下文 provider 包裹组件,以为里面所有的组件指定一个上下文的值:

function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
    </ThemeContext.Provider>
  );
}

Props 

  • value:该值为想传递给所有处于这个 provider 内读取该上下文的组件,无论它们处于多深的层级。上下文的值可以为任何类型。provider 内的组件可通过调用 useContext(SomeContext) 获取上方距离它最近的上下文 provider 的 value新代码都应该通过 useContext() 来读取上下文
function Button() {
  // ✅ 推荐方式
  const theme = useContext(ThemeContext);
  return <button className={theme} />;
}
// Contexts.js
import { createContext } from 'react';
export const ThemeContext = createContext('light');
export const AuthContext = createContext(null);

在其他文件中定义的组件可以使用 import 语句读取或提供该 context:

// Button.js
import { ThemeContext } from './Contexts.js';
function Button() {
  const theme = useContext(ThemeContext);
}

react样式问题

CSSModules

帮助解决 CSS 样式在全局作用域中命名冲突的问题,并提供模块化的 CSS 样式管理方式。

使用 CSS Modules,你可以将 CSS 文件与 React 组件关联起来,并在组件中引用样式。每个组件都有自己的局部作用域,样式类名会自动进行局部化,避免了全局样式冲突的问题。在编译过程中,CSS Modules 会将样式类名进行转换,并为每个类名生成一个唯一的标识符。这样,即使多个组件使用了相同的样式类名,它们最终生成的类名也会不同,避免了冲突。

/* styles.css */
.container {
  background-color: #f1f1f1;
  padding: 10px;
}
<div className={styles.container}>

createUseStyles

import { createUseStyles } from 'react-jss';
/* 
基于createUseStyles方法,构建组件需要的样式;返回结果是一个自定义Hook函数! 
  + 对象中的每个成员就是创建的样式类名
  + 可以类似于less等预编译语言中的“嵌套语法”,给其后代/伪类等设置样式!!
自定义Hook执行,返回一个对象,对象中包含:
  + 我们创建的样式类名,作为属性名
  + 编译后的样式类名「唯一的」,作为属性值
  {box: 'box-0-2-1', title: 'title-0-2-2', list: 'list-0-2-3'}
而我们在JS中编写的样式,最后会编译为:
    .box-0-2-1 {
        width: 300px;
        background-color: lightblue;
    }
    .title-0-2-2 {
        color: red;
        font-size: 20px;
    }
    .title-0-2-2:hover {
        color: green;
    }
    .list-0-2-3 a {
        color: #000;
        font-size: 16px;
    }
相对于CSSModules的好处:因为样式是写在JS中的,我们就可以基于一些逻辑操作,实现样式的动态化管理!!
*/
const useStyles = createUseStyles({
    box: {
        backgroundColor: 'lightblue',
        width: '300px'
    },
    title: {
        fontSize: '20px',
        color: 'red',
        '&:hover': {
            color: props => props.color
        }
    },
    list: props => {
        return {
            '& a': {
                fontSize: props.size + 'px',
                color: '#000'
            }
        };
    }
});
const Nav = function Nav() {
    let { box, title, list } = useStyles({
        size: 14,
        color: 'orange'
    });
    return <nav className={box}>
        <h2 className={title}>购物商城</h2>
        <div className={list}>
            <a href="">首页</a>
            <a href="">秒杀</a>
            <a href="">我的</a>
        </div>
    </nav>;
};

image.png

styled-components

/* 编写组件的样式:基于CSS-IN-JS思想中的styled-components插件 */
/*
 基于 “styled.标签名” 这种方式编写需要的样式
   + 样式要写在“ES6模板字符串”中
   + 返回并且导出的结果是一个自定义组件

 如果编写样式的时候没有提示,我们可以在vscode中安装一个官方插件:vscode-styled-components
 */
const MenuBox = styled.div`
    background-color: lightpink;
    width: 400px;
    .title{
        font-size: ${titleSize}px;
        line-height: 40px;
        &:hover{
            color: ${colorRed};
        }
    }
    li{
        font-size: 16px !important;
    }
`;
class Menu extends React.Component {
    render() {
        return <MenuBox>
            <h2 className="title">产品分类列表</h2>
            <CommonListBox>
                <li>手机</li>
                <li>电脑</li>
                <li>家电</li>
            </CommonListBox>
        </MenuBox>;
    }
};

react中路由问题

react-router-dom

核心:

基于<HashRouter>把所有要渲染的内容包起来,开启HASH路由 
      + 后续用到的<Route>、<Link>等,都需要在HashRouter/BrowserRouter中使用
      + 开启后,整个页面地址,默认会设置一个 #/ 哈希值

    Link实现路由切换/跳转的组件
      + 最后渲染完毕的结果依然是A标签
      + 它可以根据路由模式,自动设定点击A切换的方式

路由基础:

{/* 
             Switch:确保路由中,只要有一项匹配,则不再继续向下匹配
             exact:设置匹配模式为精准匹配
             */}
            <Switch>
                <Route exact path="/" component={A} />
                <Route path="/b" component={B} />
                <Route path="/c" render={() => {
                    // 当路由地址匹配后,先把render函数执行,返回的返回值就是我们需要渲染的内容
                    // 在此函数中,可以处理一些事情,例如:登录态检验....
                    let isLogin = true;
                    if (isLogin) {
                        return <C />;
                    }
                    return <Redirect to="/login" />
                }} />
                {/* 
                  // 放在最后一项,path设置※或者不写,意思是:以上都不匹配,则执行这个规则
                  <Route path="*" component={404组件} /> 
                  // 当然也可以不设置404组件,而是重定向到默认 / 地址:
                  <Redirect from="" to="" exact/>
                    + from:从哪个地址来
                    + to:重定向的地址
                    + exact是对from地址的修饰,开启精准匹配
                */}
                <Redirect to="/" />
            </Switch>

配置路由表

/* 
配置路由表:数组,数组中每一项就是每一个需要配置的路由规则
   + redirect:true 此配置是重定向
   + from:来源的地址
   + to:重定向的地址
   + exact:是否精准匹配
   + path:匹配的路径
   + component:渲染的组件
   + name:路由名称(命名路由)
   + meta:{} 路由元信息「包含当前路由的一些信息,当路由匹配后,我们可以拿这些信息做一些事情...」
   + children:[] 子路由
   + ...
*/
import { lazy } from 'react';
import A from '../views/A';
import aRoutes from './aRoutes';

// 一级路由的理由表
const routes = [{
    redirect: true,
    from: '/',
    to: '/a',
    exact: true
}, {
    path: '/a',
    name: 'a',
    component: A,
    meta: {},
    children: aRoutes
}, {
    path: '/b',
    name: 'b',
    component: lazy(() => import('../views/B')),
    meta: {}
}, {
    path: '/c/:id?/:name?',
    name: 'c',
    component: lazy(() => import('../views/C')),
    meta: {}
}, {
    redirect: true,
    to: '/a'
}];
export default routes;

自定义路由渲染组件,传入路由表

const App = function App() {
    return <HashRouter>
        <HomeHead />
        {/* 路由容器 */}
        <div className="content">
            <RouterView routes={routes} />
        </div>
    </HashRouter>;
};

RouterView路由容器

/* 调用组件的时候,基于属性传递路由表进来,我们根据路由表,动态设定路由的匹配规则 */
const RouterView = function RouterView(props) {
    // 获取传递的路由表
    let { routes } = props;
    return <Switch>
        {/* 循环设置路由匹配规则 */}
        {routes.map((item, index) => {
            let { redirect, from, to, exact, path, component: Component } = item,
                config = {};
            if (redirect) {
                // 重定向的规则
                config = { to };
                if (from) config.from = from;
                if (exact) config.exact = true;
                return <Redirect key={index} {...config} />;
            }
            // 正常匹配规则
            config = { path };
            if (exact) config.exact = true;
            return <Route key={index} {...config} render={(props) => {
                // 统一基于RENDER函数处理,当某个路由匹配,后期在这里可以做一些其它事情
                // Suspense.fallback:在异步加载的组件没有处理完成之前,先展示的Loading效果!!
                return <Suspense fallback={<>正在处理中...</>}>
                    <Component {...props} />
                </Suspense>;
            }} />;
        })}
    </Switch>;
};

useHistory/useLocation/useRouteMatch和withRouter

/*
 只要在<HashRouter>/<BrowserRouter>中渲染的组件:
   我们在组件内部,基于useHistory/useLocation/useRouteMatch这些Hook函数,就可以获取history/location/match这些对象信息!!
   即便这个组件并不是基于<Route>匹配渲染的!!
 只有基于<Route>匹配渲染的组件,才可以基于props属性,获取这三个对象信息!!

 问题:如果当前组件是一个类组件,在<HashRouter>内,但是并没有经过<Route>匹配渲染,我们如何获取三个对象信息呢?
   解决方案:基于函数高阶组件,自己包裹一层进行处理!!
   在react-router-dom v5版本中,自带了一个高阶组件 withRouter ,就是用来解决这个问题的!!
 */
class HomeHead extends React.Component {
    render() {
        // console.log(this.props); //有三个对象信息了
        return <NavBox>
            {/* 
             NavLink VS Link
               都是实现路由跳转的,语法上几乎一样,区别就是:
               每一次页面加载或者路由切换完毕,都会拿最新的路由地址,和NavLink中to指定的地址「或者pathname地址」进行匹配
                 + 匹配上的这一样,会默认设置active选中样式类「我们可以基于activeClassName重新设置选中的样式类名」
                 + 我们也可以设置exact精准匹配
               基于这样的机制,我们就可以给选中的导航设置相关的选中样式!!
             */}
            <NavLink to="/a">A</NavLink>
            <NavLink to="/b">B</NavLink>
            <NavLink to="/c">C</NavLink>
        </NavBox>;
    }
}
/* const Handle = function Handle(Component) {
    // Component:真正需要渲染的组件 HomeHead
    // 返回一个代理/高阶组件「导出去供别的地方调用的就是HOC组件」
    return function HOC(props) {
        // props:调用HOC传递的属性,其实这些属性原本是想传递给HomeHead的
        // HOC是个函数组件,我们可以在这里基于Hook函数获取需要的三个对象信息,然后手动作为属性,传递给HomeHead
        let history = useHistory(),
            location = useLocation(),
            match = useRouteMatch();
        return <Component {...props} history={history} location={location} match={match} />;
    };
}; */
export default withRouter(HomeHead);

/* const HomeHead = function HomeHead(props) {
    console.log(props);
    console.log(useHistory());
    return <NavBox>
        <Link to="/a">A</Link>
        <Link to="/b">B</Link>
        <Link to="/c">C</Link>
    </NavBox>;
};
export default HomeHead; */

image.png

hash路由

<nav class="nav-box">
        <a href="#/">首页</a>
        <a href="#/product">产品中心</a>
        <a href="#/personal">个人中心</a>
    </nav>
    <div class="view-box"></div>
<script>
        /*
        HASH路由 
          + 改变页面的哈希值(#/xxx),主页面是不会刷新的
          + 根据不同的哈希值,让容器中渲染不同的内容「组件」
        */
        // 获取渲染内容的容器
        const viewBox = document.querySelector('.view-box');
        // 构建一个路由匹配表:每当我们重新加载页面、或者路由切换(切换哈希值),都先到这个路由表中进行匹配;根据当前页面的哈希值,匹配出要渲染的内容(组件)!!
        const routes = [{
            path: '/',
            component: '首页的内容'
        }, {
            path: '/product',
            component: '产品中心的内容'
        }, {
            path: '/personal',
            component: '个人中心的内容'
        }];

        // 路由匹配的办法
        const routerMatch = function routerMatch() {
            let hash = location.hash.substring(1),
                text = "";
            routes.forEach(item => {
                if (item.path === hash) {
                    text = item.component;
                }
            });
            viewBox.innerHTML = text;
        };

        // 一进来要展示的是首页的信息,所以默认改变一下HASH值
        location.hash = '/';
        routerMatch();

        // 监测HASH值的变化,重新进行路由匹配
        window.onhashchange = routerMatch;
    </script>

history路由

<nav class="nav-box">
        <a href="/">首页</a>
        <a href="/product">产品中心</a>
        <a href="/personal">个人中心</a>
    </nav>
    <div class="view-box"></div>

    <!-- IMPORT JS -->
    <script>
        /*
        History路由{浏览器路由} 
          + 利用了H5中的HistoryAPI来实现页面地址的切换「可以不刷新页面」
          + 根据不同的地址,到路由表中进行匹配,让容器中渲染不同的内容「组件」
        问题:我们切换的地址,在页面不刷新的情况下是没有问题的,但是如果页面刷新,这个地址是不存在的,会报404错误!!此时我们需要服务器的配合:在地址不存在的情况下,也可以把主页面内容返回!!
        */
        const viewBox = document.querySelector('.view-box'),
            navBox = document.querySelector('.nav-box');

        // 点击A实现页面地址切换,但是不能刷新页面
        navBox.onclick = function (ev) {
            let target = ev.target;
            if (target.tagName === 'A') {
                ev.preventDefault(); //阻止A标签页面跳转&刷新的默认行为
                history.pushState({}, "", target.href);
                // 去路由匹配
                routerMatch();
            }
        };

        // 路由匹配的办法
        const routes = [{
            path: '/',
            component: '首页的内容'
        }, {
            path: '/product',
            component: '产品中心的内容'
        }, {
            path: '/personal',
            component: '个人中心的内容'
        }];
        const routerMatch = function routerMatch() {
            let path = location.pathname,
                text = "";
            routes.forEach(item => {
                if (item.path === path) {
                    text = item.component;
                }
            });
            viewBox.innerHTML = text;
        };

        // 默认展示首页
        history.pushState({}, "", "/");
        routerMatch();

        // 监听popstate地址变化事件;此事件:执行go/forward/back等方法(或者点击前进后退按钮)可以触发,但是执行pushState/replaceState等方法无法触发!!
        window.onpopstate = routerMatch;
    </script>

v6

基础:

const App = function App() {
    return <HashRouter>
        <HomeHead />
        <div className="content">
            {/* 
            所有的路由匹配规则,放在<Routes>中;
            每一条规则的匹配,还是基于<Route>;
              + 路由匹配成功,不再基于component/render控制渲染的组件,而是基于element,语法格式是<Component/>
              + 不再需要Switch,默认就是一个匹配成功,就不在匹配下面的了
              + 不再需要exact,默认每一项匹配都是精准匹配
            原有的<Redirect>操作,被 <Navigate to="/" /> 代替!!
              + 遇到 <Navigate/> 组件,路由就会跳转,跳转到to指定的路由地址
              + 设置 replace 属性,则不会新增立即记录,而是替换现有记录
              + <Navigate to={{...}}/> to的值可以是一个对象:pathname需要跳转的地址、search问号传参信息
            */}
            <Routes>
                <Route path="/" element={<Navigate to="/a" />} />
                <Route path="/a" element={<A />}>
                    {/* v6版本中,要求所有的路由(二级或者多级路由),不在分散到各个组件中编写,而是统一都写在一起进行处理!! */}
                    <Route path="/a" element={<Navigate to="/a/a1" />} />
                    <Route path="/a/a1" element={<A1 />} />
                    <Route path="/a/a2" element={<A2 />} />
                    <Route path="/a/a3" element={<A3 />} />
                </Route>
                <Route path="/b" element={<B />} />
                <Route path="/c/:id?/:name?" element={<C />} />
                {/* 如果以上都不匹配,我们可以渲染404组件,也可以重定向到A组件「传递不同的问号参数信息」 */}
                <Route path="*" element={<Navigate to={{
                    pathname: '/a',
                    search: '?from=404'
                }} />} />
            </Routes>
        </div>
    </HashRouter>;
};

封装:

import { Routes, Route, useNavigate, useLocation, useParams, useSearchParams } from 'react-router-dom';

/* 统一渲染的组件:在这里可以做一些事情「例如:权限/登录态校验,传递路由信息的属性...」 */
const Element = function Element(props) {
    let { component: Component } = props;

    // 把路由信息先获取到,最后基于属性传递给组件:只要是基于<Route>匹配渲染的组件,都可以基于属性获取路由信息
    const navigate = useNavigate(),
        location = useLocation(),
        params = useParams(),
        [usp] = useSearchParams();

    // 最后要把Component进行渲染
    return <Component navigate={navigate} location={location} params={params} usp={usp} />;
};

/* 递归创建Route */
const createRoute = function createRoute(routes) {
    return <>
        {routes.map((item, index) => {
            let { path, children } = item;
            // 每一次路由匹配成功,不直接渲染我们设定的组件,而是渲染Element;在Element做一些特殊处理后,再去渲染我们真实要渲染的组件!!
            return <Route key={index} path={path} element={<Element {...item} />}>
                {/* 基于递归方式,绑定子集路由 */}
                {Array.isArray(children) ? createRoute(children) : null}
            </Route>;
        })}
    </>;
};

/* 路由容器 */
export default function RouterView() {
    return <Suspense fallback={<>正在处理中...</>}>
        <Routes>
            {createRoute(routes)}
        </Routes>
    </Suspense>;
};

/* 创建withRouter */
export const withRouter = function withRouter(Component) {
    // Component:真实要渲染的组件
    return function HOC(props) {
        // 提前获取路由信息,作为属性传递给Component
        const navigate = useNavigate(),
            location = useLocation(),
            params = useParams(),
            [usp] = useSearchParams();
        return <Component {...props} navigate={navigate} location={location} params={params} usp={usp} />;
    };
};

使用withRouter

const HomeHead = function HomeHead(props) {
  console.log(props); //也具备了路由信息
  return (
    <NavBox>
      <NavLink to="/a">A</NavLink>
      <NavLink to="/b">B</NavLink>
      <NavLink to="/c">C</NavLink>
    </NavBox>
  );
};
export default withRouter(HomeHead);

/* 在react-router-dom v6中 ,实现路由跳转的方式:

  • <Link/NavLink to="/a" > 点击跳转路由
  • 遇到这个组件就会跳转
  • 编程式导航:取消了history对象,基于navigate函数实现路由跳转 import { useNavigate } from 'react-router-dom'; const navigate = useNavigate(); navigate('/c'); navigate('/c', { replace: true }); navigate({ pathname: '/c' }); navigate({ pathname: '/c', search: '?id=100&name=zhufeng' }); ... */

/* 在react-router-dom v6中 ,即便当前组件是基于匹配渲染的,也不会基于属性,把history/location/match传递给组件!!想获取相关的信息,我们只能基于Hook函数处理!!

  • 首先要确保,需要使用“路由Hook”的组件,是在Router「HashRouter或BrowserRouter」内部包着的,否则使用这些Hook会报错!!
  • 只要在内部包裹的组件,不论是否是基于匹配渲染的
    • 默认都不可能再基于props获取相关的对象信息了
    • 只能基于“路由Hook”去获取!!

为了在类组件中也可以获取路由的相关信息:

  1. 稍后我们构建路由表的时候,我们会想办法:继续让基于匹配渲染的组件,可以基于属性获取需要的信息
  2. 不是基于匹配渲染的组件,我们需要自己重写withRouter「v6中干掉了这个API」,让其和基于匹配渲染的组件,具备相同的属性!! */

传参

    const handle = () => {
        /* 
        // 问号传参
        navigate({
            pathname: '/c',
            search: qs.stringify({
                id: 100,
                name: 'zhufeng'
            })
        }); 
        */

        /* // 路径参数
        navigate(`/c/100/zhufeng`); */

        // 隐式传参
        navigate('/c', {
            //历史记录池替换现有地址
            replace: true,
            //隐式传参信息
            state: {
                id: 100,
                name: 'zhufeng'
            }
        });
    };