jsx语法
JSX:javascript and xml(html)
-
最外层只能有一个根元素节点
-
<></>
fragment空标记,即能作为容器把一堆内容包裹起来,还不占层级结构 -
动态绑定数据使用{},大括号中存放的是JS表达式
- 可以直接放数组:把数组中的每一项都呈现出来
- 一般情况下不能直接渲染对象
- 但是如果是JSX的虚拟DOM对象,是直接可以渲染的
-
设置行内样式,必须是
style={{color:'red'...}}; -
设置样式类名需要使用的是
className; -
JSX中进行的判断一般都要基于三元运算符来完成
-
JSX中遍历数组中的每一项,动态绑定多个JSX元素,一般都是基于数组中的
map来实现的- 和vue一样,循环绑定的元素要设置
key值(作用:用于DOM-DIFF差异化对比)
- 和vue一样,循环绑定的元素要设置
-
JSX语法具备过滤效果(过滤非法内容),有效防止XSS攻击(扩展思考:总结常见的XSS攻击和预防方案?)
虚拟DOM
const virtualDOM = React.createElement(
React.Fragment,
null,
React.createElement('h1', { className: 'title', style: { color: 'red' } }, 'hello world'),
React.cloneElement('div', { className: 'box' }, React.createElement('span', null, text)),
)
console.log(virtualDOM)
// 创建虚拟dom
export function createElement(ele, props, ...children) {
let virtualDOM = {
$$typeof: Symbol.for('react.element'),
key: null,
ref: null,
type: null,
props: {},
}
let len = children.length
virtualDOM.type = ele
if (props !== null) {
virtualDOM.props = { ...props }
}
if (len === 1) virtualDOM.props.children = children[0]
if (len > 1) virtualDOM.props.children = children
return virtualDOM
}
console.log(createElement('h1', {className: 'title', style: {color: 'red'}}, 'hello world'))
真实DOM
/*
封装迭代对象的方法
--for...in 方法:性能差,既可以迭代私有属性,也可以迭代公有属性,一直找到Object原型链终点;只能迭代‘可枚举’,‘非Symbol类型’的属性
Object.getOwnPropertyNames 获取私有属性[无关是否可枚举]
Object.getOwnPropertySymbols(arr) 获取Symbol类型的私有属性
let keys = Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj))
也可以用ES6中的Reflect.ownKeys 获取所有的私有属性
let keys = Reflect.ownKeys(obj)
*/
Array.prototype.KEY = 'key'
let arr = [1, 2, 3]
arr[Symbol('source')] = 'source'
// 封装迭代对象的方法
export function iteratorObject(obj, callback) {
if (obj === null || typeof obj !== 'object') throw TypeError('obj must be an object')
if (typeof callback !== 'function') throw TypeError('callback must be a function')
let keys = Reflect.ownKeys(obj)
keys.forEach((key) => {
let value = obj[key]
// 每一次迭代回传callback
callback(value, key)
})
}
iteratorObject(arr,(value,key)=>{
console.log(value,key)
})
// 创建真实dom
export function render(virtualDOM, container) {
let { type, props } = virtualDOM
if (typeof type === 'string') {
// 创建元素
let el = document.createElement(type)
// 设置props
iteratorObject(props, (value, key) => {
// className处理
if (key === 'className') {
return (el.className = value)
}
//样式处理
if (key === 'style') {
return iteratorObject(value, (styleVal, styleKey) => {
el.style[styleKey] = styleVal
})
}
// 子节点处理
if (key === 'children') {
let children = value
if (!Array.isArray(children)) children = [children]
children.forEach((child) => {
// 子节点是文本节点
if (/^(string|number)$/.test(typeof child)) {
return el.appendChild(document.createTextNode(child))
}
// 子节点是新的虚拟dom-递归
render(child, el)
})
return
}
// 设置属性
el.setAttribute(key, value)
})
// 插入容器
container.appendChild(el)
}
}
let jsxObj = createElement('div', { className: 'container' }, createElement('span', null, text))
render(jsxObj, document.getElementById('root'))
组件化开发
函数式组件
function DemoOne(props) {
console.log(props)
return <div className="demo-box">我是demoOne组件</div>
}
root.render(
<Fragment>
<DemoOne title="标题" className="box" x={10} data={(1, 2, 3)}></DemoOne>
</Fragment>,
)
渲染机制
- 基于
babel-preset-react-app把调用的组件转换为createElement格式
React.createElement(
DemoOne,
{
title:"标题" ,
className:"box",
x:10,
data:[1,2,3]
}
)
- createElement方法执行创建虚拟dom对象
{
$$typeof:Symbol(react,element),
key:null,
props:{title:"标题",className:'box',x:10,data:[1,2,3]},
ref:null,
type:DemoOne
}
-
基于
root.render把虚拟dom转换成真实dom- 函数式组件,虚拟dom的
type值不在为元素标签字符串了,而是一个函数 - 此时会执行
DemoOne()函数 - 将虚拟dom中的
props作为实参传给函数--DemoOne(props) - 函数执行返回虚拟dom
- 最后基于
render函数把组件返回的虚拟dom变为真实dom,插入到容器中
- 函数式组件,虚拟dom的
props
调用组件,传进的props被冻结了,是只读的,组件内修改props会报错 作用:父组件调用子组件时,可以基于属性把信息传递给子组件,让子组件呈现不同的效果
关于对象的规则设置
-
冻结:被冻结的对象不能修改成员值,不能新增,删除成员,不能给成员做劫持(
Object.defineProperty)- 冻结对象
Object.freeze(obj) - 检测对象是否被冻结
Object.isFrozen(obj)
- 冻结对象
-
密封:被密封的对象不能新增,删除成员,但是可以修改成员值,也可以做劫持
- 密封对象:
Object.seal(obj) - 检测是否被密封:
Object.isSealed(obj)
- 密封对象:
-
不可拓展:不能新增成员
- 不可拓展对象:
Object.preventExtensions(obj) - 检测是否不可拓展:
Object.isExtensible(obj)
- 不可拓展对象:
props规则校验
当父组件没有传递某属性时,可以在子组件内设置静态属性默认值
function DemoOne(props) {
let { title,className, style } = props
return (
<div className={`demo-box ${className}`} style={style}>
我是demoOne组件
<h2 className="title">{title}</h2>
</div>
)
}
DemoOne.defaultProps = {
x:0
}
export default DemoOne
设置其他规则,例如:数据格式,是否必传...依赖于官方插件 prop-types
传递进来的属性,首先会经历校验,不管校验成功还是失败,都不影响获取props,但会报警告
DemoOne.propTypes = {
title: PropTypes.string.isRequired,
x: PropTypes.number,
className: PropTypes.string,
style: PropTypes.object,
data:PropTypes.oneOf([
PropTypes.number,
PropTypes.bool
])
}
props之children
当单闭合调用或没有传子节点,children为undefined
当传了子节点,children为一个对象或一个数组
插槽机制:父组件传递子节点,在子组件中可以使用{children}实现插槽
function DemoOne(props) {
let { title, className, style, children } = props
console.log(children)
return (
{children}
<div className={`demo-box ${className}`} style={style}>
我是demoOne组件
<h2 className="title">{title}</h2>
</div>
)
}
基于React.Children对props.children的几种情况做处理:count\forEach\map\toArray...
function DemoOne(props) {
let { title, className, style, children } = props
// 基于React.Children对props.children的几种情况做处理:count\forEach\map\toArray...
// 如果undefined 为空数组,如果只有一项则转为数组
children = React.Children.toArray(children)
console.log(children)
return (
<>
{children}
<div className={`demo-box ${className}`} style={style}>
我是demoOne组件
<h2 className="title">{title}</h2>
</div>
<hr />
</>
)
}
root.render(
<Fragment>
<DemoOne title="标题" className="box" x={10} data={[1, 2, 3]} style={styleObj}>
<span>单子节点</span>
</DemoOne>
<DemoOne title="哈哈">
<span>两个子节点</span>
<span>两个子节点</span>
</DemoOne>
<DemoOne />
</Fragment>,
)
具名插槽-调用组件时自定义属性名,在子组件中通过children的props属性进行条件渲染
function DemoOne(props) {
let { title, className, style, children } = props
// 基于React.Children对props.children的几种情况做处理:count\forEach\map\toArray...
// 如果undefined 为空数组,如果只有一项则转为数组
children = React.Children.toArray(children)
let headerSlot = [],
footerSlot = [],
defaultSlot = []
children.forEach((child) => {
let { slot } = child.props
if (slot === 'header') {
headerSlot.push(child)
} else if (slot === 'footer') {
footerSlot.push(child)
} else {
defaultSlot.push(child)
}
})
return (
<>
{headerSlot}
<div className={`demo-box ${className}`} style={style}>
我是demoOne组件
<h2 className="title">{title}</h2>
</div>
{footerSlot}
<hr />
</>
)
}
root.render(
<Fragment>
<DemoOne title="标题" className="box" x={10} data={[1, 2, 3]} style={styleObj}>
<span>单子节点</span>
</DemoOne>
<DemoOne title="哈哈">
<span slot='footer'>底部</span>
<span>hahaha</span>
<span slot='header'>头部</span>
</DemoOne>
<DemoOne />
</Fragment>,
)
类组件(动态组件)
函数组件又称之静态组件(改变值,视图不会刷新),使用类组件或使用hooks函数可以实现改变值视图刷新
render函数再渲染的时候,如果type是字符串,创建一个标签;如果是普通函数,把函数执行,并且把props传递给函数,构造函数(类):把构造函数基于new执行,也就是创建类的一个实例
创建类组件
- 必须继承
React.Component - 必须设置
render方法,返回jsx - 如果写了
constructor,则必须写super
调用类组件--new执行
- 先规则校验 && 再初始化属性 --会把传递进来的属性挂载到this实例上,即便不写
constructor,react内部也会自动执行,所以在组件内部其他函数中,只要this为实例,都可以直接使用this.props
// 规则校验
static defaultProps = {
title: '投票',
supNum: 10,
oppNum: 5,
}
static propTypes = {
title: PropTypes.string,
supNum: PropTypes.number,
oppNum: PropTypes.number,
}
// 初始化属性
constructor(props) {
super(props)
}
- 初始化状态(state状态改变-视图刷新),需要手动初始化,如果不设置,初始值为null ==>>
this.state = null
// 初始化状态
state = {
supNum: 10,
oppNum: 5,
}
// 渲染视图
render() {
let { title } = this.props
let { oppNum, supNum } = this.state
}
-
修改状态-更新视图
this.setState:修改状态this.forceUpdate():强制视图刷新
<button
onClick={() => {
this.setState({
supNum: supNum + 1,
})
}}>
支持
</button>
<button
onClick={() => {
this.state.oppNum++
this.forceUpdate()
}}>
反对
</button>
- 触发周期函数
componentWillMount(不推荐使用此钩子,将被移除),UNSAFE_componentWillMount可以去除警告,但在严格模式下会报错 - 触发
render函数 - 触发周期函数
componentDidMount
组件更新
组件内部状态更新
父组件第一次渲染:父willMount->父render->[子willMount->子render->子didMount]->父didMount
-
触发
shouldComponentUpdate钩子,是否允许更新,如果基于forceUpdate()强制更新,则跳过此钩子的校验 -
触发
UNSAFE_componentWillUpdate钩子,组件更新之前(这个阶段,属性和状态还未改变) -
修改状态
-
触发render组件更新
- 按照最新的状态/属性,把返回的jsx编译为
virtualDOM DOM-Diff和第一次渲染出来的virtualDOM进行比对- 把差异的部分进行渲染为真实dom
- 按照最新的状态/属性,把返回的jsx编译为
-
触发
componentDidUpdate钩子,组件更新完毕
父子组件执行顺序: 深度优先原则:父组件在操作中,遇到子组件,一定是把子组件处理完,父组件再继续处理
父组件更新触发子组件更新
父组件更新: 父shouldUpdate->父willUpdate->父render->[子willReceiveProps->子shouldUpdate->子willUpdate->子render-子didUpdate]->父didUpdate
- 触发
UNSAFE_componentWillReceiveProps - 触发
shouldComponentUpdate
UNSAFE_componentWillMount() {
console.log('componentWillMount--第一次渲染之前')
}
componentDidMount() {
console.log('componentDidMount--第一次渲染结束-可以获取真实dom了')
}
shouldComponentUpdate(nextProps, nextState) {
console.log('shouldComponentUpdate--组件状态更新')
// this,state存储的是旧状态, nextState存储修改的最新的状态
console.log(this.state, nextState)
// 此周期函数需要返回true或false来控制是否允许下一步操作, false不允许更新
return true
}
UNSAFE_componentWillUpdate(){
console.log('UNSAFE_componentWillUpdate--组件更新之前')
}
componentDidUpdate(){
console.log('componentDidUpdate--组件更新之后')
}
UNSAFE_componentWillReceiveProps(nextProps){
console.log('UNSAFE_componentWillReceiveProps--父组件更新传入新props',this.props,nextProps)
}
父组件销毁:父willUnMount->子wiiUnMount->子销毁->父销毁
函数组件第一次渲染完成后,无法基于内部操作使得组件自更新,但是如果调用它的父组件更新了,那么相关的子组件也会更新
类组件可以通过this.setState或forceUpdate更新视图,类组件具备:属性,状态,周期函数,ref...而函数组件里只具备属性
PureComponent
PureComponent和Component的区别
-
PureComponent会给类组件默认加一个shouldComponentUpdate钩子- 在此周期函数中,他会对新旧属性/状态做一个浅比较
- 如果经过浅比较,发现属性和状态没有改变,则返回false
import React from 'react'
function isObject(obj) {
return obj !== null && /^(object|function)$/.test(typeof obj)
}
function shallowEqual(obj1, obj2) {
if (!isObject(obj1) || !isObject(obj2)) return false
if (obj1 === obj2) return true
// 比较成员数量
let keysA = Reflect.ownKeys(obj1),
keysB = Reflect.ownKeys(obj2)
if (keysA.length !== keysB.length) return false
// 数量一致再比较内部成员
for (let i = 0; i < keysA.length; i++) {
let key = keysA[i]
// 如果一个对象中有,另一个没有;或,都有这个成员,但是成员值不一样
if (!obj2.hasOwnProperty(key) || !Object.is(obj1[key], obj2[key])) {
return false
}
}
return true
}
class DemoTwo extends React.PureComponent {
state = {
arr: [1, 2, 3], //此时地址为0x001
}
render() {
return (
<div>
{arr.map((item, index) => {
return <div key={index}>{item}</div>
})}
<br />
<button
onClick={() => {
arr.push(4) //给0x001地址里加一个值4
// this.setState({ arr }) //此时地址为0x001 浅比较,视图不会刷新
this.setState({arr:[...arr]}) //让arr状态的地址改变
}}>新增</button>
</div>
)
}
// shouldComponentUpdate(nextProps, nextState) {
// let { props, state } = this
// return !shallowEqual(props, nextProps) || !shallowEqual(state, nextState)
// }
}
Ref
基于ref获取DOM
给元素标签设置ref-获取真实dom,给类组件设置ref-获取组件实例,给函数组件设置ref-会报错
1.给需要获取的元素设置ref='xxx',组件内基于this.refs.xxx获取DOM元素
2.把ref设置为函数写法ref={x=>this.xxx = x}
- x为函数形参:存储的是当前DOM元素
- 将获取的元素挂载到组件的某个属性上,在组件内部使用this.xxx获取
3.基于React.createRef()创建一个ref对象-->{current:null},给元素设置ref对象即可
class DemoThree extends React.Component {
box3 = React.createRef()
render() {
return (
<div>
<h2 className="title" ref="titleBox">温馨提示</h2>
<h2 className="title" ref={(x) => (this.titleBox2 = x)}>友情提示</h2>
<h2 className="title" ref={this.box3}>郑重提示</h2>
</div>
)
}
componentDidMount() {
console.log(this.refs.titleBox)
console.log(this.titleBox2)
console.log(this.box3.current)
}
}
4.当给函数组件设置ref时,可以使用React.forwardRef()实现转发,可以获取到子组件内部的某个元素
class ChildOne extends React.Component {
render() {
return <div>类组件</div>
}
}
const ChildTwo = React.forwardRef((props, ref) => {
return (
<div>函数组件<button ref={ref}>按钮</button></div>
)
})
class DemoFour extends React.Component {
render() {
return (
<div>
<ChildOne ref={(x) => (this.child1 = x)} />
<ChildTwo ref={(x) => (this.child2 = x)} />
</div>
)
}
componentDidMount() {
console.log('类组件设置ref-获取组件实例', this.child1)
console.log('函数组件设置ref-会报错,使用React.forwardRef转发', this.child2)
}
}
setState
setState方法参数
this.setState([partialState],[callback])
-
partialState:支持部分状态修改 -
callback:状态更改之后执行的回调---类似于Vue中的nextTick- 发生在
componentDidUpdate钩子之后[componentDidUpdate会发生在任何状态更改视图更新之后,而setState的回调只针对指定状态修改之后触发] - 即便使用
shouldComponentUpdate钩子,阻止状态/视图更新,setState的回调函数也会执行
- 发生在
class SetStateDemo extends React.Component {
state = {
x: 10,
y: 5,
z: 0,
}
handle = () => {
let { x, y, z } = this.state
this.setState({ x: 100 }, () => {
console.log('setState-指定状态更新完毕')
})
}
shouldComponentUpdate() {
// 即便阻止状态/视图更新,setState的回调函数也会执行
return false
}
componentDidUpdate() {
console.log('视图更新完毕')
}
render() {
console.log('render')
let { x, y, z } = this.state
return (
<div>
x:{x}-y:{y}-z:{z}
<br />
<button onClick={this.handle}>修改状态</button>
</div>
)
}
}
setState的异步更新
在React18中 ,setState在任何地方执行,都是异步操作--更新队列机制
更新队列机制-基于异步操作,实现状态的'批处理',减少视图跟新次数,降低渲染消耗的性能,有效管理代码执行的逻辑顺序
原理:利用了更新队列updater机制来处理的
- 在当前的相同事件段内(浏览器最小反应间隔),遇到的
setState都会放到updater中 - 此时状态和视图均为更新
- 当所有代码操作结束,会通知
updater队列中的任务执行 - 把所有的
setState合并在一起执行,触发一次视图更新
state = {
x: 10,
y: 5,
z: 0,
}
handle = () => {
let { x, y, z } = this.state
this.setState({ x: x + 1 })
console.log(this.state.x) //10
this.setState({ y: y + 1 })
console.log(this.state.y) //5
this.setState({ z: z + 1 })
console.log(this.state.z) //0
// 改变三个状态,视图只刷新一次
}
- React18中:不论在什么地方执行
setState,它都是基于updater机制异步更新的; - 而在React16中:在合成事件(Jsx中 onXxxx事件)、周期函数中,
setState的操作是异步的!但是如果setState出现在其他异步操作中(如:定时器、手动获取dom事件监听addEventListener等),setState都会变成同步操作(立即更新状态,渲染视图)
flushSync
flushSync会立刻更新updater队列,批处理一次,让state状态处于最新状态
import React from 'react'
import { flushSync } from 'react-dom'
class SetStateDemo extends React.Component {
state = {
x: 10,
y: 5,
z: 0,
}
handle = () => {
let { x, y } = this.state
this.setState({ x: x + 1 })
console.log(this.state) //10,5,0
// 在flushSync 执行后会立即刷新updater队列,批处理一次
flushSync(() => {
this.setState({ y: y + 1 })
console.log(this.state) //10,5,0
})
console.log(this.state) //11,6,0
// 在修改z之前,确保x和y都已经更新了
this.setState({ z: x + y })
}
componentDidUpdate() {
console.log('视图更新完毕')
}
render() {
console.log('render')
let { x, y, z } = this.state
return (
<div>
x:{x}-y:{y}-z:{z}
<br />
<button onClick={this.handle}>修改状态</button>
</div>
)
}
}
export default SetStateDemo
setState的函数写法
handle = () => {
for (let i = 0; i < 100; i++) {
// 此时20次setState 会进入updater队列,x状态值不会改变,只会在最后一个setState执行一次,值为11,6,1
// this.setState({
// x: this.state.x + 1,
// y: this.state.y + 1,
// z: this.state.z + 1,
// })
// setState 第一个参数可以写成函数 ,参数为上一次的状态 也是执行一次,但是值为110,105,100
this.setState((prevState) => {
return {
x: prevState.x + 1,
y: prevState.y + 1,
z: prevState.z + 1,
}
})
}
合成事件
绑定方式
基于React内部处理,如果我们给合成事件绑定一个普通函数,那此函数内的this是undefined
-
可以在jsx中使用
bind绑定this,预先处理函数中的this与实参 -
直接把绑定的函数写成箭头函数,此时箭头函数会自动接收一个参数
SyntheticBaseEvent合成事件对象,如果是经过bind处理过的函数,则SyntheticBaseEvent会在最后一个参数 -
SyntheticBaseEvent:React内部经过特殊处理,把浏览器的事件对象统一化后,构建的一个事件对象- clientX/clientY
- pageX/pageY
- target
- type
- preventDefault
- stopPropagation
- nativeEvent:原生浏览器事件对象
class EventDemo extends React.Component {
// 如果我们给合成事件绑定一个普通函数,那此函数内的this是undefined
// 如果想让普通函数内的this为组件实例,只需在jsx内部使用bind绑定this
// 或者直接把绑定的函数写成箭头函数,此时箭头函数会自动接收一个参数SyntheticBaseEvent合成事件对象
//EventDemo.prototype.handle = function handle(){}
handle1(x, y,e) {
console.log(this, x, y,e) //如果经过bind处理,最后一个参数才是SyntheticBaseEvent
}
// 这种写法,不会在原型上添加方法,而是给实例加一个私有属性
handle2 = (e) => {
console.log(this,e) //EventDemo,SyntheticBaseEvent
}
render() {
return (
<div>
{/* 可以使用bind改变this */}
<button onClick={this.handle1.bind(this, 10, 20)}>按钮1</button>
<button onClick={this.handle2}>按钮2</button>
</div>
)
}
}
合成事件处理原理
React中的合成事件不是简单的给元素基于addEventListener做的事件绑定,React的合成事件绑定全都是基于‘事件委托’处理的
- 在React17及以后的版本,都是委托给root容器(捕获和冒泡都做了委托)
- 在17版本以前都是委托给document容器的(而且只作了冒泡阶段的委托)
- 对于没有实现事件传播机制的事件,才是单独做的事件绑定(例如:
onMouseEnter/onMouseLeave)
在组件渲染的时候,如果发现jsx元素中有绑定onXxx/onXxxCapture这样的属性,不会给当前元素直接做事件绑定,只是把绑定的方法赋值给元素的相关属性!例如:outer.onClick =() => {console.log('outer 冒泡'} onClickCapture=() => {console.log('outer 捕获')}
然后对root容器做了事件绑定(捕获和冒泡):因为组件中所渲染的内容,最后都会插入到root容器中,这样点击页面中任何一个元素,都会把root的点击行为触发,而在给root绑定的事件中,会把之前给组件元素中设置的onXxx/onXxxCapture属性,在相应的阶段执行
<body>
<div id="root" class="center">
<div id="outer" class="center">
<div id="inner" class="center"></div>
</div>
</div>
<script>
const root = document.querySelector('#root'),
outer = document.querySelector('#outer'),
inner = document.querySelector('#inner');
// 经过视图渲染,遇到合成事件,并没有直接给元素做事件绑定,而是给outer/inner加了onClick/onClickCapture属性
outer.onClick = () => { console.log('outer 冒泡[合成]') }
outer.onClickCapture = () => { console.log('outer 捕获[合成]') }
inner.onClick = () => { console.log('inner 冒泡[合成]') }
inner.onClickCapture = () => { console.log('inner 捕获[合成]') }
// 给root添加事件绑定(捕获和冒泡都做了)
root.addEventListener('click', (e) => {
// 获取事件路径[事件源->...->window]
let path = e.composedPath();
[...path].reverse().forEach(ele => {
let handle = ele.onClickCapture
if (handle) handle()
})
}, true)
root.addEventListener('click', (e) => {
let path = e.composedPath();
path.forEach(ele => {
let handle = ele.onClick
if (handle) handle()
})
}, false)
</script>
</body>
样式私有化
行内样式
import React from 'react';
const Demo = function Demo(props) {
const titleSty = {
color: props.color,
fontSize: '16px'
};
const boxSty = {
width: '300px',
height: '200px'
};
return <div style={boxSty}>
<h1 style={titleSty}>标题</h1>
<h2 style={{ ...titleSty, fontSize: '14px' }}>子标题</h2>
</div>;
};
export default Demo;
less/scss嵌套类名
保证最外层类名唯一
.personal-box {
width: 300px;
height: 200px;
.title {
color: red;
font-size: 16px;
}
.sub-title {
.title;
font-size: 14px;
}
}
CSS Modules
CSS的规则都是全局的,任何一个组件的样式规则,都对整个页面有效;产生局部作用域的唯一方法,就是使用一个独一无二的class名字
创建xxx.module.css,react脚手架中有对css Module的配置
// react-dev-utils/getCSSModuleLocalIdent.js
const loaderUtils = require('loader-utils');
const path = require('path');
module.exports = function getLocalIdent(
context,
localIdentName,
localName,
options
) {
// Use the filename or folder name, based on some uses the index.js / index.module.(css|scss|sass) project style
const fileNameOrFolder = context.resourcePath.match(
/index.module.(css|scss|sass)$/
)
? '[folder]'
: '[name]';
// Create a hash based on a the file location and class name. Will be unique across a project, and close to globally unique.
const hash = loaderUtils.getHashDigest(
path.posix.relative(context.rootContext, context.resourcePath) + localName,
'md5',
'base64',
5
);
// Use loaderUtils to find the file or folder name
const className = loaderUtils.interpolateName(
context,
fileNameOrFolder + '_' + localName + '__' + hash,
options
);
// Remove the .module that appears in every classname when based on the file and replace all "." with "_".
return className.replace('.module_', '_').replace(/./g, '_');
};
全局作用域
CSS Modules 允许使用 :global(.className) 的语法,声明一个全局规则。凡是这样声明的class,都不会被编译成哈希字符串。
// xxx.module.css
:global(.personal) {
width: 300px;
height: 200px;
}
// xxx.jsx
const Demo = function Demo() {
return <div className='personal'>
...
</div>;
};
class继承/组合
在 CSS Modules 中,一个选择器可以继承另一个选择器的规则,这称为”组合”
// xxx.module.css
.title {
color: red;
font-size: 16px;
}
.subTitle {
composes: title;
font-size: 14px;
}
// 组件还是正常的调用,但是编译后的结果
<h1 class="demo_title__tN+WF">标题</h1>
<h2 class="demo_subTitle__rR4WF demo_title__tN+WF">子标题</h2>
React-JSS
JSS是一个CSS创作工具,它允许我们使用JavaScript以生命式、无冲突和可重用的方式来描述样式。JSS 是一种新的样式策略! React-JSS 是一个框架集成,可以在 React 应用程序中使用 JSS。它是一个单独的包,所以不需要安装 JSS 核心,只需要 React-JSS 包即可。React-JSS 使用新的 Hooks API 将 JSS 与 React 结合使用。
从 react-jss 第10版本之后,不支持在类组件中使用,只能用于函数组件中!
import React from 'react';
import { createUseStyles } from 'react-jss';
const useStyles = createUseStyles({
personal: {
width: '300px',
height: '200px',
// 基于 & 实现样式嵌套
'& span': {
color: 'green'
}
},
title: {
// 使用动态值
color: props => props.color,
fontSize: '16px'
},
// 使用动态值
subTitle: props => {
return {
color: props.color,
fontSize: '14px'
};
}
});
const Demo = function Demo(props) {
const { personal, title, subTitle } = useStyles(props);
return <div className={personal}>
<h1 className={title}>珠峰培训</h1>
<h2 className={subTitle}>珠峰培训</h2>
<span>珠峰培训</span>
</div>;
};
export default Demo;
如果想在类组件中使用,创建一个代理组件(函数组件),获取ReactJss的样式,将样式基于属性传给类组件
import React from 'react';
import { createUseStyles } from 'react-jss';
const useStyles = createUseStyles({
...
});
// 高阶组件
const withStyles = function withStyles(Component) {
return function (props) {
const styles = useStyles(props);
return <Component {...props} {...styles} />;
};
};
class Demo extends React.Component {
render() {
const { personal, title, subTitle } = this.props;
return <div className={personal}>
...
</div>;
}
}
// 使用高阶组件
export default withStyles(Demo);
Styled-Components
CSS-IN-JS 的模式:也就是把CSS像JS一样进行编写
styled-components.com/docs/basics… 想要有语法提示,可以安装vscode插件:vscode-styled-components
import styled from 'styled-components'
export const primaryColor = 'skyblue'
export const dangerColor = 'red'
export const medSize = '16px'
export const MainBox = styled.main.attrs((props) => {
return {
// 设置默认值
size: props.size || 20,
}
})`
line-height: ${(props) => props.size}px;
p {
color: ${primaryColor};
&:hover {
color: ${dangerColor};
}
}
`
import { MainBox } from './VoteMainStyle'
const VoteMain = function VoteMain(props) {
let { supNum, oppNum } = props
let ratio = useMemo(() => {
let ratio = '--',
total = supNum + oppNum
if (total > 0) ratio = ((supNum / total) * 100).toFixed(2)
return ratio
}, [supNum, oppNum])
return (
<MainBox size={40}>
<p>支持人数:{supNum}人</p>
<p>反对人数:{oppNum}人</p>
<p>支持比例:{ratio}%</p>
</MainBox>
)
}
高阶组件HOC
基于闭包[柯里化函数]实现的组件代理,可以在代理组件中处理一些业务逻辑,最后基于属性传递给要渲染的组件
import React from 'react'
const HocDemo = function HocDemo(props) {
console.log(props)
return <div>haah</div>
}
const ProxyTest = function ProxyTest(Component) {
// 返回一个组件
return function HOC(props) {
let isUse = false
return <Component {...props} isUse={isUse}></Component>
}
}
// 导出HOC
export default ProxyTest(HocDemo)
React Hooks
React组件分类
-
函数组件
- 不具备'状态,ref,周期函数等内容',第一次渲染完毕之后,无法基于组件内部的来控制其更新,因此称之为静态组件
- 但是具备props 和 插槽,父组件可以控制其重新渲染
- 渲染流程简单,渲染速度快
- 基于FP(函数式编程)思想,提供更细粒度的逻辑组织和复用
-
类组件
- 具备'状态,ref,周期函数等内容',可
- 以灵活控制组件更新,基于钩子函数也可灵活掌控组件不同阶段做不同的事情
- 渲染流程繁琐,渲染速度相对较慢
- 基于OOP(面向对象)思想,更方便实现继承
-
Hooks组件
- 基于React提供的Hooks函数,让函数式组件动态化
useState
基础用法
作用: 在函数式组件中使用状态,修改状态值可让函数组件更新,类似类组件中的setState
- 执行useState 方法传递初始值,返回的是一个数组[状态值,修改状态的方法]
- 函数组件没有实例的概念,调用组件不再是创建类的实例,而是把函数执行,产生一个私有上下文而已,所有在函数组件中不涉及this
- 函数组件的每一次渲染(或者更新),都会把函数重新执行,产生一个全新的私有上下文
- 内部代码重新执行,涉及的函数需要重新构建(这些函数的作用域,是每一次执行函数组件所产生的闭包)
- 每一次执行函数组件,重新执行useState,但是只有第一次传入的初始值会生效,其余以后再执行,获取的状态都是上一次函数执行过后的最新状态值,返回的修改状态的方法也是全新的
// var _state
// function useState(initialValue) {
// if (typeof _state === 'undefined') _state = initialValue
// var setState = function setState(value) {
// _state = value
// console.log('通知视图更新')
// }
// return [_state, setState]
// }
function UseStateDemo() {
let [num, setNum] = useState(0)
const handleClick = () => {
setNum(num + 10)
}
return (
<div>
<span>{num}</span>
<Button type="primary" onClick={handleClick}>
累加
</Button>
</div>
)
}
export default UseStateDemo
- 在useState 返回的 更新状态的方法中 ,不会像类组件中的this.setState一样支持部分状态更改,需要修改整个状态对象,如果只修改部分状态,那其他属性会是undefined
- 官方建议:需要多个状态,就把useState执行多次,而不会写成对象形式
function UseStateDemo(props) {
// 在useState 返回的 更新状态的方法中 ,不会像类组件中的this.setState一样支持部分状态更改,需要修改整个状态对象,如果只修改部分状态,那其他属性会是undefined
// 官方建议:需要多个状态,就把useState执行多次,而不会写成对象形式
// let [state, setState] = useState({
// supNum: 10,
// oppNum: 5,
// })
// const handle = (type) => {
// if (type === 'sup') {
// setState({
// ...state,
// supNum: state.supNum + 1,
// })
// } else {
// setState({
// ...state,
// oppNum: state.oppNum + 1,
// })
// }
// }
let [supNum, setSupNum] = useState(10),
[oppNum, setOppNum] = useState(5)
const handle = (type) => {
if (type === 'sup') {
setSupNum(supNum + 1)
} else {
setOppNum(oppNum + 1)
}
}
return (
<div className="vote-box">
<div className="header">
<h2 className="title">{props.title}</h2>
<span className="num">{supNum + oppNum}</span>
</div>
<div className="main">
<p>支持人数:{supNum}人</p>
<p>反对人数:{oppNum}人</p>
</div>
<div className="footer">
<Button type="primary" onClick={handle.bind(null, 'sup')}>
支持
</Button>
<Button type="primary" danger onClick={handle.bind(null, 'opp')}>
反对
</Button>
</div>
</div>
)
}
同步异步
在react18中 基于useState创建出来的修改状态的方法,他们执行也是异步操作,原理等同于类组件中的this.setState(基于更新队列)实现状态批处理
- useState 自带性能优化机制,每一次修改状态值时,会拿最新的状态值和之前的状态值作比较[基于Object.is作比较],如果发现两次值是一样的,则不会修改状态,也不会让视图刷新[类似于PureComponent,shouldComponentUpdate中做的优化]
function UseStateDemo(props) {
console.log('render')
let [x, setX] = useState(10)
const handle = () => {
// for (let i = 0; i < 10; i++) {
// setX(x + 1)
// }
// 如果修改的状态值与之前的状态值一样的话,视图不会更新
setX(10)
}
return (
<div className="demo">
<span className="num">x:{x}</span>
<button onClick={handle}>新增</button>
</div>
)
}
useState的函数用法
- useState修改状态的方法可以传递一个函数,函数参数接受的值为上一次的状态值
function UseStateDemo(props) {
console.log('render')
let [x, setX] = useState(10)
const handle = () => {
// 此时视图渲染1次,值为11
// for (let i = 0; i < 10; i++) {
// setX(x + 1)
// }
// 此时视图渲染1次,值为20
for (let i = 0; i < 10; i++) {
setX((prev) => prev + 1)
}
}
return (
<div className="demo">
<span className="num">x:{x}</span>
<button onClick={handle}>新增</button>
</div>
)
}
- 如果useState传递的是一个函数,则此函数只会在组件第一次渲染时执行,后续更新时,都是拿最新状态值,而不会再去执行函数。如果把状态的初始化操作放在函数外,那每一次更新组件都会执行初始化逻辑,而更新时,useState用到的只是更新之后的状态,用不到初始值,此时需要把初始化状态的操作放到useState()的函数中
function UseStateDemo(props) {
let [x, setX] = useState(() => {
let { a, b } = props,
total = 0
for (let i = x; i <= y; i++) {
total += +String(Math.random()).substring(2)
}
return total
})
}
useEffect
基本用法
-
useEffect(callback)
- 在第一次渲染完毕之后,执行callback,等价于componentDidMount
- 在组件每一次更新完毕之后,也会执行callback,等价于componentDidUpdate
-
useEffect(callback,[])
- 只有第一次渲染完毕之后,才会执行callback,之后每一次视图更新完毕,callback不再执行,类似于componentDidMount
- 当[]中传递了状态,当依赖的状态值(或多个状态中的一个)发生改变,也会触发callback执行,如果依赖的状态没有改变,则callback不会执行
-
useEffect(()=>{return ()=>{}})
- 当callback返回一个函数,此函数会在组件释放时执行
- 如果组件更新,会把上一次返回的函数执行,所以会获取到上一次的状态值
useEffect 必须在函数的最外层作用域中,不能把他放到条件判断,循环等操作语句中
function UseEffectDemo() {
let [x, setX] = useState(0)
useEffect(() => {
// 可以获取到最新的状态值
console.log('ok', x)
console.log(document.querySelector('.num'))
})
useEffect(() => {
console.log('ok2', x)
}, [])
useEffect(() => {
console.log('ok3', x)
}, [x])
useEffect(() => {
return () => {
// 获取的是上一次的状态值
console.log('ok4', x)
}
}, [x])
const handle = () => {
setX(x + 1)
}
return (
<div className="demo">
<span className="num">x:{x}</span>
<button onClick={handle}>新增</button>
</div>
)
}
useLayoutEffect
- 如果链表中的callback执行又修改了状态值[视图更新],对于useEffect来说:第一次真实dom已经渲染,组件更新会重新渲染真实dom,所以频繁修改状态时,会出现样式/内容闪烁
- 对于useEffect来讲:会阻塞浏览器渲染真实dom,优先执行effect链表中的callback,等到callback执行完毕,再合并渲染一次真实dom
- useLayoutEffect的callback 要优先于useEffect 中的callback执行
- 在两者的callback中都可以获取到dom元素[原因:真实dom已经创建,区别只是浏览器是否渲染]
视图更新的步骤
编译jsx为createElement格式
执行createElement创建虚拟dom
执行render创建真实dom(diff)
- useLayoutEffect会阻塞第四步操作,先去执行effect链表中的方法(同步执行)
- useEffect不会阻塞浏览器渲染,callback和浏览器渲染时同时进行的(异步操作)
浏览器渲染绘制真实dom
function UseLayoutEffectDemo() {
console.log('render')
let [x, setX] = useState(0)
useLayoutEffect(() => {
console.log('useLayoutEffect')
if (x == 0) {
setX(10)
}
}, [x])
useEffect(() => {
console.log('useEffects')
if (x == 0) {
setX(10)
}
}, [x])
return (
<div
className="demo"
style={{
background: x === 0 ? 'red' : 'green',
}}>
<span className="num">x:{x}</span>
<button onClick={() => {setX(0)}}>新增</button>
</div>
)
}
useRef
基本使用
- 基于ref={(x) => (box = x)} 的方式,可以把创建的dom元素(或子组件的实例)赋值给box变量
- 也可以基于React.createRef()创建ref对象,然后通过current属性获取dom或实例
- 通过hook函数useRef创建ref对象,通过current属性获取dom或实例
useRef在每一次组件更新时,不会重复创建新的ref对象,获取到的还是第一次的对象,而React.createRef每次都会重新创建新的ref对象
let prev1, prev2
function UseRefDemo() {
console.log('render')
let [x, setX] = useState(0)
let box = useRef(),
box2 = React.createRef()
if (!prev1) {
// 组件第一次渲染,存储一份ref对象
prev1 = box
prev2 = box2
} else {
// 组件更新,验证第一次创建的ref与第二次创建的ref对象是否一致
// useRef在每一次组件更新时,不会重复创建新的ref对象,获取到的还是第一次的对象
console.log(prev1 === box) //true
// 而React.createRef每次都会重新创建新的ref对象
console.log(prev2 === box2) //false
}
useEffect(() => {
console.log(box.current)
console.log(box2.current)
}, [])
return (
<div className="demo">
<span className="num" ref={box}>x:{x}</span>
<span className="num" ref={box2}>box2</span>
<button onClick={() => {setX(1)}}>新增</button>
</div>
)
}
useImperativeHandle
- 如果给函数子组件设置ref会报错,需要使用React.forwardRed实现ref转发,获取子组件内部元素
- 在子组件使用useImperativeHandle可以将子组件状态或方法返回给父组件
// 基于forwardRef转发获取子组件内部的某个元素,基于useImperativeHandle能获取子组件的状态或方法
const Child = React.forwardRef(function Child(props, ref) {
let [text, setText] = useState('你好')
useImperativeHandle(ref, () => {
// 在这里返回的状态或方法,可以被父组件的ref对象接收到
return {
text,
submit,
}
})
const submit = () => {}
return (
<div className="child">
<span ref={ref}>hahaha</span>
</div>
)
})
function UseRefDemo() {
let x = useRef()
useEffect(() => {
console.log(x.current)
}, [])
return (
<div className="demo">
<Child ref={x}></Child>
</div>
)
}
useMemo
基本使用
let xxx = useMemo(callback,[...args])- useMemo 具备计算缓存效果,在依赖值没有发生改变,callback没有触发执行的时候,xxx获取的是上一次计算出来的结果,类似与vue中的computed
const UseMemoDemo = function () {
let [supNum, setSupNum] = useState(10),
[oppNum, setOppNum] = useState(5),
[x, setX] = useState(0)
/**
* 组件每一次更新,都要把函数重新执行,此时如果是其他状态更新了导致视图更新,此逻辑不需再次执行
* 只有依赖的值变化,才去执行这段逻辑
* let xxx = useMemo(callback,[...args])
* useMemo 具备计算缓存效果,在依赖值没有发生改变,callback没有触发执行的时候,xxx获取的是上一次计算出来的结果,类似与vue中的computed
*/
let ratio = useMemo(() => {
console.log('ok')
let total = supNum + oppNum,
ratio = '--'
if (total > 0) ratio = ((supNum / total) * 100).toFixed(2) + '%'
return ratio
}, [supNum, oppNum])
return (
<div className="vote-box">
<div className="main">
<p>支持人数:{supNum}人</p>
<p>反对人数:{oppNum}人</p>
<p>支持比例:{ratio}</p>
<p>x:{x}</p>
</div>
<div className="footer">
<Button type="primary" onClick={() => { setSupNum(supNum + 1) }}> 支持 </Button>
<Button type="primary" onClick={() => { setOppNum(oppNum + 1) }}> 反对 </Button>
<Button type="primary" onClick={() => { setX(x + 1) }}> x+1 </Button>
</div>
</div>
)
}
useCallback
-
const xxx = useCallback(callback,[...args]) -
组件第一次渲染,useCallback执行,创建一个函数赋给xxx
-
组件后续更新,判断依赖的值是否改变,如果改变,重新创建新的函数堆内存,赋值给xxx,如果依赖没有更新,或没有设置依赖,则不会创建新的函数
-
使用场景:不用每个函数都用
useCallback,父组件嵌套子组件,父组件将方法基于属性传递给子组件时,此时这个方法用useCallback更好- 父组件使用
useCallback,每一次传递相同堆内存的函数 - 子组件中验证props的值是否发生改变,如果没有改变,则不让组件更新(类组件继承
React.PureComponent即可,函数组件使用React.memo包裹)
- 父组件使用
// class Child extends React.PureComponent {
// render() {
// console.log('child render')
// return <div>子组件</div>
// }
// }
const Child = React.memo(function (props) {
console.log('child render')
return <div>子组件</div>
})
const UseCallbackDemo = function () {
let [x, setX] = useState(0)
const handle = useCallback(() => {}, [])
return (
<div className="vote-box">
<Child handle={handle}></Child>
<div className="main">
<p>x:{x}</p>
</div>
<div className="footer">
<Button type="primary" onClick={() => { setX(x + 1)} }>x+1</Button>
</div>
</div>
)
}
当useCallback如果数组不传参,则永远都是第一次创建时的函数闭包作用域,其函数内部的状态引用也永远是第一次渲染时的状态
父子组件通信(props)
-
以父组件为主导,基于属性实现通信
- 父组件将属性状态传递给子组件
- 父组件基于插槽,可以将html结构传递给子组件
- 父组件将方法传递给子组件,子组件执行方法改变状态
-
父组件基于ref获取子组件实例[或者子组件基于useImperativeHandle暴露数据或方法]
祖先后代通信(上下文context)
- 祖先组件需要把状态,修改状态的方法放在上下文中
- 后代组件执行修改状态的方法,祖先组件更新,然后后代组件也会跟着更新,从而获取最新的上下文信息重新绑定
上下文对象
//VoteContext.js
import React from "react";
const VoteContext = React.createContext({});
export default VoteContext
父组件:基于上下文对象提供的Provider组件的value属性 向上下文中存储信息
class Vote extends React.Component {
state = {
supNum: 10,
oppNum: 0,
}
change = (type) => {
let { supNum, oppNum } = this.state
if (type === 'sup') {
this.setState({ supNum: supNum + 1 })
} else {
this.setState({ oppNum: oppNum + 1 })
}
}
render() {
let { supNum, oppNum } = this.state
return (
// 基于上下文对象提供的Provider组件的value属性 向上下文中存储信息
<VoteContext.Provider value={{ supNum, oppNum, change: this.change }}>
<div className="vote-box">
<header>
<h2 className="title">合计</h2>
<span className="num">{supNum + oppNum}</span>
</header>
<VoteMain supNum={supNum} oppNum={oppNum}></VoteMain>
<VoteFooter change={this.change}></VoteFooter>
</div>
</VoteContext.Provider>
)
}
}
子组件1:设置私有属性contextType上下文对象,从this.context获取信息
class VoteMain extends React.Component {
static contextType = VoteContext
render() {
console.log(this.context)
let { supNum, oppNum } = this.context
let ratio = '--',
total = supNum + oppNum
if (total > 0) ratio = ((supNum / total) * 100).toFixed(2)
return (
<main>
<p>支持人数:{supNum}人</p>
<p>反对人数:{oppNum}人</p>
<p>支持比例:{ratio}%</p>
</main>
)
}
}
子组件2:基于VoteContext.Consumer组件获取上下文中的信息
class VoteFooter extends React.PureComponent {
render() {
console.log('footer render')
return (
<VoteContext.Consumer>
{(context) => {
let { change } = context
return (
<footer>
<Button type="primary" onClick={change.bind(null, 'sup')}>
支持
</Button>
<Button type="primary" danger onClick={change.bind(null, 'opp')}>
反对
</Button>
</footer>
)
}}
</VoteContext.Consumer>
)
}
}
useContext
const VoteMain = function VoteMain() {
let { supNum, oppNum } = useContext(VoteContext)
let ratio = useMemo(() => {
let ratio = '--',
total = supNum + oppNum
if (total > 0) ratio = ((supNum / total) * 100).toFixed(2)
return ratio
}, [supNum, oppNum])
return (
<main>
<p>支持人数:{supNum}人</p>
<p>反对人数:{oppNum}人</p>
<p>支持比例:{ratio}%</p>
</main>
)
}
useReducer
- 用useReducer来替换 useState,对数据实行批量管理
- 一个组件的逻辑很复杂,需要大量的状态,此时使用useReducer
import { useReducer } from 'react'
//初始值
const initialState = {
num: 30,
n: 40,
}
//数据管理员
const Reducer = function Reducer(state, action) {
state = { ...state }
switch (action.type) {
case 'sup':
state.num++
break
case 'opp':
state.num--
break
default:
break
}
return state
}
function B() {
let [state, dispatch] = useReducer(Reducer, initialState)
return (
<div>
BBBBB---{state.num}
<button
onClick={() => {
dispatch({ type: 'sup' })
}}>
加
</button>
<button
onClick={() => {
dispatch({ type: 'opp' })
}}>
减
</button>
</div>
)
}
export default B
Redux
基础用法
- 创建
store,规划reducer中的acticon[type]行为 - 在入口中,基于上下文对象,把
store放入上下文中,需要用到store的组件,从上下文中获取状态 - 组件中基于
store.getState()获取公共状态,基于store.dispatch(action)完成任务派发
使用公共状态的组件,必须基于
store.subscribe()向事件池中加入让组件更新的方法,公共状态改变触发事件让组件更新,从而获取最新的状态进行绑定
// store
import { createStore } from 'redux'
let initial = {
supNum: 10,
oppNum: 5,
}
const reducer = (state = initial, action) => {
// state存储公共状态
// action 每一次基于dispatch派发的时候,传递进的对象要求具备type属性
// 先克隆一份,避免后续操作直接影响到仓库中的状态,必须通过action才能修改状态
state = { ...state }
switch (action.type) {
case 'VOTE_SUP':
state.supNum++
break
case 'VOTE_OPP':
state.oppNum++
break
default:
}
return state
}
const store = createStore(reducer)
export default
// 入口
root.render(
<Fragment>
<ConfigProvider locale={zhCN}>
{/* 基于上下文对象,全局注册store */}
<VoteContext.Provider value={{ store }}>
<Vote></Vote>
</VoteContext.Provider>
</ConfigProvider>
</Fragment>,
)
函数组件中使用store
const Vote = function () {
// 获取上下文中的store
const { store } = useContext(VoteContext)
// 获取store中的状态
let { supNum, oppNum } = store.getState()
let [_, setNum] = useState(0)
// 组件第一次渲染完毕,把让组件更新的方法放到store的事件池中,每一次状态更新都会触发这个事件
useEffect(() => {
// store.subscribe()的返回值是一个方法,可以把放入事件池的方法移除掉
let unsubscribe = store.subscribe(() => {
setNum(+new Date())
})
}, [])
return (
<div className="vote-box">
<header>
<h2 className="title">合计</h2>
<span className="num">{supNum + oppNum}</span>
</header>
<VoteMain></VoteMain>
<VoteFooter></VoteFooter>
</div>
)
}
类组件中使用store
class VoteMain extends React.Component {
static contextType = VoteContext
render() {
const { store } = this.context
let { supNum, oppNum } = store.getState()
let ratio = '--',
total = supNum + oppNum
if (total > 0) ratio = ((supNum / total) * 100).toFixed(2)
return (
<main>
<p>支持人数:{supNum}人</p>
<p>反对人数:{oppNum}人</p>
<p>支持比例:{ratio}%</p>
</main>
)
}
componentDidMount() {
const { store } = this.context
store.subscribe(() => {
this.forceUpdate()
})
}
}
const VoteFooter = function VoteFooter(props) {
const { store } = useContext(VoteContext)
console.log('footer render')
return (
<footer>
<Button
type="primary"
onClick={() => {
store.dispatch({
type: 'VOTE_SUP',
})
}}>
支持
</Button>
<Button
type="primary"
danger
onClick={() => {
store.dispatch({
type: 'VOTE_OPP',
})
}}>
反对
</Button>
</footer>
)
}
简单实现redux
import utils from './assets/utils'
/**
* 手写redux
* @param {function} reducer
*/
export const createStore = function (reducer) {
/**
* 公共状态
*/
let state
/**
* 事件池
*/
let listeners = []
/**
* 获取公共状态
*/
const getState = function () {
return state
}
/**
* 向事件池注入让组件更新的方法
* @param {function} listener 事件
*/
const subscribe = function (listener) {
if (typeof listener !== 'function') throw new TypeError('subscribe的参数为一个函数!')
// 将事件加入事件池
if (!listeners.includes(listener)) listeners.push(listener)
/**
* 从事件池,移除方法的函数
*/
return function unsubscribe() {
let index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
/**
* 派发任务通知reducer执行action
* @param {object} action 行为
*/
const dispatch = function (action) {
if (!utils.isPlainObject(action)) throw new TypeError('dispatch必须传一个对象')
if (typeof action.type === 'undefined') throw new TypeError('action中必须要有type属性')
// 执行reducer 传递公共状态,行为对象,接收执行的返回值,替换公共状态
state = reducer(state, action)
// 当状态更改 把事件池中的方法执行
listeners.forEach((listener) => {
listener()
})
}
// redux内部默认进行一次派发,给状态赋初始值
dispatch({
// 保证 type与store中的其他type不冲突
type: Symbol(),
})
return {
getState,
subscribe,
dispatch,
}
}
react-redux
- react-redux 内部自己创建了上下文对象,并且可以把store放在上下文中,我们在组件使用时,无需我们自己获取上下文中的store
- 在组件中,我们想获取公共状态时无需自己基于上下文获取store,也无需自己基于getState获取状态
- 也不需要我们收到把组件更新的方法放在事件池中
import { Provider } from 'react-redux'
import store from './views/react-redux/store'
import Vote from './views/react-redux/Vote'
root.render(
<Fragment>
<ConfigProvider locale={zhCN}>
<Provider store={store}>
<Vote></Vote>
</Provider>
</ConfigProvider>
</Fragment>,
)
import { connect } from 'react-redux'
const Vote = function (props) {
// 获取store中的状态
let { supNum, oppNum } = props
return (
<div className="vote-box">
<header>
<h2 className="title">合计</h2>
<span className="num">{supNum + oppNum}</span>
</header>
<VoteMain></VoteMain>
<VoteFooter></VoteFooter>
</div>
)
}
/**
* connect(mapStateToProps,mapDispatchProps)(component)
* mapStateToProps 可以获取到redux中的状态 作为属性传递给组件
* mapDispatchProps 可以把需要派发的任务 作为属性传递给组件
*/
export default connect((state) => state.vote)(Vote)
import action from './store/actions'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
const VoteFooter = function VoteFooter(props) {
let { sup, opp } = props
return (
<footer>
<Button type="primary" onClick={sup}>
支持
</Button>
<Button type="primary" danger onClick={opp}>
反对
</Button>
</footer>
)
}
export default connect(null, (dispatch) => {
return {
sup() {
dispatch(action.vote.sup())
},
opp() {
dispatch(action.vote.opp())
},
}
})(React.memo(VoteFooter))
// 简写
export default connect(
null,
action.vote,
// (dispatch) => {
// console.log(bindActionCreators(action.vote, dispatch))
// return {
// sup() {},
// opp() {},
// }}
)(React.memo(VoteFooter))
redux-thunk与redux-promise
const delay = (time) => {
return new Promise((res) => {
setTimeout(() => {
res()
}, time)
})
}
const voteAction = {
// redux-thunk语法
sup() {
return async (dispatch) => {
await delay()
dispatch({ type: TYPES.VOTE_SUP })
}
},
// redux-promise语法
async opp() {
await delay(1000)
return {
type: TYPES.VOTE_OPP,
}
},
}
redux-thunk和redux-promise中间件,都是处理异步派发的,区别是一个是手动,一个是自动
-
都是派发两次,第一次派发用的是重写的dispatch,这个方法不会去校验对象是否有type属性,也不会在乎传递的对象是否为标准普通对象
-
第二次派发
- redux-thunk:把返回的函数执行,把真正的dispatch传递给函数
- redux-promise:监听返回的promise实例,在实例成功后,再基于真正的dispatch,把成功的结果再进行派发
redux-toolkit
store/index.js
import { configureStore } from '@reduxjs/toolkit'
import reduxLogger from 'redux-logger'
import reduxThunk from 'redux-thunk'
import taskSlice from './features/task.slice'
const store = configureStore({
// 指定reducer,按模块管理各个切片
reducer: {
task: taskSlice,
},
// 使用中间件-默认集成了reduxThunk,如果写了middleware,则会替换内部的,所以必须要加上reduxThunk
middleware: [reduxLogger, reduxThunk],
})
export default store
各模块切片
import { createSlice } from '@reduxjs/toolkit'
import { getTaskList } from '../../../../api/task'
// 创建切片
const taskSlice = createSlice({
// 切片名
name: 'task',
// 状态初始值
initialState: {
taskList: null,
},
// 处理业务逻辑
reducers: {
getAllTaskList(state, action) {
// state:redux中的状态基于immer库管理,无需自己克隆了
// action:派发的行为对象,也无需考虑行为标识的问题了,传递的其他信息,都是以action.payload传递进来
state.taskList = action.payload
},
updateTaskById(state, { payload }) {
let { taskList } = state
if (!Array.isArray(state.taskList)) return
state.taskList = taskList.map((item) => {
if (+item.id === +payload) {
item.state = 2
item.complete = new Date().toLocaleString('zh-CN', { hour12: false })
}
return item
})
},
removeTaskById(state, { payload }) {
let { taskList } = state
if (!Array.isArray(state.taskList)) return
state.taskList = taskList.filter((item) => +item.id !== +payload)
},
},
})
// 从切片中获取actionCreator:此处的方法与reducers中的仅仅是函数名相同
// 方法执行,返回需要派发的action对象 {type: 'task/getAllTaskList', payload: undefined}
// 后期基于dispatch进行任务派发
export let { getAllTaskList, removeTaskById, updateTaskById } = taskSlice.actions
// 异步派发-基于redux-thunk
export const getAllTaskListAsync = () => {
return async (dispatch) => {
let taskList = []
try {
let { code, list } = await getTaskList(0)
if (+code === 0) {
taskList = list
}
} catch (_) {}
// 派发任务
dispatch(getAllTaskList(taskList))
}
}
export default taskSlice.reducer
组件中使用hook函数useSelector获取状态,useDispatch获取派发任务的方法
import { useSelector, useDispatch } from 'react-redux'
import { getAllTaskListAsync, removeTaskById, updateTaskById } from './store/features/task.slice'
function Task() {
// 获取公共状态
let { taskList } = useSelector((state) => state.task),
dispatch = useDispatch()
let [selectedIndex, setSelectedIndex] = useState(0),
[tableData, setTableData] = useState([]),
[tableLoading, setTableLoading] = useState(false),
const queryTaskList = async () => {
if (!taskList) {
setTableLoading(true)
await dispatch(getAllTaskListAsync())
setTableLoading(false)
}
}
useEffect(() => {
queryTaskList()
}, [])
useEffect(() => {
if (!taskList) taskList = []
if (selectedIndex !== 0) {
taskList = taskList.filter((item) => +item.state === +selectedIndex)
}
setTableData(taskList)
}, [selectedIndex, taskList])
// 删除
const handleRemove = async (id) => {
try {
let { code } = await removeTask(id)
if (+code == 0) {
dispatch(removeTaskById(id))
message.success('删除成功~')
} else {
message.error('删除失败~')
}
} catch (_) {}
}
// 完成
const compelete = async (id) => {
try {
let { code } = await completeTask(id)
if (+code == 0) {
dispatch(updateTaskById(id))
message.success('操作成功~')
} else {
message.error('操作失败~')
}
} catch (_) {}
}
......
}
fecth
let promise = fetch(url,options)- fetch 不能设置超时时间,不借助插件不能中断请求
- fetch 只要服务器有返回信息(不管http状态码为多少),都说明网络请求成功,最后的promise都是成功的,只有没有任何反馈的(请求中断,超时,断网等)才会是失败,而axios中只有返回的状态码以2开始的才会认为是成功的
- fetch 没有像axios对GET请求参数的处理,我们需要手动拼到url后面
fecth配置项
-
method:请求的方式,默认为get [GET, POST, PUT, DELETE...]
-
mode:请求的模式,默认是cors (设置跨域) [no-cors, cors, same-origin]
-
cache:缓存的模式,默认是default [default, no-cache, reload, force-cache, only-if-cached]
-
credentials:资源凭证, 默认是same-origin [include, same-origin, omit] fetch默认情况下,跨域请求中是不允许携带资源凭证,例如cookie
- include:同源和跨域都允许
- same-origin:只有同源才允许
- omit:都不可以
-
headers:Headers实例,自定义请求头信息
-
body:请求体-只适用于POST,需要设置Content-type
- JSON:application/json
- URLENCODED字符串:application/x-www-form-urlencoded 如:'xxx=xxx&xxx=xxx'
- 普通字符串:text/plain
- FormData对象
- 二进制/Buffer
封装fetch
import qs from 'qs'
import { message } from 'antd'
import utils from '../assets/utils'
const httpParamMethods = ['GET', 'HEAD', 'DELETE', 'OPTIONS']
const httpDataMethods = ['POST', 'PUT', 'PATCH']
const baseURL = '/api'
const http = (config) => {
// 初始化config & 校验config
if (!utils.isPlainObject(config)) config = {}
// initial
config = Object.assign(
{
url: '',
method: 'GET',
credentials: 'include',
headers: null,
body: null,
params: null,
responseType: 'json',
signal: null,
},
config,
)
if (!config.url) throw TypeError('url must be required')
if (!utils.isPlainObject(config.headers)) config.headers = {}
if (config.params !== null && !utils.isPlainObject(config.params)) config.params = null
let { url, method, credentials, headers, body, params, responseType, signal } = config
url = baseURL + url
// 处理params问号传参
if (params) {
url += `${url.includes('?') ? '&' : '?'}${qs.stringify(params)}`
}
// 处理请求体信息 根据后端要求传的格式来,同时设置请求头
if (utils.isPlainObject(body)) {
body = qs.stringify(body)
headers['Content-type'] = 'application/x-www-form-urlencoded'
}
// 请求拦截器
let token = localStorage.getItem('tk')
if (token) headers['authorization'] = token
method = method.toUpperCase()
config = {
method,
credentials,
headers,
catch: 'no-cache',
signal,
}
if (/^(POST|PUT|PATCH)$/i.test(method) && body) config.body = body
return fetch(url, config)
.then((response) => {
// 响应拦截器
let { status, statusText } = response
if (/^(2|3)\d{2}$/.test(status)) {
// 请求状态码为2或3开头成功 -处理返回结果的格式
let result
switch (responseType.toLowerCase()) {
case 'text':
result = response.text()
break
case 'arraybuffer':
result = response.arrayBuffer()
break
case 'blob':
result = response.blob()
break
default:
result = response.json()
}
return result
}
// 状态码失败
return Promise.reject({
code: -1,
status,
statusText,
})
})
.catch((reason) => {
if (reason && typeof reason === 'object') {
let { code, status } = reason
if (code === -1) {
// 状态码
switch (+status) {
case 400:
message.error('参数有误')
break
case 500:
message.error('服务器错误')
break
default:
message.error('网络繁忙,请您稍后再试')
}
} else if (code === 20) {
// 请求中断
message.error('请求中断')
} else {
message.error('网络繁忙,请您稍后再试')
}
} else {
message.error('网络繁忙,请您稍后再试')
}
return Promise.reject(reason)
})
}
httpParamMethods.forEach((item) => {
http[item.toLowerCase()] = function (url, config) {
// 如果不传config则默认为空对象
if (!utils.isPlainObject(config)) config = {}
config['url'] = url
config['method'] = item
return http(config)
}
})
httpDataMethods.forEach((item) => {
http[item.toLowerCase()] = function (url, body, config) {
if (!utils.isPlainObject(config)) config = {}
config['url'] = url
config['method'] = item
config['body'] = body
return http(config)
}
})
export default http
http
.get('/getTaskList', {
params: {
state: 2,
},
})
.then((res) => {
console.log('http', res)
})
document.body.addEventListener('click', () => {
http
.post('/addTask', {
task: 'fetch测试',
time: new Date().toLocaleString('zh-CN', { hour12: false }),
})
.then((res) => {
console.log('http', res)
})
})
Mobx
装饰器
类的装饰器
- 语法
@函数
class Xxx{}
- 创建类的时候,会把装饰器函数执行
-
- target: 当前装饰的这个类的原型,我们可以在装饰器函数中给类设置静态私有属性方法
- 编译后的结果
* var _class
* const test = target => {target.num = 100}
* let Demo = test(_class = class Demo {}) || _class
- 装饰器函数执行返回的结果会替换原有类
- 同一个类可以使用多个装饰器,处理顺序,从下往上执行
const sum = (target) => {
console.log('sum')
target.prototype.sum = function sum() {}
}
const staticNum = (target) => {
console.log('staticNum')
target.num = 10
target.setNum = function setNum(value) {
this.num = value
}
}
/**
* 编译后的结果
* var _class
* let Demo = sum(_class = staticNum(_class = class Demo{}) || _class) || _class
*/
@sum
@staticNum
class Demo {}
console.dir(Demo)
- 可以基于闭包传递不同的值,让装饰器有不同的效果
const test = (x, y) => {
console.log(1)
// 返回的函数是装饰器函数
return (target) => {
console.log(2)
target.num = x + y
}
}
const handle = () => {
console.log(3)
return (target) => {
console.log(4)
target.handle = 'handle'
}
}
/**
* 执行顺序1 3 4 2 先执行外层函数执行,再按从下到上的顺序执行装饰器函数
*/
@test(10, 20)
@handle()
class Demo {}
console.dir(Demo)
属性/方法装饰器
- target Demo.prototype
- name 'x'/'getX'
- descriptor 修饰的属性 {configurable: true, enumerable: true, writable: true, initializer: ƒ} / {writable: true, enumerable: false, configurable: true, value: ƒ}
const readonly = (target, name, descriptor) => {
// 修改属性描述符
descriptor.writable = false
}
// 创建记录执行时间日志的装饰器
const logger = (target, name, descriptor) => {
// 把之前现的getX函数赋值
let oldValue = descriptor.value
// 然后重写d.getX
descriptor.value = function (...args) {
console.time(name)
let res = oldValue.apply(this, args)
console.timeEnd(name)
return res
}
}
class Demo {
@readonly x = 100
@logger
getX() {
return this.x
}
}
let d = new Demo()
// d.x = 200
// Demo.prototype.getX = function() {}
console.log(d.getX())
执行顺序
const A = () => {
console.log(1)
return () => {
console.log(2)
}
}
const B = () => {
console.log(3)
return () => {
console.log(4)
}
}
// 1 - 3 - 4 -2
class Demo {
@A()
@B()
getX() {}
}
返回值
const test = (target, name, descriptor) => {
// 返回值必须是一个规则描述的对象,也就是对name修饰属性/方法的规则描述
return {
enumerable: false,
initializer() {
return 'hello'
},
}
}
class Demo {
@test
x = 100
}
let d = new Demo()
console.log(d)
mobx
js中支持装饰器安装两个插件
npm i @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties
package.json
"babel": {
"presets": [
"react-app"
],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
[
"@babel/plugin-proposal-class-properties",
{
"loose": true
}
]
]
}
基础使用
import { observable, action } from 'mobx'
import { observer } from 'mobx-react'
class Store {
@observable num = 10
@action change() {
this.num++
console.log('num', this.num)
}
}
let store = new Store()
//类组件
@observer
class Demo1 extends React.Component {
render() {
return (
<div>
<span>{store.num}</span>
<br />
<button
onClick={() => {
store.change()
}}>
累加
</button>
</div>
)
}
}
//----------------------------
//函数组件
const Demo1 = observer(function Demo1() {
return (
<div>
<span>{store.num}</span>
<br />
<button
onClick={() => {
store.change()
}}>
累加
</button>
</div>
)
})
export default Demo1
原理
observable:把状态变为可监测的,以后基于autorun/@observer等监测机制才会生效,经过observable处理后的数据,是基于es6中的Proxy做过数据劫持的,后期修改数据,可以在setter中做特殊处理,例如把监听器执行
autorun:首先会立即执行一次,自动建立依赖监测,当依赖的状态值发生改变,callback会重新执行
observe:创建监听器,对对象进行监听,当对象中的某个成员发生改变,出发回调函数执行,前提是对象基于observable修饰
import { observable, autorun, observe } from 'mobx'
class Store {
// 把状态变为可监测的,以后基于autorun/@observer等监测机制才会生效
@observable x = 10
}
let store = new Store()
autorun(() => {
// 首先会立即执行一次,自动建立依赖监测,当依赖的状态值发生改变,callback会重新执行
console.log('自动监测', store.x)
})
let obj = observable({
a: 10,
b: 20,
})
// 经过observable处理后的数据,是基于es6中的Proxy做过数据劫持的,后期修改数据,可以在setter中做特殊处理,例如把监听器执行
console.log(obj) //Proxy
// 创建监听器,对对象进行监听,当对象中的某个成员发生改变,出发回调函数执行,前提是对象基于observable修饰
observe(obj, function (change) {
console.log(change)
})
// observable无法直接修饰原始值,需要通过observable.box,通过get获取值
let x = observable.box(20)
console.log(x.get())
computed
class Store {
@observable x = 10
@observable count = 20
@observable price = 100
// 创建计算属性
@computed get total() {
console.log('run')
return this.price * this.count
}
}
let store = new Store()
autorun(() => {
console.log('总价', store.total)
})
reaction:reaction 与 autorun一样,都是监听器,提供更细腻化的状态检测,默认是不会执行的,自己指定需要监测的状态
reaction(
() => {
store.x, store.total
},
() => {
console.log('reaction', store.x, store.total)
},
)
action:修饰函数的装饰器,他让函数中状态的更改变为异步批处理,bound 保证函数中的this总是store的实例
class Store {
@observable x = 10
@observable count = 20
// 修饰函数的装饰器,他让函数中状态的更改变为异步批处理
@action.bound change() {
this.x = 20
this.count = 30
}
}
let store = new Store()
autorun(() => {
console.log('autorun', store.x)
})
setTimeout(() => {
let func = store.change
func() //没有设置bound this是undefined
}, 2000)
configure:mobx全局配置
// 强制使用action模式修改状态值,直接改会报错 如果不用action可以使用runInAction(()=>{})修改状态
configure({ enforceActions: 'observed' })
React-router-dom
v5
路由组件规则
路由表
路由懒加载
路由信息对象
也能通过hook函数拿到路由参数
在v5版本中 withRouter就是来解决类组件中使用路由参数信息
路由跳转
方案一:Link跳转
<Link to="/xxx">导航</Link>
<Link to={{
pathname:'/xxx',
search:'',
state:{}
}}>导航</Link>
<Link to="/xxx" replace>导航</Link>
方案二:编程式导航
history.push('/c');
history.push({
pathname: '/c',
search: '',
state: {}
});
history.replace('/c');
路由传参
方案一:问号传参
特点:最常用的方案之一;传递信息暴露到URL地址中,不安全而且有点丑,也有长度限制!!
// 传递
history.push({
pathname: '/c',
search: 'lx=0&name=zhufeng'
});
// 接收
import { useLocation } from 'react-router-dom';
let { search } = useLocation();
方案二:路径参数 特点:目前主流方案之一;
// 路由表
{
// :xxx 动态匹配规则
// ? 可有可无
path: '/c/:lx?/:name?',
....
}
// 传递
history.push(`/c/0/zhufeng`);
//接收
import { useRouteMatch } from 'react-router-dom';
let { params } = useRouteMatch();
方案三:隐式传参
特点:传递信息是隐式传递,不暴露在外面;页面刷新,传递的信息就消失了!!
// 传递
history.push({
pathname: '/c',
state: {
lx: 0,
name: 'zhufeng'
}
})
// 接收
import { useLocation } from 'react-router-dom';
let { state } = useLocation();
Link和NavLink
NavLink和Link都可以实现路由跳转,只不过NavLink有自动匹配,并且设置选中样式「active」的特点!!
- 每一次路由切换完毕后「或者页面加载完」,都会拿当前路由地址,和NavLink中的to「或者to中的pathname进行比较」,给匹配的这一项A,设置active样式类!!
- NavLink可以设置 exact 精准匹配属性
- 可以基于 activeClassName 属性设置选中的样式类名
// 结构
<NavLink to="/a">A</NavLink>
<NavLink to="/b">B</NavLink>
<NavLink to="/c">C</NavLink>
// 样式
const NavBox = styled.nav`
a{
margin-right: 10px;
color: #000;
&.active{
color:red;
}
}
`;
v6
不同点
-
移除了Switch:默认就是一个匹配成功就不再继续向下匹配
-
不再需要exact,默认每一项都是精准匹配
-
Redirect -> 代替方案:Navigate:
<Route path="/" element={<Navigate to="/a" replace/>} />- 可以设置replace属性:不会新增记录,替换现有记录
- to的值可以是一个对象
-
withRouter -> 自己实现高阶组件
-
每一条Route规则都,不再基于component或render,而是element,语法格式
element={<Component/>} -
新增路由容器:
Outlet,用来渲染二级多级路由匹配,不用像5版本一样二级路由写到各个组件中,可以统一管理 -
路由跳转
- Link/NavLink
- 编程式导航:取消了history对象,通过hook函数:
import { useNavigate } from 'react-router-dom';
App.jsx
import React from "react";
import { HashRouter, Routes, Route, Navigate } from 'react-router-dom';
import HomeHead from './components/HomeHead';
/* 导入需要的组件 */
import A from './views/A';
import B from './views/B';
import C from './views/C';
import A1 from './views/a/A1.jsx';
import A2 from './views/a/A2.jsx';
import A3 from './views/a/A3.jsx';
const App = function App() {
return <HashRouter>
<HomeHead />
<div className="content">
<Routes>
{/* 一级路由 「特殊属性 index」*/}
<Route path="/" element={<Navigate to="/a" />} />
<Route path="/a" element={<A />} >
{/* 二级路由 */}
<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" element={<C />} />
<Route path="*" element={<Navigate to={{pathname:'/a',search:'?lx=404'}} />} />
</Routes>
</div>
</HashRouter>;
};
export default App;
A.jsx
import { Link, Outlet } from 'react-router-dom';
...
const A = function A() {
return <DemoBox>
...
<div className="view">
<Outlet />
</div>
</DemoBox>;
};
export default A;
跳转及传参
// C组件的路由地址
<Route path="/c/:id?/:name?" element={<C />} />
/* 跳转及传参 */
import { useNavigate } from 'react-router-dom';
const B = function B() {
const navigate = useNavigate();
return <div className="box">
B组件的内容
<button onClick={() => {
navigate('/c');
navigate('/c', { replace: true });
navigate(-1);
navigate({
pathname: '/c/100/zxt',
search: 'id=10&name=zhufeng'
});
navigate('/c', { state: { x: 10, y: 20 } });
}}>按钮</button>
</div>;
};
export default B;
/* 接收信息 */
import { useParams, useSearchParams, useLocation, useMatch } from 'react-router-dom';
const C = function C() {
//获取路径参数信息
let params = useParams();
console.log('useParams:', params);
//获取问号传参信息
let [search] = useSearchParams();
search = search.toString();
console.log('useSearchParams:', search);
//获取location信息「pathname/serach/state...」
let location = useLocation();
console.log('useLocation:', location);
//获取match信息
console.log('useMatch:', useMatch(location.pathname));
return <div className="box">
C组件的内容
</div>;
};
export default C;
路由表及懒加载
import React, { Suspense } from "react";
import { Route, Routes, useNavigate, useParams, useSearchParams, useLocation, useMatch } from 'react-router-dom';
import routes from "./routes";
// 渲染内容的特殊处理
const Element = function Element(props) {
let { component: Component, path } = props,
options = {
navigate: useNavigate(),
params: useParams(),
query: useSearchParams()[0],
location: useLocation(),
match: useMatch(path)
};
return <Component {...options} />;
};
// 递归创建路由规则
const createRoute = function createRoute(routes) {
return <>
{routes.map((item, index) => {
return <Route key={index} path={item.path} element={<Element {...item} />}>
{item.children ? createRoute(item.children) : null}
</Route>;
})}
</>;
};
// 路由表管控
const RouterView = function RouterView() {
return <Suspense fallback={<>正在加载中...</>}>
<Routes>
{createRoute(routes)}
</Routes>
</Suspense>;
};
export default RouterView;
router/routes.js
import { lazy } from 'react';
import { Navigate } from 'react-router-dom';
import A from '../views/A';
import aRoutes from './aRoutes';
const routes = [{
path: '/',
component: () => <Navigate to="/a" />
}, {
path: '/a',
name: 'a',
component: A,
meta: {},
children: aRoutes
}, {
path: '/b',
name: 'b',
component: lazy(() => import('../views/B')),
meta: {}
}, {
path: '/c',
name: 'c',
component: lazy(() => import('../views/C')),
meta: {}
}, {
path: '*',
component: () => <Navigate to="/a" />
}];
export default routes;
router/aRoutes.js
import { lazy } from 'react';
import { Navigate } from 'react-router-dom';
const aRoutes = [{
path: '/a',
component: () => <Navigate to="/a/a1" />
}, {
path: '/a/a1',
name: 'a-a1',
component: lazy(() => import('../views/a/A1')),
meta: {}
}, {
path: '/a/a2',
name: 'a-a2',
component: lazy(() => import('../views/a/A2')),
meta: {}
}, {
path: '/a/a3',
name: 'a-a3',
component: lazy(() => import('../views/a/A3')),
meta: {}
}];
export default aRoutes;
App.jsx
import React from "react";
import { HashRouter } from 'react-router-dom';
import HomeHead from './components/HomeHead';
import RouterView from "./router";
const App = function App() {
return <HashRouter>
<HomeHead />
<div className="content">
<RouterView />
</div>
</HashRouter>;
};
export default App;