React B站学习杂记

学习来源 B站尚硅谷的课程

react

概述

用于构建用户界面的js库

  1. react库的组成
  • 核心库:
    react.js -- react 核心库
    react-dom.js -- 用于react操作DOM
    babel.min.js -- 用于将jsx转化为浏览器可识别的js语言

  • 额外库:
    prop-types -- 用于验证props属性,显示在log内给与开发者使用,通过静态引入,或者npm包安装

  • 查看react版本号

npm info react
  • 查看react脚手架版本号
create-react-app --version
create-react-app -V

0-1. JSX语法
jsx语法是 一种react描述 组件html结构的开发语法
主要要求如下:

  • 定义DOM元素时候,不能加引号
  • 标签内混入JS表达式要用 { }
  • 样式类名不能使用class关键字,而是使用className
  • 不管是函数组件还是类组件,返回jsx结构只能有一个根标签
  • 标签不管是双标签闭合,还是自闭合,必须闭合
  • 组件标签 必须使用大写字母开头
  • 小写字母开头标签,会自动查找html对应标签,如果没有就会报错
  • 区别与传统js,事件使用驼峰命名,onclick => onClick
  1. prop-types 通过静态引入


类组件三大属性 props 限制

类组件三大属性 props 简写
  1. propTypes 通过包安装
import PropTypes from 'prop-types';
//接收props组件内
export default class MyCmp extends Component {
  static propTypes = {
    xxx: PropTypes.func.isRequired   // 传递至MyCmp组件上的属性xxx 类型为函数,且必填
  }
  static defaultProps = {
    xxx: 123                                        //属性缺省值配置
  }
}
  1. 函数式组件
    定义函数返回一个结构,然后将这个结果的jsx,返回的虚拟DOM转化为真实DOM,渲染至容器DOM节点上
function Cmp1(){
  return 

我是一个组件

} ReactDOM.render(,document.getElementById('root'));

3-1. 函数式组件使用props

function Person(props){
    console.log(props)
    return 
hello world 类型{typeof props.myname} - {props.myname}
} ReactDOM.render( , document.getElementById('root') );

HOOKS

  • react 16.8 推出的语法新特性
  • 常用的hook有:
react.useState  --  赋予函数式组件操作state的能力
react.useEffect  --  赋予函数式组件使用生命周期 ( componentDidMount, componentDidUpdate, componentWillUnmount )
react.useRef -- 赋予函数式组件操作元素节点的能力

3-2. 通过react.useState的hook,使得函数式组件拥有操作state的能力

import React from 'react'
import ReactDOM from 'react-dom'

function Funcmp(props){
    //格式为数组,结构后,第一项为state,第二项为方法
    
    // 修改一般数据
    let [ num,changeNum ] = React.useState({ num:0 })
    let [sName,changeName] = React.useState('zhangsan'); 
  
    //方式一,对象式修改数据方法
    function addFn(){
      changeNum(num+1);
    };

    //修改对象数据
    let [sNum,changeNum] = React.useState({num1:1,num2:1});

    function changeNumFn1(){
        //调用操作state的方法
        changeNum({...sNum,num1:sNum.num1+1});
    
        //方式二,函数式来修改数据方法
        //函数的参数就是 state数据
        changeNum((data)=>{
           // console.log(data);   //{num1: 1, num2: 1} 
           return {...data,num2:data.num2*2}
        });
    };

    function changeNumFn2(){
        //调用操作state的方法
        changeNum({...sNum,num2:sNum.num2*2});
    };

    return (
        
当前的数值 { num } , { sNum.num1 }--{ sNum.num2 }
) };

3-3. 通过react.useEffect的hook,使得函数式组件拥有操作生命周期的能力
语法结构参数不同,可能出现的生命周期

react.useEffect(()=>{
   console.log(1111);

   return (()=>{
      //此处为componentWillUnmount
   })
},[ ])
  • 如果react.useEffect的第二个参数为空数组,则函数内1111只会触发一次,此处为componentDidMount
  • 如果react.useEffect的第二个参数没有,则函数内1111会触发1+N次,此处为componentDidMount+componentDidUpdate
  • 如果react.useEffect的第二个参数为数组内有一项或以上,则针对这些项,为componentDidMount+componentDidUpdate
    针对这些项目以外的项,为componentDidMount
  • react.useEffect第一个参数的返回的函数,为componentWillUnmount

3-4. 通过React.useRef 赋予函数式组件,通过ref获取DOM节点的能力

import React from 'react'

export default function Funcmp3() {
    //设置state数据
    let [inputVal,changeMes] = React.useState('');
    let inputRef = React.useRef();
    //通过ref获取DOM元素的value值
    function showMes(){
        console.log(inputRef.current.value);
    }
    //表单改变state
    function changeMesFn(event){
        changeMes(event.target.value)
    }
  
    return (
        
) }
  1. 类组件
// 类组件继承了React组件上的属性与方法,其中三大属性为 state props refs
class MyCmp extends React.Component {
 render (){
    return ( 

我是类组件

) } } ReactDOM.render(,document.getElementById('root'))
类组件 三大属性

4-2. 关于类组件的一些注意事项

  • 组件中的render方法内的,this指向为组件实例
  • 组件自定义方法中this为undefined,如何解决
    方法1: 通过在constructor内,通过bind修改this的指向
    方法2: 在constructor外,通过变量和箭头函数的方式,修改this指向
  • 状态数据,不能直接修改或者更新, 如:this.state.xxx = xxx, 必须要使用 setState方法,如果修改数据为对象要创建新对象(object、array),详见 下面第7条
  1. 类组件 三大属性 state
  • 什么情况下使用state, 类组件如果需要维护一个自身的状态,可以使用,函数组件没有这个属性
  • 复杂的类组件可以使用, 简单的类组件不需要一个state
  • state定义的位置
    方式1:可以在constructor内定义,如:
class MyCmp extends React.Component {
  constructor(props){
     super(props)
     this.state  = {  somedata: 100  }
  }
  render (){
    let {  somedata  } = this.state;
    return ( 

我是类组件 数据为 { somedata }

) } }

方式2:在contructor外定义,如:

