React基础知识

114 阅读16分钟

jsx语法

JSX:javascript and xml(html)

  • 最外层只能有一个根元素节点

  • <></> fragment空标记,即能作为容器把一堆内容包裹起来,还不占层级结构

  • 动态绑定数据使用{},大括号中存放的是JS表达式

    • 可以直接放数组:把数组中的每一项都呈现出来
    • 一般情况下不能直接渲染对象
    • 但是如果是JSX的虚拟DOM对象,是直接可以渲染的
  • 设置行内样式,必须是 style={{color:'red'...}}

  • 设置样式类名需要使用的是className

  • JSX中进行的判断一般都要基于三元运算符来完成

  • JSX中遍历数组中的每一项,动态绑定多个JSX元素,一般都是基于数组中的map来实现的

    • 和vue一样,循环绑定的元素要设置key值(作用:用于DOM-DIFF差异化对比)
  • 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,插入到容器中

props

调用组件,传进的props被冻结了,是只读的,组件内修改props会报错 作用:父组件调用子组件时,可以基于属性把信息传递给子组件,让子组件呈现不同的效果

关于对象的规则设置

  1. 冻结:被冻结的对象不能修改成员值,不能新增,删除成员,不能给成员做劫持(Object.defineProperty)

    1. 冻结对象Object.freeze(obj)
    2. 检测对象是否被冻结Object.isFrozen(obj)
  2. 密封:被密封的对象不能新增,删除成员,但是可以修改成员值,也可以做劫持

    1. 密封对象:Object.seal(obj)
    2. 检测是否被密封:Object.isSealed(obj)
  3. 不可拓展:不能新增成员

    1. 不可拓展对象:Object.preventExtensions(obj)
    2. 检测是否不可拓展: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

当单闭合调用或没有传子节点,childrenundefined

当传了子节点,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.Childrenprops.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
  • 触发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.setStateforceUpdate更新视图,类组件具备:属性,状态,周期函数,ref...而函数组件里只具备属性

PureComponent

PureComponentComponent的区别

  • 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
  // 改变三个状态,视图只刷新一次
}

1697115522856.png

1697116075573.png

1697287786031.png

  • 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,
      }
    })
  }

1697291921633.png

合成事件

绑定方式

基于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>

1697342002619.png

1697342411336.png

样式私有化

行内样式

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

image-20231110161843285.png

  • 在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(基于更新队列)实现状态批处理

image-20231113094534507.png

  • 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>
  )
}

image-20231113095843335.png

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>
  )
}

image-20231113111940195.png

useLayoutEffect

  • 如果链表中的callback执行又修改了状态值[视图更新],对于useEffect来说:第一次真实dom已经渲染,组件更新会重新渲染真实dom,所以频繁修改状态时,会出现样式/内容闪烁
  • 对于useEffect来讲:会阻塞浏览器渲染真实dom,优先执行effect链表中的callback,等到callback执行完毕,再合并渲染一次真实dom
  • useLayoutEffect的callback 要优先于useEffect 中的callback执行
  • 在两者的callback中都可以获取到dom元素[原因:真实dom已经创建,区别只是浏览器是否渲染]

视图更新的步骤

  1. 编译jsx为createElement格式

  2. 执行createElement创建虚拟dom

  3. 执行render创建真实dom(diff)

    • useLayoutEffect会阻塞第四步操作,先去执行effect链表中的方法(同步执行)
    • useEffect不会阻塞浏览器渲染,callback和浏览器渲染时同时进行的(异步操作)
  4. 浏览器渲染绘制真实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如果数组不传参,则永远都是第一次创建时的函数闭包作用域,其函数内部的状态引用也永远是第一次渲染时的状态

image-20240131105418316.png

父子组件通信(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

1708307821417.png

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

基础用法

  1. 创建store,规划reducer中的acticon[type]行为
  2. 在入口中,基于上下文对象,把store放入上下文中,需要用到store的组件,从上下文中获取状态
  3. 组件中基于store.getState()获取公共状态,基于store.dispatch(action)完成任务派发

使用公共状态的组件,必须基于store.subscribe()向事件池中加入让组件更新的方法,公共状态改变触发事件让组件更新,从而获取最新的状态进行绑定

image-20240201171555995.png

// 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

image-20240202175634906.png

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

1708244632295.png

路由组件规则

1708244805511.png

路由表

1708245933046.png

路由懒加载

1708246393781.png

1708246698570.png

路由信息对象

1708247055389.png

1708247090172.png

也能通过hook函数拿到路由参数

1708247132140.png

1708247365925.png

1708247456794.png

在v5版本中 withRouter就是来解决类组件中使用路由参数信息

1708247547994.png

1708247654396.png

路由跳转

方案一: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';

1708255361515.png

1708256399407.png

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;