React笔记

29 阅读24分钟

生命周期

类组件

各生命周期解析

1.componentWillMount

已废弃。
componentWillMount() 是 React 类组件中的挂载前生命周期方法,在组件第一次渲染前被调用一次。

2.render ***


render() 是一个必须实现的方法,用来描述组件的 UI 结构。

3.componentDidMount ***


componentDidMount() 是 React 类组件的生命周期方法,在组件 挂载(mount)完成后调用。

4.shouldComponentUpdate **

shouldComponentUpdate 是 React 类组件中的一个生命周期方法,它主要用于 性能优化 —— 决定组件在接收到新的 props 或 state 时是否需要重新渲染。
​
注意:
如果创建类组件时继承的是React.PureComponent,会默认添加上这个生命周期,会对新老属性/状态做浅比较,没变化不会进行后续的更新。
如果继承的是React.Component,不会默认加这个生命周期。

5.componentWillUpdate


已废弃。
componentWillUpdate 是 React 类组件中的一个 过时生命周期方法,它在组件 更新之前(即 render 执行之前) 被调用,用于执行一些在组件更新前的准备操作。

6.componentDidUpdate **


componentDidUpdate 是 React 类组件中的一个常用生命周期方法,在组件 更新完成(即 DOM 已更新)后 被调用。

7.componentWillReceiveProps


已废弃。
父组件更新导致子组件更新时,子组件触发。

8.componentWillUnmount ***


componentWillUnmount 是 React 类组件中的卸载阶段的生命周期方法,在组件即将从页面上被移除时触发。

各情况下生命周期的执行

1.组件初始化过程触发的生命周期


componentWillMount -> render -> componentDidMount

2.组件更新过程触发的生命周期


shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate

3.组件销毁过程触发的生命周期


componentWillUnmount -> 组件销毁

4.父子组件嵌套,初始化过程触发的生命周期


父componentWillMount -> 父render【子componentWillMount -> 子render -> 子componentDidMount】-> 父componentDidMount

5.父子组件嵌套,更新过程触发的生命周期


父shouldComponentUpdate -> 父componentWillUpdate -> 父render【子componentWillReceiveProps -> 子shouldComponentUpdate -> 子componentWillUpdate -> 子render -> 子componentDidUpdate】-> 父componentDidUpdate

6.父子组件嵌套,销毁过程触发的生命周期


父componentWillUnmount -> 销毁中【子componentWillUnmount -> 子销毁】-> 父销毁

函数组件

useEffect的常用写法