class MyCmp extends React.Component {
  //contructor 不是必写
  state  = {  somedata: 100  }
  render (){
    let {  somedata  } = this.state;
    return ( 

我是类组件 数据为 { somedata }

) } }
  • 修改state,不能直接赋值,而是需要调用setState方法
  • 通过事件,如onClick调用一个事件处理函数,来调用setState方法
  1. 关于事件处理函数放置的位置也存在两种
    方式一,放在constructor内,值得注意的是需要修改this指向
constructor(props){
   super(props)  //写constructor必须写super(props)
   this.eventHandle = this.eventHandle.bind(this);
}
eventHandle(){
  dosomething...
}

方式二,放在constructor外面 写成变量赋值箭头函数的形式,this会因为箭头函数自动指向实例对象

constructor(props){
  super(props)
  ...
}
eventHandle = () => { 
  dosomething...
}
  1. 关于setState的注意事项
    **React遵循函数式编程规范。在函数式编程中,不推荐直接修改原始数据。 **
    如果要改变或更改数据,则必须复制数据副本来更改。
    所以,函数式编程中总是生成原始数据的转换副本,而不是直接更改原始数据。
  • 纯函数的特点:
    第一,同一个输入对应同一个输出
    第二,函数内部不能使用不可预测的操作,如 网络请求、输入输出设备
    第三,函数内部不能使用 Date.now() 与 Math.random()

  • 什么情况下,需要在组件内使用state?
    某一个组件自己使用的,则state放在这个组件下
    某一些组件用一个数据,则state放在它们共同的父级 (官方称之为 状态提升

  • 要修改的state对象下,如果键名对应键值为一个字符串,可以直接通过setState修改
    如:

class xxx extends React.Component {
state = { isActive: false }  
...
mouseEnterFn = ()=> {
    this.setState({
        'isActive': true
    })
}
}
  • 要修改的state对象下,如果键名对应键值为一个对象(object,array),那么不能直接修改原来的数据,而要生成一个新的对象
  • 原理:
    react底层进行了一个浅拷贝,对比前后修改的对象是否是同一个对象(至于对象内的值是否改变无关紧要), 只关心引用地址值

如 数组的修改:

class xxx extends React.Component {
    state = {
      list:[]
    }
    changeList = ()=> {
        let dataArr = this.state.list;
        let NewList = [];
        //方式一,通过concat返回一个新数组,而不能直接用push等方法直接在dataArr上操作
        NewList = dataArr.concat([xxxx])
        //方式二,通过展开运算符来返回新数组,并按照意愿调整新增数据的位置(插入第一个,或者插入最后一个)
        NewList = [xxxx , ...dataArr]
        NewList = [...dataArr , xxxx]
        //方式三,通过数组map方法返回新数组,一般用于处理数据或者筛选数据
        NewList = dataArr.map((item)=>{ return item*2; })
        NewList = dataArr.map((item)=>{ if(item%2==0){ return item; } })
        //方式四,通过filter来返回新数组 
        //注意:filter() 方法创建一个新的数组; filter() 不会对空数组进行检测; filter() 不会改变原始数组;
        NewList = dataArr.filter((item)=>{ return item%2==0; })
        //方式五,forEach遍历原来数组,根据条件,将筛选项插入新数组内
        dataArr.forEach((item)=>{  if(item%2==0){ NewList.push(item); } })
        
        this.setState({
            list: NewList 
        })
    }
}
  • 要修改的state对象下,如果键名对应键值为一个对象(object,array),那么不能直接修改原来的数据,而要生成一个新的对象
    如 对象的修改:
class xxx extends React.Component {
    state = {
      obj: { a:1, b:2 } 
    }
    changeList = ()=> {
        let dataObj = this.state.obj;
        //方式一,通过assign返回一个新的对象( 属于浅拷贝 ),并修改对象内的数据
        dataObj = Object.assign({ },dataObj,{a:10,b:20})
        //方式二,展开运算符拷贝和修改数据
        dataObj = { ...dataObj, a:10 }
        
        this.setState({
            obj: NewObj
        })
    }
}
  • setState是异步的,设置state是同步操作,但是渲染是异步操作
    通过setState的第二个参数可以获取最新的state数值,这个函数会在页面render后触发
import React, { Component } from 'react'
class Count extends Component {
  state = {
    num:0
  }
  addNum = () =>{
    var oldNum = this.state.num;
    this.setState({
      num: oldNum+1
    },function(){
      console.log(this.state.num); //1
    })
    console.log(this.state.num);  //0
  }
}
  • setState不仅仅可以接收对象,也可以接收函数
    setState函数形式的格式: this.setState(( oldState )=>{ return xxxx })
    此外这种写法也支持第二个参数,即render后的callback函数
    对象形式写法是函数形式写法的语法糖
  //-- 对象形式
  this.setState({
    num: this.state.num+1
  });

  //-- 函数形式 (最终函数return出来的依然是一个对象)
  this.setState((oldstate)=>{
    return { num: oldstate.num+1 };
  },function(){
    console.log(this.state.num);
  })

写法推荐的建议:
如果不需要依赖之前的值,优先写对象形式
如果依赖之前的值,可以写函数形式,这样之前的值就不用再保存到一个变量内了,直接从函数参数里拿即可

  1. 类组件 三大属性 props
  • props - 通过组件标签挂载的属性,使得父组件可以将数据或者方法传递至子组件中
    子组件通过 this.props 来获取父级组件的属性以及方法
  • 组件之间通信的方式之一
  • 函数类型组件也是可以获取到props的
function Person(props){
    console.log(props)
    return 
hello world 类型{typeof props.myname} - {props.myname}
} ReactDOM.render( , document.getElementById('root') );
  • react-router-dom,操作路由 其中
    this.props.match.params -- params 风格理由配置
    this.props.history.search -- query 风格 路由配置
    this.props.history.state -- state 风格 理由配置
    都是挂载在props属性上面
  1. 类组件 三大属性 refs
    用来获取元素节点
    一共有四种方式来获取元素节点
  • 方式一:
    字符串类型获取 (存在效率问题,未来可能会被移除,官方不建议使用)
class xxx extends React.Component {
  doSearch = ()=> {
    let _searchInput = this.refs.searchInput;
    console.log(_searchInput.value);
  }
  render (){
    return 
  }
}
  • 方式二:
    ref等于函数,在标签内写行内函数,函数参数为当前DOM节点,通过箭头函数,将this指向组件实例
class xxx extends React.Component {
  doSearch = ()=> {
    let _searchInput = this.searchInput;
    console.log(_searchInput.value);
  }
  render (){
    return { this.searchInput = currentNode}} placeholder="输入搜索内容" />
  }
}
  • 方式三:
    ref等于函数,不将函数写在行内,而是在组件内定义,函数的参数为操作的DOM节点
