生命周期
类组件
各生命周期解析
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.jsx
const 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.js
import { 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.js
import { 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.js
import { 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>