1.组件挂载时执行一次(类似 componentDidMount


useEffect(() => {
  
}, []) // 空依赖数组表示只执行一次

2.依赖数据变化时执行(类似 componentDidUpdate


useEffect(() => {
  
}, [count]) // count 改变时才执行

3.组件卸载时执行(类似 componentWillUnmount


useEffect(() => {
  return () => {
    // 组件卸载时执行
  }
}, [])

状态管理

useState

常用写法


const [num,setNum] = useState(10)
setNum(20)

// 作用: 对于一些初始值要经过复杂计算得到,用函数写法在里面写逻辑,这样的好处是: 只有第一次渲染组件处理这些逻辑,以后组件更新,这样的逻辑就不会再运行了.
const [num,setNum] = useState(()=>{
  return 10
})
setNum(20)

const [num,setNum] = useState(10)
setNum(prev=>{ // prev:存储上一次的状态值
    return prev+1 // 返回的信息是我们要修改的状态值
})

注意点


const [num,setNum] = useState(10)
const handle = ()=>{
  setNum(num+10)
  setNum(num+20)
  setNum(num+30)
}
/*
最终结果:num的值为40,组件更新1次
​
解析:因为闭包的原因,他们相当于:
setNum(10+10)
setNum(10+20)
setNum(10+30)
setNum有批处理机制,在更新队列依次执行后,得到实际的更新结果是:将num更新成40,更新视图
*/

const [num,setNum] = useState(10)
const handle = ()=>{
  setNum(num+10)
  flushSync()
  setNum(num+10)
  flushSync()
  setNum(num+10)
}
/*
最终结果:num的值为20,组件不会更新3次(按理解是只会更新1次,不知道啥原因更新了2次)
​
解析:因为闭包的原因,他们相当于:
setNum(10+10)
flushSync()
setNum(10+10)
flushSync()
setNum(10+10)
flushSync()能刷新更新队列,将已经在更新队列的任务执行并更新视图。
第一个setNum(10+10)推到更新队列后,接着flushSync(),此时实际的更新结果:将num更新成20,更新页面。
第二个setNum(10+10)推到更新队列后,接着flushSync(),此时实际的更新结果:将num更新成20,更新页面。但此时组件的num值已经被第一个setNum(10+10)修改成20了,由于useState自带的性能优化机制,新旧值一致,不进行后续的更新。
第三个setNum(10+10)和第二个一样。
*/

const [num,setNum] = useState(10)
const handle = ()=>{
  setNum(prev=>{
      console.log(prev); // 10
      return prev+10
  })
  setNum(prev=>{
      console.log(prev); // 20
      return prev+10
  })
  setNum(prev=>{
      console.log(prev); // 30
      return prev+10
  })
}
/*
最终结果:num的值为40,组件更新1次
​
解析:setNum的函数写法,prev的值是更新队列中的最新结果,在更新队列中执行时:
执行第一个setNum时,prev的值为10,故相当于:
setNum(10+10)
执行完第一个setNum时,prev的值变成了10+10=20。
执行第二个setNum时,prev的值为20,故相当于:
setNum(20+10)
执行完第二个setNum时,prev的值变成了20+10=30。
执行第三个setNum时,prev的值为30,故相当于:
setNum(30+10)
所以实际的更新结果是:将num更新成40,更新视图。
*/

const [num,setNum] = useState(10)
const handleSync = ()=>{
  return new Promise((resolve, reject) => {
    resolve()
  })
}
const handle = ()=>{
  setNum(20)
  handleSync().then(()=>{
    setNum(40)
    setNum(50)
  })
  setTimeout(()=>{
    setNum(60)
    setNum(70)
  })
  setNum(30)
}
console.log('渲染',num);
​
/*
在React18中:
最终结果:组件更新3次:
'渲染',30
'渲染',50
'渲染',70
​
在React16中:
最终结果:组件更新5次:
'渲染',30
'渲染',40
'渲染',50
'渲染',60
'渲染',70
​
解析:
react18: setNum操作都是是异步的【合成时间、周期函数、定时器、Promise、手动获取DOM元素做的事件绑定...】
react16:在【合成事件、周期函数】中,setNum操作是异步的;在【定时器、Promise、手动获取DOM元素做的事件绑定...】,setNum操作是同步的。
​
*/

useReducer

常用写法


const numReducer = (state,action)=>{
    state = {...state}
    const { step } = action.payload
    switch (action.type) {
        case 'plus':
            state.num += step
            break;
        case 'minus':
            state.num -= step
            break;
    }
    return state
}
const numInitialArg = {num:0}
​
// 定义
const [numState,numDispatch] = useReducer(numReducer,numInitialArg)
​
// 修改值(派发)
numDispatch({type:'plus',payload:{step:1}})

// 第三个属性init的作用: 对于一些初始值要经过复杂计算得到,在第三个属性init的作用中写,这样的好处是: 只有第一次渲染组件处理这些逻辑,以后组件更新,这里的逻辑就不会再运行了.const numReducer = (state,action)=>{
    state = {...state}
    const { step } = action.payload
    switch (action.type) {
        case 'plus':
            state.num += step
            break;
    }
    return state
}
const numInitialArg = {num:0}
const init = (initialArg)=>{
    // 假设此处为大量复杂的计算
    return {...initialArg,num:20}
}
​
const [numState,numDispatch] = useReducer(numReducer,numInitialArg,init)

注意点


const numReducer = (state,action)=>{
    state = {...state}
    const { step } = action.payload
    switch (action.type) {
        case 'plus':
            state.num += step
            break;
    }
    return state
}
const numInitialArg = {num:0}
const init = (initialArg)=>{
    // 假设此处为大量复杂的计算
    return initialArg
}
​
const [numState,numDispatch] = useReduce(numReducer,numInitialArg,init)
​
const handle = ()=>{
  numDispatch({type:'plus',payload:{step:1}})
  numDispatch({type:'plus',payload:{step:2}})
  numDispatch({type:'plus',payload:{step:3}})
}
​
/*
最后的结果为:6,组件更新1次
useReduce也会进行批处理,不过useReducer的这种写法,dispatch派发就像useState的setState的函数式写法一样,会依次执行reducerh函数,函数的state为上一次派发执行完成的结果。
*/

flushSync


flushSync:用于刷新更新队列,即将前面已经放入到更新队列的任务全部执行,并更新视图,再执行后续的代码。

React.memo


React.memo 能提升性能,避免因父组件重新渲染导致子组件不必要地重新渲染。
​
它是浅比较 props,如果传入的是对象、数组、函数,记得配合 useMemo / useCallback 使用,否则每次都变引用,失去缓存效果
​
不适合使用在轻量组件上(性能收益不大,反而增加开销)

useMemo


useMemo 类似 vue的计算属性
​
仅当依赖到状态方式改变时才会重新计算,即[x,y]改变会重新执行回调函数里面代码
​
作用:
1.避免不必要的复杂计算。
2.缓存依赖对象,防止子组件无意义更新

// 作用:1.避免不必要的复杂计算。
const [x,setX] = useState(1)
const [y,setY] = useState(1)
    
const count = useMemo(()=>{
    // 假设这里涉及大量的计算逻辑
    return x+y
},[x,y])

/*
    对于对象和数组类型,假设需传递给子组件,并且子组件使用了React.memo, 可防止子组件无意义更新
*/
​
// 比如这样写:组件刷新时,obj的地址会变化,因此子组件会重新渲染
let obj = {}
<Son obj={obj} ></Son>
​
// 改成这样写:组件刷新是,obj的地址不会变化,子组件不会重新渲染
const obj = useMemo(()=>{
  return {}
},[])

useCallback

用来缓存函数的引用,避免组件重复创建函数、导致子组件不必要的重新渲染。

useCallback 的作用和 useMemo 的第二点作用(缓存依赖对象,防止子组件无意义更新)一致,主要区别是:函数用useCallback,对象和数组用 useMemo。

Ref


// 获取dom
const domRef = useRef(null)
​
<div ref={domRef}>123</div>

/*
    获取组件实例
    React.forwardRef
    useImperativeHandle
*/// 父组件:
const sonRef = useRef(null)
<Son ref={sonRef}></Son>
​
// 子组件
const Son = React.forwardRef(function Son(props,ref){
    const [num,setNum] = useState(0)
    useImperativeHandle(ref,()=>{
        return {
            num
        }
    })
    return (
      <div>son:{num}</div>
    )
})

合成事件

React中合成事件的处理原理:

React中的合成事件,是基于“事件委托”处理的,并不是给当前元素基于addEventListener单独做的事件绑定
>在React17及以后的版本,都是委托给#root这个容器【捕获和冒泡都做了委托】
>在17版本之前,都是委托给document容器的【而且只做了冒泡阶段的委托】
>对于没有实现事件传播机制的事件,才是单独做的事件绑定【例如:onMouseEnter/onMouseLeave】

在组件渲染的时候,如果发现JSX元素属性中有 onXxx/OnXxxCapture 这样的属性,不会给当前元素直接做事件绑定,只是把绑定的方法赋值给元素的相关属性。

对 #root 这个容器做了事件绑定【捕获和冒泡都做了】
>组件中所渲染的内容,最后都会插入到 #root 容器中,这样点击页面中任何一个元素,最后都会把 #root 的点击行为触发;
>而在给 #root 绑定的方法中,把之前给元素设置的 onXxx/onXxxCapture 属性,按顺序执行!

import React,{ useCallback, useMemo, useReducer, useRef, useState,useContext, useEffect } from 'react';
​
// 爷爷的父亲
const Demo = function Demo(){
    const yeyeCapture = ()=>{
        console.log('爷爷捕获');
    }
    const yeye = ()=>{
        console.log('爷爷冒泡');
    }
​
    const babaCapture = ()=>{
        console.log('爸爸捕获');
    }
    const baba = ()=>{
        console.log('爸爸冒泡');
    }
​
    const erziCapture = ()=>{
        console.log('儿子捕获');
    }
    const erzi = ()=>{
        console.log('儿子冒泡');
    }
​
    useEffect(()=>{
        document.querySelector('#yeye').addEventListener('click',()=>{
            console.log('爷爷捕获-原生');
        },true)
        document.querySelector('#yeye').addEventListener('click',()=>{
            console.log('爷爷冒泡-原生');
        })
​
        document.querySelector('#baba').addEventListener('click',()=>{
            console.log('爸爸捕获-原生');
        },true)
        document.querySelector('#baba').addEventListener('click',()=>{
            console.log('爸爸冒泡-原生');
        })
​
        document.querySelector('#erzi').addEventListener('click',()=>{
            console.log('儿子捕获-原生');
        },true)
        document.querySelector('#erzi').addEventListener('click',()=>{
            console.log('儿子冒泡-原生');
        })
​
    },[])
​
    return (
        <div>
            <div onClickCapture={yeyeCapture} onClick={yeye} id="yeye" style={{width:'300px',height:'300px',backgroundColor:'#c6c6ed'}}>
                <div onClickCapture={babaCapture} onClick={baba} id="baba" style={{width:'200px',height:'200px',backgroundColor:'rgb(238 196 223)'}}>
                    <div onClickCapture={erziCapture} onClick={erzi} id="erzi" style={{width:'100px',height:'100px',backgroundColor:'rgb(196 238 230)'}}></div>
                </div>
            </div>
        </div>
    )
}
​
export default Demo
react18结果:

爷爷捕获
爸爸捕获
儿子捕获
爷爷捕获-原生
爸爸捕获-原生
儿子捕获-原生
儿子冒泡-原生
爸爸冒泡-原生
爷爷冒泡-原生
儿子冒泡
爸爸冒泡
爷爷冒泡
react16结果:

爷爷捕获-原生
爸爸捕获-原生
儿子捕获-原生
儿子冒泡-原生
爸爸冒泡-原生
爷爷冒泡-原生
爷爷捕获
爸爸捕获
儿子捕获
儿子冒泡
爸爸冒泡
爷爷冒泡

react18

委托给 #root, 捕获和冒泡都做了委托

react 16

委托给 document, 只做了冒泡阶段的委托

组件通讯

父子通讯

1.父组件内容传给子组件使用。 props


import PropTypes from "prop-types";
​
const Son = function Son({
    name,
    age=0 // 设置默认值
}){
    return (
        <div>
            <div>name-{name}</div>
            <div>name-{age}</div>
        </div>
    )
}
​
Son.propTypes = {
    name: PropTypes.string.isRequired, // 设置类型和必填等
    age: PropTypes.number
}

2.子组件传递内容给父组件使用。 props


//父组件
const Father = function Father(){
​
    const getSonName = (info)=>{
        console.log(info);
    }
​
    return (
        <div>
            <Son sendMyData={getSonName}></Son>
        </div>
    )
}
​
// 子组件
const Son = function Son({
    sendMyData
}){
    const info = {
        name:'wyz',
        age:20
    }
    
    return (
        <div>
            <div><button onClick={()=>sendMyData(info)}>传递</button></div>
        </div>
    )
}

祖先后代通讯

基于上下文方案。useContext 。类似vue的 provide/ inject


// 祖先组件
const Context = React.createContext()
const Demo = function Demo(){
    const data = {}
    return (
        // 数据传到value中
        <Context.Provider value={data}>
        </Context.Provider>
    )
}
​
// 后代组件
const Son = function Son(){
    const {data} = useContext(Context)
    return (
        <div></div>
    )
}

任意组件间通信

基于库 mitt


// src/eventBus.js
import mitt from "mitt";
const emitter = mitt();
export default emitter;
​
import bus from './eventBus'// 传数据
bus.emit('erziA:data',data)
​
// 注册事件监听器
bus.on('erziA:data',handle)
​
// 移除某个事件的指定监听器。
bus.off('erziA:data',handle)
// 移除某个事件的所有监听器。
bus.off('erziA:data')
// 移除所有事件的所有监听器。
bus.all.clear()
​
// 查看当前所有已绑定的事件及其对应的监听器。
bus.all

插槽

react没有插槽这个概念,但是可以使用某些写法实现这个功能

默认插槽


// 父组件
<Son>
    <div>内容</div>
</Son>
​
// 子组件
const Son = function Son({children}){
    return (
        <div>{children || '插槽默认内容'}</div>
    )
}

具名插槽

1.简单写法,通过 props 直接传dom内容


//父组件
<Son
  header={<div>我是头部</div>}
  footer={<div>我是底部</div>}
>
  <div>我是主体内容</div>
</Son>
​
// 子组件
const Son = function Son({header,footer,children}){
    return (
        <div>
            {/* 头部插槽 */}
            {header}
            {/* 主体内容插槽 */}
            {children}
            {/* 底部插槽 */}
            {footer}
        </div>
    )
}

2.复杂写法,组合式组件模式


// 父组件
<Son>
  <div>我是主体内容</div>
  <Son.Header>我是头部</Son.Header>
  <Son.Footer>我是底部</Son.Footer>
</Son>
​
// 子组件
const Son = function Son({children}){
    const headerSlot = [],footerSlot = [],defaultSlot = [];
    React.Children.forEach(children,(child)=>{
        switch (child.type) {
            case Son.Header:
                headerSlot.push(child)
                break;
            case Son.Footer:
                footerSlot.push(child)
                break;
            default:
                defaultSlot.push(child)
                break;
        }
    })
    return (
        <div className="box">
            {/* 头部插槽 */}
            {headerSlot}
            {/* 主体内容插槽 */}
            {defaultSlot}
            {/* 底部插槽 */}
            {footerSlot}
        </div>
    )
}
Son.Header = ({children})=>(<div className="header-box">{children}</div>)
Son.Footer = ({children})=>(<div className="footer-box">{children}</div>)

作用域插槽

1.默认插槽


//  子组件
const Son = function Son({children}){
    const colorList = ['red','blue','yellow']
​
    return (
        <div>{typeof children === 'function' ? children(colorList) : children}</div>
    )
}
​
// 父组件
const Demo = function Demo(){
​
    return (
        <Son>
            {(colorList)=>{
                return colorList.map((item,index)=>{
                    return <div key={index} style={{color:item}}>{item}</div>
                })
            }}
        </Son>
    )
}

2.具名插槽,简单写法


// 子组件
const Son = function Son({header,footer,children}){
    const headerContent = {
        'A':'头部内容A',
        'B':'头部内容B',
        'C':'头部内容C',
    }
    const mainContent = {
        'A':'主体内容A',
        'B':'主体内容B',
        'C':'主体内容C',
    }
    const footerContent = {
        'A':'底部内容A',
        'B':'底部内容B',
        'C':'底部内容C',
    }
​
    return (
        <div>
            {typeof header === 'function' ? header(headerContent) : header}
            {typeof children === 'function' ? children(mainContent) : children}
            {typeof footer === 'function' ? footer(footerContent) : footer}
        </div>
    )
}
​
// 父组件
const Demo = function Demo(){
​
    return (
        <Son
            header={(headerContent)=><div>{headerContent['A']}</div>}
            footer={(footerContent)=><div>{footerContent['C']}</div>}
        >
            {(mainContent)=><div>{mainContent['B']}</div>}
        </Son>
    )
}

3.具名插槽,组合式组件模式


import React,{} from "react";
​
const Son = function Son({children}){
    const headerSlot = [],footerSlot = [],defaultSlot = [];
    const slots = Array.isArray(children) ? children : [children];
    slots.forEach(item=>{
        switch (item.type) {
            case Son.Header:
                headerSlot.push(item)
                break;
            case Son.Footer:
                footerSlot.push(item)
                break;
            default:
                defaultSlot.push(item)
                break;
        }
    })
    
    const headerContent = {
        'A':'头部内容A',
        'B':'头部内容B',
        'C':'头部内容C',
    }
    const mainContent = {
        'A':'主体内容A',
        'B':'主体内容B',
        'C':'主体内容C',
    }
    const footerContent = {
        'A':'底部内容A',
        'B':'底部内容B',
        'C':'底部内容C',
    }
​
    return (
        <div>
            {/* 头部 */}
            {headerSlot.map((item, index) => React.cloneElement(item, {key:index, headerContent}))}
            {/* 主体 */}
            {defaultSlot.map((item,index)=>{
                return typeof item == 'function' ? <React.Fragment key={index}>{item(mainContent)}</React.Fragment> : <React.Fragment key={index}>{item}</React.Fragment>;
            })}
            {/* 底部 */}
            {footerSlot.map((item, index) => React.cloneElement(item, {key:index, footerContent}))}
        </div>
    )
}
Son.Header = ({headerContent,children})=>{
    return typeof children == 'function' ? children(headerContent) : children;
}
Son.Footer = ({footerContent,children})=>{
    return typeof children == 'function' ? children(footerContent) : children;
}
​
const Demo = function Demo(){
​
    return (
        <Son>
            <Son.Header>{(headerContent)=><div>{headerContent['A']}</div>}</Son.Header>
            <Son.Footer>{(footerContent)=><div>{footerContent['A']}</div>}</Son.Footer>
            {(mainContent)=><div>{mainContent['A']}</div>}
            <Son.Header>{(headerContent)=><div>{headerContent['B']}</div>}</Son.Header>
            <Son.Footer>{(footerContent)=><div>{footerContent['B']}</div>}</Son.Footer>
            {(mainContent)=><div>{mainContent['B']}</div>}
            <Son.Header>头部</Son.Header>
            <Son.Footer>底部</Son.Footer>
            <div>主体</div>
        </Son>
    )
}
​
export default Demo

Redux

工程化写法

文件结构如下

src/
  store/
    actions/
      auth.js
      user.js
    reducers/
      auth.js
      index.js
    action-types.js
    index.js

各文件内容

/store/index.js


import { createStore,applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension';
import reduxLogger from 'redux-logger'
import { thunk } from 'redux-thunk';
​
import reducer from './reducers'const store = createStore(
    reducer,
    composeWithDevTools(applyMiddleware(reduxLogger,thunk))
)
​
export default store

/store/action-types.js


// 登陆 相关
export const LOGIN_REQUEST = 'LOGIN_REQUEST'
export const LOGIN_SUCCEED = 'LOGIN_SUCCEED'
export const LOGIN_ERROR = 'LOGIN_ERROR'
export const LOGOUT = 'LOGOUT'

/store/reducers/auth.js


import { cloneDeep } from 'lodash-es'
import { LOGIN_REQUEST, LOGIN_SUCCEED, LOGIN_ERROR, LOGOUT } from '../actios-types'// 登陆相关
const initial = {
    token: localStorage.getItem('token') || '',
    user: JSON.parse(localStorage.getItem('user') || null),
    loading: false,
    error: ''
}
​
const authReducer = function(state=initial,action){
    state = cloneDeep(state)
​
    const { type, payload } = action
​
    switch ( type ) {
        // 发起登陆
        case LOGIN_REQUEST:
            state.loading = true
            state.error = ''
            break;
​
        // 登陆成功
        case LOGIN_SUCCEED:
            state.token = payload?.token || ''
            state.user = payload?.user || null
            state.loading = false
            state.error = ''
            break;
​
        // 登陆失败
        case LOGIN_ERROR:
            state.loading = false
            state.error = payload?.error || ''
            break;
​
        // 退出登陆
        case LOGOUT:
            state.token = ''
            state.user = null
            state.loading = false
            state.error = ''
            break;
    }
​
    return state
}
​
export default authReducer

/store/reducers/index.js


import { combineReducers } from 'redux'import authReducer from './auth'const reducer = combineReducers({
    auth: authReducer
})
​
export default reducer

/store/action/auth.js


// 登陆 相关
import { LOGIN_REQUEST, LOGIN_SUCCEED, LOGIN_ERROR, LOGOUT } from '../actios-types'
import { loginApi, userApi } from '../../api'const authAction = {
    // 登陆 - 使用了 redux-thunk 中间件的写法
    login({userName,password}){
        return async (dispatch)=>{
            // 派发:登陆开始
            dispatch({ type:LOGIN_REQUEST })
​
            try {
                // 登陆请求
                const res = await loginApi({userName,password})
                if(res.code == 200){
                    // 获取当前登陆信息请求
                    const userRes = await userApi()
                    if(userRes.code == 200){
                        // 保存token和用户信息
                        localStorage.setItem('token', res.data.token)
                        localStorage.setItem('user', JSON.stringify(userRes.data.user))
                        // 派发:登陆成功
                        dispatch({ 
                            type: LOGIN_SUCCEED, 
                            payload: { 
                                token: res.data.token,
                                user: userRes.data.user
                            } 
                        })
                    }else{
                        // 派发:登陆失败
                        dispatch({ 
                            type: LOGIN_ERROR, 
                            payload: { error: userRes.msg || '登陆失败' } 
                        })
                    }
                }else{
                    // 派发:登陆失败
                    dispatch({ 
                        type: LOGIN_ERROR, 
                        payload: { error: res.msg || '登陆失败' } 
                    })
                }
            } catch (error) {
                // 派发:登陆失败
                dispatch({ 
                    type: LOGIN_ERROR, 
                    payload: { error:'登陆失败' } 
                })
            }
        } 
    },
​
    // 退出
    logout(){
        // 清楚token和用户信息
        localStorage.removeItem('token')
        localStorage.removeItem('user')
        // 派发
        return { type: LOGOUT }
    }
}
​
export default authAction

/store/action/index.js


import authAction from "./auth";
​
const action = {
    auth: authAction
}
​
export default action

组件中使用

1.根组件中构建上下文


import { Provider } from 'react-redux'
import store from "./store";
​
const Demo = function Demo(){
    return (
       <Provider store={store}>
            <Login></Login>
       </Provider>
    )
}

2.后代组件中使用


const Login = function Login({login,logout}){
​
    const [userName,setUserName] = useState('')
    const [password,setPassword] = useState('')
​
    const handleInput = (field,e)=>{
        let value = e.target.value
        switch (field) {
            case 'userName':
                setUserName(value)
                break;
            case 'password':
                setPassword(value)
                break;
        }
    }
​
    const handleLogin = ()=>{
        login({userName,password})
    }
​
    const handleLogout = ()=>{
        logout()
    }
    
    return (
        <div>
            <div>登陆</div>
            <div>用户名 <input type="text" value={userName} onInput={e=>handleInput('userName',e)}/> </div>
            <div>密码 <input type="text" value={password} onInput={e=>handleInput('password',e)}/></div>
            <div><button onClick={handleLogin}>登录</button> <button onClick={handleLogout}>退出</button></div>
        </div>
    )
}
​
export default connect(
    state=>state.auth,
    action.auth
)(Login)

redux-toolkit写法

文件结构如下

src/
  store/
    features/
      auth.js
    index.js

各文件内容

/store/index.js

import { configureStore } from '@reduxjs/toolkit'
import reduxLogger from 'redux-logger'
import authSliceReducer from './features/auth.js'const store = configureStore({
    // 指定reducer
    reducer:{
        // 按模块管理各个切片
        auth: authSliceReducer
    },
    // 使用中间件
    middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(reduxLogger)
})
​
export default store

/store/features/auth.js

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { loginApi, userApi } from '../../api'const authSlice = createSlice({
    // 切片名称
    name:'auth',
    // 此切片对应reducer中的初始状态
    initialState:{
        token: localStorage.getItem('token') || '',
        user: JSON.parse(localStorage.getItem('user') || null),
        loading: false,
        error: ''
    },
    // 编写不同业务逻辑下,对公共状态的修改
    reducers:{
        
    },
    // 处理 slice 外部的 action(最典型就是 createAsyncThunk 的三态 action)。
    extraReducers:(builder) => {
        builder
            // 发起登陆
            .addCase(loginAsync.pending,(state)=>{
                state.loading = true
                state.error = ''
            })
            // 登陆成功
            .addCase(loginAsync.fulfilled,(state,action)=>{
                state.token = action.payload?.token || ''
                state.user = action.payload?.user || null
                state.loading = false
                state.error = ''
            })
            // 登陆失败
            .addCase(loginAsync.rejected,(state,action)=>{
                state.loading = false
                state.error = action.payload?.error || ''
            })
            // 退出登陆
            .addCase(logoutAsync.fulfilled,(state)=>{
                state.token = ''
                state.user = null
                state.loading = false
                state.error = ''
            })
    }
})
​
// 登陆
export const loginAsync = createAsyncThunk('auth/login',async({userName,password},{ rejectWithValue })=>{
    try {
        // 登陆请求
        const res = await loginApi({userName,password})
        // 派发:登陆失败
        if(res.code != 200) return rejectWithValue({ error: res.msg || '登陆失败' })
        // 获取当前登陆信息请求
        const userRes = await userApi()
        // 派发:登陆失败
        if(userRes.code != 200) return rejectWithValue({ error: userRes.msg || '登陆失败' })
        // 保存token和用户信息
        localStorage.setItem('token', res.data.token)
        localStorage.setItem('user', JSON.stringify(userRes.data.user))
        // 派发:登陆成功
        return { 
            token: res.data.token,
            user: userRes.data.user
        }
    } catch (error) {
        // 派发:登陆失败
        return rejectWithValue({ error: '登陆失败' })
    }
})
​
// 退出登陆
export const logoutAsync = createAsyncThunk('auth/logout',()=>{
    // 清楚token和用户信息
    localStorage.removeItem('token')
    localStorage.removeItem('user')
})
​
export default authSlice.reducer

组件中使用

根组件


import { Provider } from 'react-redux'
import store from "./store";
​
const Demo = function Demo(){
    return (
       <Provider store={store}>
            <Login></Login>
       </Provider>
    )
}

业务组件


import { useSelector,useDispatch } from 'react-redux'
import { loginAsync, logoutAsync } from './store2/features/auth.js'const Login = function Login({login,logout}){
​
    const { token } = useSelector(state=>state.auth)
    const dispatch = useDispatch()
​
    const [userName,setUserName] = useState('')
    const [password,setPassword] = useState('')
​
    const handleInput = (field,e)=>{
        let value = e.target.value
        switch (field) {
            case 'userName':
                setUserName(value)
                break;
            case 'password':
                setPassword(value)
                break;
        }
    }
​
    const handleLogin = ()=>{
        dispatch(loginAsync({userName,password}))
    }
​
    const handleLogout = ()=>{
        dispatch(logoutAsync({userName,password}))
    }
    
    return (
        <div>
            <div>登陆</div>
            <div>用户名 <input type="text" value={userName} onInput={e=>handleInput('userName',e)}/> </div>
            <div>密码 <input type="text" value={password} onInput={e=>handleInput('password',e)}/></div>
            <div><button onClick={handleLogin}>登录</button> <button onClick={handleLogout}>退出</button></div>
        </div>
    )
}
​
export default Demo

router


yarn add react-router-dom@5.3.4

router5

功能组件

HashRouter


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

Route


- <Route> 路由 
  + path 路由地址。值为*或者不写,表示所有规则都匹配
  + component 路由匹配时,需要渲染的组件
  + exact 开启精准匹配,路由默认是模糊匹配的
  + render 当路由匹配后,先把 render 函数执行, 返回的返回值就是我们需要渲染的内容
模糊匹配:
<Route path="/" component={Home}></Route>
<Route path="/login" component={Login}></Route>
访问:/login,两个路由的都会匹配上。

开启精准匹配:
<Route path="/" component={Home}></Route>
<Route path="/login" component={Login}></Route>
访问:/login,只会匹配到 /login。

Switch


<Switch> 确保路由中,只要有一项匹配,则不再继续向下匹配
模糊匹配:
<Switch>
	<Route path="/" component={Home}></Route>
	<Route path="/login" component={Login}></Route>
</Switch>
访问:/,会匹配到路由/,匹配成功后不会再往下匹配。
访问:/login,也会匹配到路由/,匹配成功后不会再往下匹配,故不会匹配到/login。

开启精准匹配:
<Switch>
	<Route path="/" exact component={Home}></Route>
	<Route path="/login" exact component={Login}></Route>
</Switch>
访问:/,会匹配到路由/,匹配成功后不会再往下匹配。
访问:/login,不会匹配到路由/,往下匹配,后续匹配到/login。

Redirect


- <Redirect> 重定向
  + from 从哪个地址来
  + to 重定向的地址
  + exact 对from地址的修饰,开启精准匹配

<Switch>
    {/* 访问/,重定向到 /home */}
    <Redirect from="/" exact to="/home"></Redirect>
    <Route path="/home" exact component={Home}></Route>
    <Route path="/login" exact component={Login}></Route>
    {/* 以上都不匹配,重定向到/home */}
    <Redirect to="/home"></Redirect>
</Switch>

Link/NavLink


 - <Link> 实现路由切换/跳转的组件
  + 最后渲染完毕的结果依然是A标签
  + 它可以根据路由模式,自动设定点击A切换的方式
​
<NavLink><Link> 都是实现路由跳转的,语法上几乎一致,区别是:每一次页面加载或者路由切换完毕,都会拿最新的路由地址,和NavLink中to的指定地址(或者pathname地址)进行匹配
  + 匹配上的这一样,会默认设置 active 选中样式类【可通过 activeClassName属性 自定义类名】
  + 可加 exact 开启精准匹配
基于这样的机制,能实现给选中的导航设置选中样式!

编程式路由

获取路由对象信息方法

+ 所有组件都需要包裹在<HashRouter>中,只有这样才能在每个组件中,获取 history / location / match 等对象信息

+ 函数组件,并且是基于<Route>匹配渲染的
  > 基于 props 属性获取(注:基于rander渲染的,需要自己处理一下)
  > 基于 useHistory, useLocation, useRouteMatch Hooks函数去获取
  
+ 函数组件,但并不是基于<Route>匹配渲染的
  > 基于 Hooks 函数获取
  > 基于 withRouter 代理该组件,即可基于 props 获取

1.history


+ history 控制路由的跳转(前进、后退、push 新路径等)。
  > push(path)     跳转到新的地址(历史栈中添加新记录)
  > replace(path)  替换当前地址(不添加历史记录)
  > go(n)          跳转到历史记录中相对位置(如 go(-1) 回退)
  > goBack()       相当于 go(-1)
  > goForward()    相当于 go(1)
  > location       当前的 location 对象
  > listen()       监听路由变化

2.location


+ location 表示当前的地址信息,类似于浏览器中的 window.location。
  > pathname   当前路径(如 /user/123)
  > search     URL 中的查询字符串(如 ?id=10)
  > hash       URL 中的 hash 值(如 #section1)
  > state      使用 history.push 或 replace 时传入的 state
  > key        唯一标识(每次跳转生成的)

3.match

+ match
  > params	  路由参数对象(如 /user/:id 中的 id
  > isExact	  是否完全匹配路径
  > path	  <Route path=""> 中定义的路径
  > url	      当前匹配的 url 部分(可用于生成嵌套路由)

路由传参

1.问号传参

> 传递的信息出现在url地址上:丑、不安全、长度限制
> 信息是显式的,即使在目标路由内刷新,传递的信息也在
> 传递方式:
history.push({
	pathname:'/c',
	search:qs.stringify({
		id:100,
		name:'吴彦祖'
	})
})
> 接收方式:
	const location = useLocation()
	qs.parse(location.search.substring(1))

2.路径传参

路径参数[把需要传递的值,作为路由路径中的一部分]
> 传递的信息也在url路径中,故也存在 丑、不安全、长度限制 等问题
> 因为信息在地址中,即便在目标组件刷新,传递的信息也在
> 路由定义
	<Route path="/d/:id?/:name?" component={}></Route>
> 传递方式:
	history.push(`/d/101/陈冠希`)
> 接收方式:
	const routeMatch = useRouteMatch() 
	routeMatch.params

3.隐式传参

> 传递的信息不会出现在url地址中:安全、美观、也没有信息
> 在目标组件内刷新,传递的信息会丢失
> 传递方式:
	history.push({
		pathname:'/e',
		state:{
			id:102,
			name:'彭于晏'
		}
})
> 接收方式:
	const location = useLocation()
	location.state

路由懒加载


处理方案:
​
我们只要把最开始的内容/组件打包到“主JS”中【bundle.js】,其余的组件,打包成独立的js【或者几个组件合并在一起打包】
​
当页面加载的时候,首先只要把“主JS”【bundle.js】请求回来渲染,其余的js先不加载。因为【bundle.js】中只有最开始要渲染的组件的代码,所有体积潇,获取和渲染速度快,可以减少白屏等待的时间。其余的JS此时并没有加载,也不影响页面的第一次渲染。
​
当我切换路由的时候,和某个规则匹配,想要渲染哪个组件,再把这个组件所在的JS文件,动态导入进来进行渲染即可。
用到的API:
+ lazy - 实现懒加载
+ Suspense - 实现懒加载
+ 注释包裹 webpackChunkName:"js文件名称" - 可以实现将几个模块打包到一起

// app.jsxconst Login = withLazyLoad(()=>import('./views/Login/index')) // 路由懒加载写法
​
<Switch>
    <Redirect from="/" to="/home" exact></Redirect>
    <Route path="/home" component={Home}></Route>
    <Route path="/login" render={Login}></Route>
    <Redirect to="/home"></Redirect>
</Switch>

// home.jsx// 实现懒加载,并打包到一起
const Users = withLazyLoad(()=>import(/* webpackChunkName:"HomeOther" */'../Users/index')) 
const Tabs = withLazyLoad(()=>import(/* webpackChunkName:"HomeOther" */'../Tabs/index')) 
​
<Switch>
    <Redirect from="/home" to="/home/news" exact></Redirect>
    <Route path="/home/news" component={News}></Route>
    <Route path="/home/users" component={Users}></Route>
    <Route path="/home/tabs" component={Tabs}></Route>
    <Redirect to="/home/news"></Redirect>
</Switch>

// withLazyLoad.js// 路由懒加载 HOC
import React, { lazy, Suspense } from 'react';
​
export default function withLazyLoad(importFunc, fallback = <>加载中...</>){
    const LazyComponent = lazy(importFunc);
    return function Hoc(props){
        return (
            <Suspense fallback={fallback}>
                <LazyComponent {...props}></LazyComponent>
            </Suspense>
        )
    }
}

路由表统一管理

文件定义

src
 - router
    - routes
	   - app.js
	   - home.js
	- index.js

// router/index.js/*
    配置路由表:数组,数组中每一项就是每一个需要配置的路由规则
        + redirect 是否为从定向
        + from 来源的地址
        + to 重定向的地址
        + exact 是否精准匹配
        + path 匹配的路径
        + component 渲染的组件
        + render 路由匹配时执行的方法
        + name 路由名称(命名路由)
        + meta:{} 路由元信息
*/
import React, { Suspense } from 'react';
import { Switch, Route, Redirect } from 'react-router-dom'// 导出路由表
export { default as appRoutes } from './routes/app';
export { default as homeRoutes } from './routes/home';
​
// 导出路由封装
export const RouterView = function RouterView({routes}){
    return (
        <Switch>
            {/* 循环设置路由匹配规则 */}
            {
                routes.map((route,index)=>{
                    const { redirect,from,to,exact,path,component:Component,name,meta,render } = route
                    const config = {}
                    // 重定向
                    if(redirect){ 
                        if(to) config.to = to
                        if(from) config.from = from
                        if(exact) config.exact = exact
                        return <Redirect key={index} {...config}></Redirect>
                    }
                    // 正常匹配规则
                    if(path) config.path = path
                    if(name) config.name = name
                    if(meta) config.meta = meta
                    return <Route key={index} {...config} render={(props)=>{
                        // 当某个路由匹配时,在这可以统一进行一些其它事,类型vue的全局路由守卫
​
                        if(render) return render(props)
​
                        return(
                            // 实现路由懒加载,需要用 Suspense 包裹着实际需要渲染的组件,因为懒加载组件是异步操作,不然执行到这里的时候,Component 还不是一个组件
                            // fallback 在异步加载的组件没有处理完成之前,先展示的Loading效果。
                            <Suspense fallback={<>正在处理中...</>}> 
                                <Component {...props}></Component> 
                            </Suspense>
                        )
                    }}></Route>
                })
            }
        </Switch>
    )
}
​

// router/toutes/app.jsimport { lazy } from 'react'import Home from '../../views/Home/index'const routes = [
    {
        redirect: true,
        from: '/',
        to: '/home',
        exact: true
    },
    {
        path: '/home',
        component: Home,
    },
    {
        path: '/login',
        component: lazy(()=>import('../../views/Login/index')), // 实现路由懒加载
    },
]
​
export default routes

// router/toutes/home.jsimport { lazy } from 'react'import News from '../../views/News/index'const routes = [
    {
        redirect: true,
        from: '/home',
        to: '/home/news',
        exact: true
    },
    {
        path: '/home/news',
        component: News,
    },
    {
        path: '/home/users',
        component: lazy(/* webpackChunkName:"HomeOther" */()=>import('../../views/Users/index')), // 实现懒加载,并打包到一起
    },
    {
        path: '/home/tabs',
        component: lazy(/* webpackChunkName:"HomeOther" */()=>import('../../views/Tabs/index')), // 实现懒加载,并打包到一起
    },
]
​
export default routes

组件中使用


import { RouterView, appRoutes } from './router/index'
​
<RouterView routes={appRoutes}></RouterView>

router6

功能组件

HashRouter


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

Link/NavLink


// 和 v5 的一致

Routes

所有的路由匹配规则,都要放在在<Routes>

Route

- <Route> 路由 
  + path 路由地址。值为*或者不写,表示所有规则都匹配
  + element 控制渲染的组件
​
和 v5 的区别:
不再需要 Switch , 默认就是一个匹配成功,就不再匹配下面的了.
不再需要 exact , 默认每一项匹配都是精准匹配

Navigate

实现重定向功能。
遇到 <Navigate to=""> 组件,路由就会跳转到 to 指定的路由地址。
设置 replace 属性,则不会新增历史记录,而是代替现有的记录。

Outlet

<Outlet> 是“子路由的占位符”。

编程式路由

useNavigate()

// 用于 编程式导航(跳转路由),是 v5 history.push() 的替代方案。// ✅ 跳转新页面
navigate('/home');
​
// ✅ 替换当前历史记录(不留返回记录)
navigate('/login', { replace: true });
​
// ✅ 后退一步
navigate(-1);
​
// ✅ 前进一步
navigate(1);

useLocation()

+ location 表示当前的地址信息,类似于浏览器中的 window.location。
  > pathname   当前路径(如 /user/123)
  > search     URL 中的查询字符串(如 ?id=10)
  > hash       URL 中的 hash 值(如 #section1)
  > state      使用 history.push 或 replace 时传入的 state
  > key        唯一标识(每次跳转生成的)

useParams()


// 用于获取 动态路由参数(路径参数)。
​
<Route path="/user/:id" element={<User />} />
​
const params = useParams();
console.log(params); // { id: "123" }

useSearchParams()

// 用于 获取和设置 查询参数(问号传参的参数)。
​
const [searchParams, setSearchParams] = useSearchParams();
​
// 返回一个数组:
searchParams:一个 URLSearchParams 实例(只读)
setSearchParams:一个函数,用来修改查询参数(可写)

路由传参

问号传参


// 传参
const navigate = useNavigate()
navigate({
    pathname:'/home/users',
    search: qs.stringify({
        id: 1
    })
})
​
// 接收
// 方式一
const location = useLocation()
const params = qs.parse(location.search.substring(1))
// 方式二
const [searchParams] = useSearchParams();
searchParams.get('id')

路径传参


// 路由定义
<Route path='/home/users/:id?' element={<Users></Users>}></Route>
​
// 传参
const navigate = useNavigate()
navigate('/home/users/1')
​
// 接收
const params = useParams()

隐式传参


// 注意:刷新页面会丢失// 传参
const navigate = useNavigate()
navigate('/home/users',{
    state:{
      id:1
    }
})
​
// 接收
const location = useLocation()
location.state

路由懒加载

用到的API:
+ lazy - 实现懒加载
+ Suspense - 实现懒加载
+ 注释包裹 webpackChunkName:"js文件名称" - 可以实现将几个模块打包到一起

const Login = withLazyLoad(()=>import('./views/Login/index'))
const Users = withLazyLoad(()=>import(/* webpackChunkName:"HomeOther" */'./views/Users/index'))
const Tabs = withLazyLoad(()=>import(/* webpackChunkName:"HomeOther" */'./views/Tabs/index'))
​
<HashRouter>
  <Routes>
    <Route path='/' element={<Navigate to="/home"></Navigate>}></Route>
    <Route path='/home' element={<Home></Home>}>
      <Route path='/home' element={<Navigate to="/home/news"></Navigate>}></Route>
      <Route path='/home/news' element={<News></News>}></Route>
      <Route path='/home/users' element={<Users></Users>}></Route>
      <Route path='/home/tabs' element={<Tabs></Tabs>}></Route>
    </Route>
    <Route path='/login' element={<Login></Login>}></Route>
    <Route path='*' element={<Navigate to="/home"></Navigate>}></Route>
  </Routes>
</HashRouter>

import React, { lazy, Suspense } from "react";
​
export default function withLazyLoad(importFunc, fallback = <>加载中...</>){
    const LazyComponent = lazy(importFunc)
​
    return function HOC(props){
        return (
            <Suspense fallback={fallback}> 
                <LazyComponent {...props}></LazyComponent>
            </Suspense>
        )
    }
}

路由表统一管理

文件定义

src
 - router
    - routes.js
	- index.js
// router/index.jsimport { Suspense } from "react";
import { Routes,Route } from 'react-router-dom'
import { useNavigate,useLocation,useSearchParams,useParams } from 'react-router-dom'
import routes from "./routes";
​
/* 统一渲染的组件(类似 全局前置路由守卫 ):在这里可以做[权限/登录态校验,传递路由信息属性...] */
const Element = function Element(props){
    const { component:Component } = props
    // 传递路由信息属性
    const navigate = useNavigate()
    const location = useLocation()
    const [searchParams] = useSearchParams()
    const params = useParams()
​
    return (
        <Suspense fallback={<div>加载中...</div>}>
            <Component {...{navigate,location,searchParams,params}}></Component>
        </Suspense>
    )
}
​
/* 递归创建Route */
const createRoute = function createRoute(routes){
    return (
        <>
            {routes.map((item,index)=>{
                const { children } = item
                return (
                    <Route key={index} path={item.path} element={<Element {...item}></Element>}>
                        {Array.isArray(children) ? createRoute(children) : null}
                    </Route>
                )
            })}
        </>
    )
}
​
/* 路由容器 */
export default function RouterView(){
    return (
        <Routes>
            {createRoute(routes)}
        </Routes>
    )
}
​
/* 创建 withRouter */
export const withRouter = function withRouter(Component){
    return function HOC(props){
        // 传递路由信息属性
        const navigate = useNavigate()
        const location = useLocation()
        const [searchParams] = useSearchParams()
        const params = useParams()
​
        return <Component {...props} {...{navigate,location,searchParams,params}}></Component>
    }
}
/*
    配置路由表:数组,数组中每一项就是每一个需要配置的路由规则
        + path 匹配的路径
        + component 渲染的组件
        + children:[] 子路由
*/import { lazy } from 'react'
import { Navigate } from 'react-router-dom'import Home from '../views/Home/index'
import News from '../views/News/index'const routes = [
    {
        path: '/',
        component: ()=><Navigate to="/home"></Navigate>
    },
    {
        path: '/home',
        component: Home,
        children: [
            {
                path: '/home',
                component: ()=><Navigate to="/home/news"></Navigate>
            },
            {
                path: '/home/news',
                component: News,
            },
            {
                path: '/home/users',
                component: lazy(()=>import(/* webpackChunkName:"HomeOther" */'../views/Users/index'))
            },
            {
                path: '/home/tabs',
                component: lazy(()=>import(/* webpackChunkName:"HomeOther" */'../views/Tabs/index'))
            },
        ]
    },
    {
        path: '/login',
        component: lazy(()=>import('../views/Login/index'))
    },
    {
        path: '*',
        component: ()=><Navigate to="/home"></Navigate>
    }
]
​
export default routes

组件中使用


import RouterView from './router/index'
​
<HashRouter>
    <RouterView></RouterView>
</HashRouter>