class xxx extends React.Component {
  doSearch = (currentNode)=> {
    this.searchInput = currentNode;
    console.log(this.searchInput.value);
  }
  render (){
    return 
  }
}

※ 相较于行内方式,更新数据会被调用两次,第一次用于重置ref标签数据为null,第二次为正常获取DOM节点
※ 官方的描述,这个属于正常现象,写内联箭头函数,或者是调用类组件内的函数,都属于正常现象
※ 标签内写箭头函数还是一种主流方式

  • 方式四:
    ※ 通过类上挂载属性,通过React.createRef聊创建容器,用来储存ref的元素节点
    ※ 容器为元素一一对应,不能混用
    ※ 流程:创建ref数据容器,元素标签上ref绑定这个容器,
class xxx extends React.Component {
  searchInputRef = React.createRef()
  doSearch = ()=> {
    console.log(this.searchInputRef.current.value);
  }
  render (){
    return 
  }
}
  1. 绑定事件的一些注意点
  • 更好的兼容性:事件是react合成的自定义事件,为了更加好的浏览器兼容性,例如: onclick -> onClick
  • 更加高效:react通过事件委托的方式,将绑定事件元素,提升到父级容器
  • 不能过渡使用ref,如果可能,能不用就不用
    例如:
    获取表单的数据,不要使用ref获取表单元素节点,而是通过事件的event对象,获取表单内容
    受控组件 -> 非受控组件
class xxx extends React.Component {
  doSearch = (event)=> {
    console.log(event.target.value);
  }
  render (){
    return 
  }
}
  1. 关于事件处理函数传参的问题
    原理:
    ※ 通过函数柯里化,或者说是高阶函数,一个函数执行后返回另一个函数,来实现参数传递
    react函数如果带参数,会立即执行,这个不是react要的,它需要一个函数,
    因此借助高阶函数来实现函数传参

※ 常见的高阶函数有:promise、setTimeout、array.map等

方式一,标签内部自执行一个匿名函数,并将参数event,传递至下一个函数

class xxx extends React.Component {
  doSearch = (event,data)=> {
    console.log(event.target.value);
    console.log(data);  //somedata....
  }
  render (){
    return { this.doSearch(event,'somedata....') }} placeholder="输入搜索内容" />
  }
}

方式二,正常调用函数,在函数内部,通过箭头函数自执行,返回对应的event

class xxx extends React.Component {
  doSearch = (data)=> {
    return (event)=>{
      console.log(event.target.value);
      console.log(data);  //somedata....    
    }
  }
  render (){
    return 
  }
}
  1. 受控组件与非受控组件
    简而言之就是输入框,没有绑定onChange或onInput、onKeyUp等类似控制
    以复选框为例
   
//如果没有绑定事件onChange存在,那么它将不能改变
//react会建议使用defaultChecked 代替 checked,但它仅会在页面渲染的第一次受到数据影响
//Warning: You provided a `checked` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultChecked`. Otherwise, set either `onChange` or `readOnly`.

改写为


  1. 生命周期


    react旧的生命周期

    生命周期(旧的)

    生命周期(旧的) 总结
  • 一些概念:
    挂载组件(mount),卸载组件(unmount)
  • 生命周期调用次数:
    componentDidMount - 组件挂载,调用1次
    componentWillUnmount - 组件更新,调用N次
  • 一个组件开启定时器与关闭定时器的时机
constructor(props){
    super(props)
    this.state = { ... }
}
componentWillMount(){
    //组件将要挂载....
}
render(){
    return (
        
...
) } componentDidMount(){ this.iTmer = setInterval(()=>{ ... },1000); } componentWillUnmount(){ clearInterval(this.iTimer); } shouldComponentUpdate(){ //需要返回布尔值,组件是否允许更新 return true } componentWillUpdate(){ } componentDidUpdate(preProps,preState){ //preProps 更新之前的props //preState 更新之前的state }

13-1. react新旧版本下,生命周期的差异


生命周期react在18大版本会报错,需要注意

新旧生命周期差异
  • 即将废弃:
componentWillMount          -> 使用需要加上 UNSAFE_ 前缀
componentWillReceiveProps   -> 使用需要加上 UNSAFE_ 前缀
componentWillUpdate         -> 使用需要加上 UNSAFE_ 前缀    
  • 新增:
getDerivedStateFromProps    
    -> 静态方法 前面需要加上 static 
    -> 通过组件标签挂载属性,在这个生命周期参数可以获取,返回值可以左右组件内的state
    -> 必须有返回值,可以返回null
    -> 会导致代码冗余、会使组件难以维护
    -> 若state值都取决于props,可以使用这个生命周期
getSnapshotBeforeUpdate
    -> 可以带两个参数 prevProps, prevState     
    -> 必须有返回值,可以为null; snapshot值可以为任意,会传递至 componentDidUpdate
    -> 会将至传递给 componentDidUpdate的第三个参数
    -> 使用场景极其罕见
  1. 虚拟DOM与DIFF
    ※ key的意义?
    react数据更新 新的虚拟DOM与旧的虚拟DOM,通过DIFF算法进行比对,针对变化的地方进行更新,提高DOM复用
    有key的时候,diff会优先,对相同结构树下同key结构进行比对
    如果没有key,判断是新元素,会重新创建DOM元素,降低性能与复用率

※ 使用索引作为key有什么后果?

  • 如果单纯的展示,且新插入数据为正序排序(新插入数据在后面),没有影响排序的行为,则没有问题
  • 如果新数据插入在列表最前面,会影响所有列表项,所有的元素都会重新创建,且可能造成布局错乱
虚拟DOM在diff中key的意义

※ 常规做法:

  • 使用唯一id作为key,而不是索引
  • 使用三方包,如 nanoid,来生成唯一的key
npm install nanoid -S

import {nanoid} from 'nanoid'
//返回为函数,调用即可
console.log(nanoid());
  1. 组件之间通信手段
    ※ 方式一:props传递函数
    父组件通过向子组件上挂载方法,子组件通过props来获取方法,并调用函数,将传递数据作为参数传递至父组件

父组件:

fatherFn = (sonData)=> {
console.log('子组件数据:', sonData); //123
}

render() {
  return (
    
{/* 顶部 一般组件 */}
) }

子组件:

render(){
    let {fatherFn} = this.props;

    return (
        
{/* 注意传参要包裹一个自执行匿名箭头函数,否则返回的不是函数,因为react会默认调用带()的函数 */}
) }

※ 方式二:第三方包 'pubsub-js' ,通过发布与订阅来实现任意位置组件之间通信
安装
npm install pubsub-js -S

组件订阅(位置:生命周期的组件挂载)

componentDidMount(){
    //订阅信息
    this.subscribeObj1 = PubSub.subscribe('mydata', function(msg, data){
        console.log( msg, data ); 
        // msg - 'mydata' , data - '发布的一段信息...'
    });
}

组件解绑订阅(位置:生命周期的组件卸载)

componentWillUnmount(){
    //取消订阅
    PubSub.unsubscribe(this.subscribeObj1);
}

组件发布 位置

//发布一个信息
pubFn = ()=>{
    PubSub.publish('mydata', '发布的一段信息...');
}
render(){
  return (
    
{/* 发布一个信息 */}
) }

※ 方式三:redux、react-redux 管理数据共享
redux 第三方数据状态管理
react-redux 官方出品,与第三方包redux 搭配使用
见 (下面 ↓) redux与react-redux部分

※ 方式四:通过React.createContext来创建上下文
这个目前学习感觉像一种单向的数据通信,方便祖辈传递信息和同步信息至子孙组件

第一步,创建一个Context文件夹用于创建上下文的文件。

import React from 'react'
//这个是一个上下文的对象,名称为'MyContext'
export const MyContext = React.createContext();
export const { Provider,Consumer } = MyContext;

第二步,在祖先组件中使用Provider包裹需要祖先数据的组件

import { Provider } from './components/Context'
传递数据至Head组件

    

特别注意:

  • Provider上的value属性名称,是固定的不能修改,否则报错 Did you misspell it or forget to pass it?

第三步,方法1 - 在后代组件中获取传递的数据

import { Consumer } from './components/Context'

export default function Funcmp3() {
    return(
    

{ (value999999)=>{ return `姓名:${value999999.name} - ${value999999.age}` } }

) }

注:

  • 在Consumer内的参数可以自行定义,不用担心报错,如上‘value999999’
  • 使用Consumer既可以在函数式组件,也可以在类组件内使用

第三步,方法2 - 仅子组件为类组件时可以使用

//引入上下文对象
import { MyContext} from '../Context'

export default class Footer extends Component {
    //表名该组件需要通过上下文接收数据
    static contextType = MyContext

    render(){
      return (
        

姓名:{this.context.name} - {this.context.age}

) } }

注:

  • 类组件内 添加静态属性,标明需要接收上下文数据 static contextType = MyContext
  • 在实例的context上获取传递的数据

react-cli
react 快速开发脚手架,包含 语法检查、jsx编译、devserver热更新等..

  • 安装脚手架
    npm install create-react-app -g
  • 创建项目
    create-react-app 项目名称
  • 启动项目
    npm start
  1. 模块化引用样式,防止样式污染
//index.css  ->  index.module.css --- 固定的格式变化
import css1 from './index.module.css'

export default class MyCmp extends Component {
  render(){
    return (
      

标题

) } }
  1. 关于调试项目,如何使用代理,调用远程服务器接口
    ※ 方式一:
    在package.json中添加配置
"proxy": "http://localhost:5000"  // 一个远程服务器的地址,与你的本地服务器存在跨域

※ 方式二:
在src文件夹内,创建一个新文件 setupProxy.js

//内置包,无需安装
const proxy = require('http-proxy-middleware');
module.exports = function(app){
    app.use(
        proxy('/api',{  //规则关键词,如果存在接口 /api/list
            target:'http://localhost:5000',  //请求后台服务器接口的地址
            changeOrigin: true,  //是否伪装地址,如果不伪装则为当前静态页面的地址,否则为 上面请求服务器的地址
            pathRewrite: {'^/api':''}  //请求服务器接口会替换为 http://localhost:5000/list
        })
    )
}

上面代理配置完了,前端页面请求数据

getDetalFn = (id)=> {
    //请求数据
    let that = this;
    
    //请求地址不能写远程地址,要写本地服务器地址
    //真实服务器接收到数据的request.path为  http://localhost:5000/detail/xxx ,将规则标识api替换为空
    axios.get(`http://localhost:3000/api/detail/${id}`)
    .then(function (response) {
        let _cont = response.data;
        that.setState({
            'cont':_cont
        })
    })
    .catch(function (error) {
        // handle error
        console.log(error);
    })
}

相对于之家在package.json内配置代理,可以配置多个代理,规定代理的触发关键词

react-router

  1. 安装
    npm install react-router-dom -S

  2. 概念

  • 单页面应用 (SPA - single page web application)
  • 这个页面切换hash或者切换history(H5),来调用不同的组件,渲染更新页面上的不同区域
  • 数据通过ajax请求获取,不刷新页面
  1. 路由的理解
  • 路由是路径与组件的一个映射关系
  1. 路由的分类
  • 后端路由
    router.get(path,function(req,res){ ... })
  • 前端路由
    react中,切换path显示component,
  1. 路由内的组件
  - 基础的路由按钮
  - 可以有激活状态的路由按钮,默认有一个active 类名,可以通过activeClassName配置
  - 路由配置,映射理由与组件的关系
 - 匹配第一个相符的路由映射,对于后面的不再匹配筛选
  - 重定向,放在路由配置的最下面,作为逻辑兜底
 - history风格路由
 - hash路由

4-2. BrowserRouter与HashRouter的区别:

  • 底层原理:
    BrowserRouter 是H5的history API,不兼容IE9及以下 ( 提出业务需求时候,明确低版本浏览器不支持 )
    HashRouter 是使用URL的哈希值 (#后的部分,不会发送至服务器)

  • path表现形式不一样
    BrowserRouter 是 localhost:3000/test
    HashRouter 是 localhost:3000/#/test

  • 浏览器刷新
    BrowserRouter 没有任何影响,因为state存在history内
    HashRouter 刷新导致state参数丢失

  • HashRouter 可以解决引用静态文件路径错误问题

建议:
优先考虑 BrowserRouter (不考虑浏览器兼容的情况下)
如果使用 HashRouter 会造成state方式配置路由错误,因为没有底层history记录state数据,刷新为undefined,可以使用params或search风格路由

  1. 配置路由
  • 路由的设置
import {NavLink,Link,Route,Switch} from 'react-router-dom';
import About from './compontents/About'
import News from './compontents/News'

关于
新闻


    {/* About */}
    
    {/* News */}
    
    {/* 默认路径,兜底 */}
    

  • 路由方式的设置,在入口文件中
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom'
import App from './App';

ReactDOM.render(
  
    
  ,
  document.getElementById('root')
);
  1. 路由嵌套
  • 路由的匹配原则,优先匹配第一层路由,然后逐层匹配
{/* 二级路由 */}
热点新闻 今日新闻
{/* 呈现区域 */}
  1. 路由的细节
  • 严格模式(精准匹配)
    会影响路由嵌套

  • path可以被to包含,但是不能超过,否组件将无法被渲染

  • 可以渲染组件的情况:

home页面

  • 不能渲染组件的情况
home页面

结论: to可以包含path,且必须开头匹配,如果不匹配就失败了,这个方式属于模糊匹配

  1. 路由传参

※ 方式一 search方式
调用:


配置:


//--编程式导航
this.props.history.push({ path:'/news/today',query:{id:123} });

获取:

this.props.location.search

注:需要借助于querystring来解析地址,删除首字母?,并使用querystring.parse()来将字符串转化为对象

※ 方式二 params方式
调用:


配置:


//--编程式导航
this.props.history.push( '/news/today/123' );

获取:

this.props.match.params

※ 方式三 state方式
调用:


配置:


//--编程式导航
this.props.history.push({ path:'/news/today',state:{id:123} });

获取:

this.props.location.state

注:刷新也可以保留参数,但是如果清空浏览器缓存,state会消失,需要考虑调用undefined下属性的情况

路由组件,一般组件

7-1. 路由组件与一般组件

  • 路由组件不是自己通过标签写入页面,而是通过路由配置,让路由自己去调用
  • 一般组件需要自己去引用组件,添加到页面,无法调用this.props.history等
  • 一般组件转化为路由组件,通过withRouter包裹即可
import {withRouter} from 'react-router-dom'
class Head extends Component {
  ...
}
export default withRouter(Head);
  1. 使用params进行路由传参时候,会遇到一个情况,就是无限调用render
    情景:
    列表菜单与列表详情在同一个页面情况下,
    根据params传递参数ID,componentDidMount来请求服务器,获取数据,然后进行setState操作,
    如果点击列表菜单,再次进行操作,无限陷入render情况
    解决:
    componentDidUpdate生命周期,进行前后params的id比对,不同情况下,再次进行数据请求
componentDidUpdate(prevProps, provState, snapshot){
  let {id} = this.props.match.params;
  if(prevProps.match.params.id!==this.props.match.params.id){
    this.getDetalFn(id);
  };
}

原因:
componentDidMount调用函数中setState触发了更新componentDidUpdate,更新componentDidUpdate调用setState调用函数中setState又触发了componentDidUpdate,进而componentDidUpdate无限自己调用自己

  1. 封装一个NavLink,将常用的属性封装组件内,放置反复写
  • 定义:
import React, { Component } from 'react'
import {NavLink} from 'react-router-dom'
import './index.css'

{/* 注意  {...this.props} 放在默认属性的右侧,这样自定义属性与默认属性同时存在,自定义会覆盖默认*/}
export default class MyNavLink extends Component {
    render() {
        return (
            
        )
    }
}
  • 调用:
新闻 //使用默认的属性
热点新闻 //使用部分自定义属性
  • 注意:
    MyNavLink组件在定义时候,标签体内容在this.props.children内( 标签体内容为特殊的属性 ),所以一并与其他属性一起解构即可

redux

安装 npm install redux -S

  1. 概述:
  • 状态管理的JS库,非react官方
  • 可以配合前端三大框架使用,但基本与react配合使用
  • 集中管理react应用中多个组件共享的状态
  1. 使用场景:
  • 某个组件状态与其他组件一起用
  • 一个组件需要改变另外一个组件的状态
  • 使用原则,能不用就不用,非用不可才用
  1. 目录结构建议
    同components级目录下,创建redux文件夹,里面放置
  • store.js
  • action 文件夹
  • reducer 文件夹
  • type类型的常量.js (可有可无,方便多文件内修改action的type,放置输入错误的情况)
  1. 原理图


    原理图
  • Action Creators 创建函数返回一个对象,对象的格式为
// +data操作的action
export const addAction = (data)=>{
    return { 
        type: 'add',
        data: data*1 
    }
}
// -data操作的action
export const redAction = (data)=>{
    return { 
        type: 'redu',
        data: data*1 
    }
}
  • Reducer
//初始值
let initState = 0;
export default function reduceFn(preState=initState,action){
    switch(action.type){
        case 'add':
            return preState+action.data;
        case 'redu':
            return preState-action.data;
        default:
            return preState;         
    }
};
  • Store 创建一个数据仓库 (返回store)
import {createStore} from 'redux'
import countReducer from './count_reducer'
export default createStore(countReducer)
  • Store 派发action
//-- 派发一个+10的操作
store.dispatch(addAction(10));
//-- 派发一个减20的操作
store.dispatch(redAction(20));
  • Store 内的数据变化时候,强制调用render更新视图 ( setState一个空对象 )
store.subscribe(function(){
  that.setState({}); //强制触发render视图(当store内的数据变化时候)
})

也可以对整个App来做监听状态变化的刷新(index.js)

ReactDOM.render(,document.getElementById('root'));
//监听状态变化后,刷新App
store.subscribe(()=>{
  ReactDOM.render(,document.getElementById('root'));
});
  • 获取Store内的数据
store.getState(xxx); // store获取数据
  1. redux库的基本构成
  • Action Creator - 接收组件请求,创建一个动作,将需要做的事儿,dispatch给Store

  • Store - 将Action Creator的要求发送(之前的状态以及ActionCreator传递的事儿)给 Reducers

  • Reducers - 处理数据,返回新的数据(初始化数据(之前的状态为undefined),与更新数据)

  • React Component - 通过getState从Store请求获取新数据,以及发送请求给 Action Creator

  • React Component 相当于客户

  • Action Creator 相当于服务员 初始化时候 dispatch(action) action的{type:@@init@@} data不传递,只传递type

  • Store 相当于餐厅经理

  • Reducers 相当于厨师

  • 客户 -> 服务员 -> 经理 -> 厨师 -> 经理 -> 客户

精简流程
  1. 关于异步action如果不像放在组件内,而想放在交给action中
    由于store必须dispatch一个普通对象,而非函数,所以需要借助一个中间件。

需要借助于第三方库 redux-thunk

  • 安装 npm install redux-thunk

  • 步骤1,action创建函数不返回对象,返回一个函数,函数内容部调用同步action

  • 步骤2,store中通过applyMiddleware包裹thunk(三方包)来作为createStore的第二个参数

  • 步骤3,在组件事件处理函数中,直接用store派发action, 如:store.dispatch(addActionAsync(数据,延迟时间))

  • store.js ( 通过applyMiddleware载入中间件 )

import {createStore,applyMiddleware} from 'redux'
import countReducer from './count_reducer'
import thunk from 'redux-thunk'

export default createStore(countReducer,applyMiddleware(thunk))
  • 异步action
/*
* 根据不同逻辑,返回一个action对象 ,或者一个异步函数(需要借助于三方库 redux-thunk)
* 对象 { type:'xxx', data:'xxxx' }
* 函数 function(){ return action调用 }
*/
import {ADD,RED} from './const_vars'
import store from './store'
export const addAction = (data)=>{
    return { 
        type: ADD,
        data: data*1 
    }
}
export const redAction = (data)=>{
    return { 
        type: RED,
        data: data*1 
    }
}
export const addActionAsync = (data,time)=>{
    return ()=>{ 
        setTimeout(()=>{
            store.dispatch(addAction(data));
        },time) 
    }
}
  • 组件内
import React, { Component } from 'react'
import store from '../../redux/store'
import {addAction,redAction,addActionAsync} from '../../redux/count_action'

export default class Count extends Component {
    // 事件处理函数
    dealFn = (typeName)=> {
        let _val = this.selectEle.value;
        if(typeName===ADD){
            store.dispatch(addAction(_val));
        }else if(typeName===RED){
            store.dispatch(redAction(_val));
        }else if(typeName===ADDIFEVEN){
            let nowVal = store.getState();
            if(nowVal%2!==0){
                store.dispatch(addAction(_val));
            }
        }else if(typeName===ADDASYNC){
            //一个借助于redux-thunk的异步action
            store.dispatch(addActionAsync(_val,1000))
        }
    } 
    render() {
        return (
            

计算后的值:{store.getState()}

{/* 简化 */}      
) } }
异步action

react-redux

  1. 特点
  • react-redux官方提供,目的是减少对store频繁直接调用,如 store.getState、store.dispatch、store.subscribe,等一些列操作都放在container容器上,通过容器来做这一些列操作
  • 从react-redux中导入connect,将UI组件,state、dispatch方法导入到container容器
  • 将store创立的仓库,挂载到容器标签上
  • UI组件内的数据通过属性获取(mapStateToProps),修改状态的方法(mapDispatchToProps)同样通过属性获取
  • store与reducer的使用,同使用redux没有什么差别,主要工作在UI组件内不停的调用属性

原理图:


react-redux 原理图

总结点:


细节点
  1. 使用流程
    第一步,安装react-redux, 同component文件夹,创建一个容器文件夹,创建一个容器文件 如:Count.js
//引入Connect
import { connect } from 'react-redux'
//引入UI
import CountUI from '../components/Count'
//引入Action
import { addAction,redAction,addActionAsync } from '../redux/count_action'

//mapStateToProps 返回对象
function mapStateToProps(state){
   return {count:state} 
}

//mapDispatchToProps 返回对象,对象内是方法
function mapDispatchToProps(dispatch){
    return {
        ADDFn: (val)=>{ dispatch(addAction(val)) },
        REDFn: (val)=>{ dispatch(redAction(val)) },
        ADDASYNC: (val,time)=>{ dispatch(addActionAsync(val,time)) }
    }
}

export default connect(mapStateToProps,mapDispatchToProps)(CountUI);

第二步,在UI组件内使用,通过属性props传递过来的属性 state 与 dispatch

import React, { Component } from 'react'
import {ADD,RED,ADDIFEVEN,ADDASYNC} from '../../redux/const_vars'

export default class Count extends Component {
    dealFn = (typeName)=> {
        let _val = this.selectEle.value;
        if(typeName===ADD){
            //通过属性获取修改状态的方法
            this.props.ADDFn(_val)
        }else if(typeName===RED){
            this.props.REDFn(_val)
        }else if(typeName===ADDIFEVEN){
            let nowVal = this.props.count;
            if(nowVal%2!==0){
                this.props.ADDFn(_val)
            }
        }else if(typeName===ADDASYNC){
            this.props.ADDASYNC(_val,1000);
        }
    };

    render() {
        return (
            
{/*通过属性获取状态*/}

计算后的值:{this.props.count}

{/* 简化 */}      
) } }
  • 优化容器组件的书写方式
//引入Connect
import { connect } from 'react-redux'
//引入UI
import CountUI from '../components/Count'
//引入Action
import { addAction,redAction,addActionAsync } from '../redux/count_action'

/*
* 状态对应一个对象 ( count的命名,可以根据reducer内的处理来语义化命名 )
* 修改状态方法也对应一个对象 ( 返回对应创建action的方法即可,内部的逻辑由react-redux来处理 )
*/
export default connect(
    state => ({count: state}),
    {
        ADDFn: addAction,
        REDFn: redAction,
        ADDASYNC: addActionAsync
    }
)(CountUI);
  • 优化容器的store挂载
    将所有容器组件上挂载的store,都统一挂载到react-redux的Provider组件上,用Provider包裹App组件,以实现一次性完成对所有容器组件的store挂载
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './redux/store'
import { Provider } from 'react-redux'

ReactDOM.render(
  
    
  ,
  document.getElementById('root')
);
  • 将容器组件与UI组件整合到一起使用
import React, { Component } from 'react'
//引入Connect
import { connect } from 'react-redux'
//引入Action
import { addAction } from '../redux/count_action'

class CountUI extends Component {
    // 简化
    dealFn = (typeName)=> {
        let _val = this.selectEle.value;
        if(typeName==='add'){
            this.props.ADDFn(_val)
        }
    }    
    render() {
        return (
            

计算后的值:{this.props.count}

{/* 简化 */}
) } }; // -- 简写方式 export default connect( state => ({count: state}), { ADDFn: addAction } )(CountUI);
  • react-redux书写流程整理


    react-redux流程
  1. 多个store数据
    使用combineReducers可以将多个reducer整理一起,用一个key:value的形式保存
import {createStore,applyMiddleware,combineReducers } from 'redux'
import thunk from 'redux-thunk'

import countReducer from './reducers/count'
import personReducer from './reducers/person'

/*reducers列表*/
let allReducer = combineReducers({
    num: countReducer,
    person: personReducer
})

export default createStore(allReducer,applyMiddleware(thunk))

在容器组件内,同过key值来获取每个reducer的状态值
如:

// -- 简写方式
export default connect(
    state => ({
        num: state.num,
        person: state.person
    }),
    { ...改变状态的action的key、value对儿 }
)(CountUI);
redux、react-redux 大体流程
react-redux总结

优化以及细节工作

  1. 组件按需加载 ( Lazy,Suspense都来自与react核心库,无需二次安装 )
import React, { Component,Lazy,Suspense } from 'react'
import { Switch,Route } from 'react-router-dom'

//按需引入组件
const About = Lazy(()=>{ import('@/component/About') });

//配置路由
LOADING....}>
  
    
      

  1. 由于React的jsx语法限制,不管是类组件还是函数式组件,返回的结构只能有一个根节点,而且这个节点会被渲染到页面中,无形中添加了很多无意义的div标签
    解决方法
    使用Fragment组件来替代一般html标签
import React,{Fragment} from 'react'

export default function Funcmp2(props) {
   return (
        {/* 这个类名为aaa的标签,不会被渲染到页面上 */}
        
            

这是一个函数自增长的函数类型组件 {num}

通过React.useEffect来开启一个定时器

) };

额外说明:

  • 可以使用<> xx 代替Fragment
  • Fragment组件上,只能添加一个key属性,否则会报错
    Invalid prop "name" supplied to "React.Fragment". React.Fragment can only have "key" and "children" props.
  1. 减少不必要的组件render
    问题1: 父子组件嵌套,子组件没有引用父组件要修改的数据,但是父组件setState改变数据,会触发render,连带子组件也都一并被render了,因为组件的shouldComponentUpdate默认返回的布尔值为true
    问题2: this.setState({}); 设置一个空对象,也会触发render

解决思路:

  • 通过shouldComponentUpdate的参数nextProp、nextState与组件之前的状态比较,只有发生变化才返回true,允许更新组件,反之返回false

父组件:

export default class Cmp1 extends Component {
    ....
    
    render() {
        return (
            

我是组件1

) } }

子组件:

import React, { Component } from 'react'

export default class Cmp2 extends Component {

    shouldComponentUpdate(nextProp,nextState){
        //使用父级的state有变化
        if(nextProp.num !== this.props.num){
            return true;
        }else{
            return false;
        }
    }

    render() {

        let { num } = this.props;
        //如果父级改变的数据,不是Cmp2使用的数据,为了优化,子组件可以不用跟随父组件一起render
        return (
            
我是组件2

{num}

) } }
  • 通过React的内置组件PrueComponent来自动进行prop与state变化比较

使用PureComponent的例子
子组件:

import React, { PureComponent } from 'react'

export default class Cmp2 extends PureComponent {
    ...
    render() {

        let { num } = this.props;
        //如果父级改变的数据,不是Cmp2使用的数据,为了优化,子组件可以不用跟随父组件一起render
        return (
            
我是组件2

{num}

) } }

额外注意的:
第一,PureComponent自己一个封装shouldComponentUpdate;
如果PureCompnent里书写shouldComponentUpdate会报错
shouldComponentUpdate should not be used when extending React.PureComponent.
Please extend React.Component if shouldComponentUpdate is used.

第二,修改state使用setState时候,需要修改一个新对象,而不能修改对象上的属性,否则React浅比较,不会对同一个对象做render

//数据
state = { name:'张三' }

changeName = () => {
    let obj = this.state;

    //react不会render页面,因为obj是原来的对象,引用的内存地址是同一个
    obj.name='李四';
    this.setState(obj); 
    
    //这个会render,因为setState里是一个全新的对象
    this.setState({
      name: '李四'
    });
};

第三,设置state为空,react将不会调用render方法

this.setState({})  //设置空对象的方法,将不会触发render,在PureComponent组件中
  • 不用PureComponet的情况下,依然来手动判断,什么情况下更新组件
    在React17开始,在使用setState时候,用函数式书写方式,返回一个null,react将不会触发render
//在事件处理函数内,对setState进行二次逻辑判断,数值没有变化返回null,反之 正常返回
changeName = (ev) => {
  let {name} = this.state;
  let changeName = ev.target.value;
  this.seState(()=>{
    if(name===changeName ){
      return null;
    }else{
      return { name:  changeName }
    }
  })
}
  1. render props 组件插槽
  • 父组件通过挂载属性,等于一个函数,函数的返回值为一个组件,通过函数传参可以将父组件的数据传递至子组件,而父组件内需要预留一个位置,放这个未知的组件,这个未知的地方就是插槽的地方
  • 类似Vue在组件标签体内写的内容,会出现组件内部的slot内,react的标签体就是一个自定义属性,而在组件标签内只需调用该属性对应函数,将组件内的数据作为该函数的参数即可
  • 一般为了阅读快速理解,不成文规定大家将返回组件的自定义属性名,设置为'render'

父组件:

import React, { Component } from 'react'
import Cmp1 from './components/Cmp1'
import Cmp2 from './components/Cmp2'

export default class App extends Component {
  render() {
    return (
      
{ return }} />
) } }

Cmp1组件:

export default class Cmp1 extends Component {
    state = {
        num:1,
        name:'张三'
    }
    render() {  
    return (
        //子组件在Cmp1中希望放置的位置
        

我是组件Cmp1

{this.props.render(this.state.num)}
) }

Cmp2组件:

export default class Cmp2 extends PureComponent {
    render() {

        let { num } = this.props;
        return (
            
我是组件Cmp2

{num}

) } }
  1. 组件渲染降级,getDerivedStateFromError
  • getDerivedStateFromError 是静态属性
    对因为不确定因素无法渲染的子组件,进行降级渲染,防止整个组件嵌套体系全部因为改子组件错误,而无法渲染界面
  • getDerivedStateFromError 在生产环境下,按照我们预期会渲染一下,然后依旧报错,这个在生产环境下会正常
  • componentDidCatch 生命周期,可以帮助我们汇总错误,发送服务器后台
export default class Cmp1 extends Component {
    state = {
        list: [ xxx,xxx ]
        hasError: false
    }

    static getDerivedStateFromError(){
        //改组件嵌套的子组件存在错误,因此通过开关,阻止子组件进一步渲染
        return { 'hasError': true };
    }

    componentDidCatch(error, info){
        //将错误信息发送至后台服务器,用于记录错误日志,便于开发人员汇总与修改
        console.log('info ~~~ ');
        console.log(typeof info.componentStack); //string
    }

    render() {
        if (this.state.hasError) {
            // 你可以渲染任何自定义的降级 UI
            return 

Something went wrong.

; } return ; } }

花式报错整合

  1. 给一个不存在的组件设置数据
    移除了一个组件,或者切换路由,之前的组件开启定时器或者异步数据请求,且在恰当的时候,类组件调用了setState(函数式组件调用了React.useState的设置state的方法)
    The node you're attempting to unmount was rendered by React and is not a top-level container. Instead, have the parent component update its state and rerender in order to remove this component
    解决方法:
    在类组件compontentWillUnmount(函数式组件 React.useEffect第一个函数参数回调的函数内)
  • 针对定时器,停止定时器
    函数式组件关闭定时器:
let [num,addNum] = React.useState(0)

React.useEffect(()=>{
    let iTimer = setInterval(function(){
        addNum((num)=>{
            return num+1
        })
    },1000);

    return (()=>{
        clearInterval(iTimer);
    })
},[]);
  • 针对AJAX数据请求,设置开关,阻止其setState
    类组件关闭AJAX异步数据设置操作
componentDidMount(){
    this._isMounted = true;
    $.ajax('xxxxx',{})
        .then(res => {
            if(this._isMounted){
                this.setState({
                    aa:true
                })
            }
        })
        .catch(err => {})
}
componentWillUnMount(){
    this._isMounted = false;
}

或 网上的方法:

componentDidMount(){
    $.ajax('xxxx',{})
    .then(res => {
        this.setState({
            data: datas,
        });
    })
    .catch(err => {})
}
componentWillUnmount(){
    this.setState = (state,callback)=>{
      return;
    };
}
  1. 使用react.useEffect定义的变量xxxxxxx,不能定义在react.useEffect外面
    Assignments to the 'xxxxxxx' variable from inside React Hook React.useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside React.useEffect
    解决方式:
    修改书写方式,如:
export default function Funcmp2(props) {
    let [num,addNum] = React.useState(0)
    //不能写在外面,要写在React.useEffect里面,去定义变量
    //let iTimer = null;  //错误的

    React.useEffect(()=>{
        let iTimer = setInterval(function(){
            // addNum(num+1);
            addNum((num)=>{
                console.log(num+1);
                return num+1
            })
        },1000);

        return (()=>{
            //第一个函数的返回值为 componentWillUnmount 
            clearInterval(iTimer);
        })

    },[]);
}
  1. 使用React.useState定义的state,在React.useEffect内进行数据修改时候,不能写成对象形式,要写成函数形式
    React Hook React.useEffect has a missing dependency: 'num'. Either include it or remove the dependency array. You can also do a functional update 'addNum(n => ...)' if you only need 'num' in the 'addNum' call react-hooks/exhaustive-deps
    解决方法:
export default function Funcmp2(props) {
    let [num,addNum] = React.useState(0)

    React.useEffect(()=>{
        //错误
        //addNum(num+1);
        
        //正确
        addNum((num)=>{
            console.log(num+1);
            return num+1
        })
    },[]);
}
  1. 给一个非ReactDOM.render的组件,进行组件移除操作
    Warning: unmountComponentAtNode(): The node you're attempting to unmount was rendered by React and is not a top-level container. Instead, have the parent component update its state and rerender in order to remove this component.
    解决方法:
    父组件
export default class Head extends Component {
    componentDidMount(){
      //渲染一个自组件
      ReactDOM.render(,document.getElementById('timerBox'));
    }
    render() {
        let {addTxt} = this.state;
        return (
            
{ /* 定时器容器 */}

上面是函数式组件

) } }

子组件

import React from 'react'
import ReactDOM from 'react-dom'

export default function Funcmp2(props) {

    let [num,addNum] = React.useState(0)
    
    React.useEffect(()=>{
        let iTimer = setInterval(function(){
            addNum((num)=>{
                console.log(num+1);
                return num+1
            })
        },1000);

        return (()=>{
            clearInterval(iTimer);
        })

    },[]);

    function delFn(){
        if(props.removeNodeId){
            ReactDOM.unmountComponentAtNode(document.getElementById(props.removeNodeId)); 
        }
    }

    return (
        

这是一个函数自增长的函数类型组件 {num}

通过React.useEffect来开启一个定时器

) }

未完待续....

你可能感兴趣的:(React B站学习杂记)