React学习笔记

160 阅读45分钟

react

jsx

{}中写js表达式:执行有结果的

js表达式:

  1. 变量

  2. 数学运算

  3. 判断:三目运算符

  4. 循环中:所有命令编程的循环都不是(for,for/in for/of ),借助于数组的迭代方法处理:map

    • Nmber/string 写的啥显示啥
    • Boolean/null/undefiend/symbol/bigInt: 渲染出来就是空
    • 普通对象无法渲染
    • 数组对象:把数组中的每一项都拿出来渲染
    • 函数对象不支持渲染,但是可以用组件的方式渲染
    • ...

ReactDOM.createRoot();不能直接用HTML或者是body节点直接作为根节点,并且只能有一个根节点。可以用fragment<></>,每一个构建的视图只能有一个根节点,否则会报错。

<>
	<App />
</>
  1. 给元素设置样式

    function App() {
      return (
        <div className="App" style={{width: '100px',height:'200px',border: '1px solid red'}}>
         react 
        </div>
      );
    }
    

    行内样式:样式属性要基于驼峰命名;设置class用className

控制元素的渲染


{ flg&& <button>按钮</button>}

jsx处理的底层机制

  1. 把jsx语法编译成虚拟DOM对象。

    虚拟DOM对象:框架自己内部构建的一套对象系统

    1. 基于babel-preset-react-app 把jsx编译成React.createElement(...)这种格式

    2. 再把createElement 方法执行,创建出虚拟DOM

      babeljs.io/ 可以进行转化

      语法报编译的结果

       <div className="App" style={{width: '100px',height:'200px',border: '1px solid red'}}>
           react 
           <div>
              { flg&& <button>按钮</button>}
           </div>
       </div>
      
      import { jsx as _jsx } from "react/jsx-runtime";
      import { jsxs as _jsxs } from "react/jsx-runtime";
      _jsxs("div", {
        className: "App",
        style: {
          width: '100px',
          height: '200px',
          border: '1px solid red'
        },
        children: ["react",_jsx("div", {
          children: flg && _jsx("button", {
            children: "\u6309\u94AE"
          })
        })]
      });
      

      React.createElement(ele,props,...children)

      • ele:元素标签或者是组件
      • props: 元素属性的集合,如果没有属性就是null
      • Children: 第三个元素及以后得参数都是当前元素的子节点

      只要是元素节点必然会基于createElement处理。

  2. 把虚拟DOM构建成真实的DOM

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
   <>
    <App />
   </>
  </React.StrictMode>
);

对象遍历

封装一个对象迭代的方法

​ 基于传统的for-in循环,会存在一些性能上的缺陷, 【性能较差,既可以迭代私有的,也可以迭代公有的,只能迭代可枚举的,非Symbol属性。。。】

​ Object.getOwnPropertyNames() 获取对象非Symbol类型的私有属性

​ Object.getOwnPropertySymbols() 可以获取Symbol类型的私有属性

let keys = Object.getOwnPropertyNames(arr).concat(Object.getOwnPropertySymbols(arr))

​ 也可以基于es6中的Reflect.ownKeys代替上述操作【弊端不兼容IE】

封装迭代函数

const each = function each(obj, callback) {
    if (obj === null || typeof obj !== "object") {
        throw new Error(obj + "is not object");
    }
    if (typeof callback !== "function") {
        throw new Error(callback + "is not a function");
    }
    let keys = Reflect.ownKeys(obj);
    keys.forEach(item => {
        let value = obj[item];
        // 每次迭代都把回调函数执行。
        callback(value, item);
    });
};
each(arr,(value,key)=>{
  console.log(value);
})

函数组件

rfc

​ 在src目录下创建一个xxx.jsx文件,就是创建一个组件,创建一个函数,让函数返回一个jsx视图,或者返回虚拟对象。可以用快捷方式创建函数组件 rfc

​ 组件的名字一般都用大驼峰的写法。

import React from 'react'
export default function DemoOne() {
  return (
    <div>DemoOne</div>
  )
}

​ 调用的时候再入口导入,然后用标签的形式导入。

import React from 'react'; // react 语法核心
import ReactDOM from 'react-dom/client'; // 构建html的核心(webAPP)
import DemoOne from './views/DemoOne'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
   <>
    <DemoOne />
   </>
);

我们在调用组件的时候,可以设置组件各种各样的属性,可以用胡子语法传递。

    <DemoOne title="我是子组件" x={10} y={[10,20]} style={{width:'100px'}}/>

渲染机制

基于babel-preset-react-app 把调用的组件转换为createElement格式

import { jsx as _jsx } from "react/jsx-runtime";
/*#__PURE__*/_jsx(DemoOne, {
  title: "\u6211\u662F\u5B50\u7EC4\u4EF6",
  x: 10,
  y: [10, 20],
  style: {
    width: '100px'
  }
});

把createElement方法执行,创建一个虚拟DOM。

基于root.render把虚拟DOM变成真实的DOM。

基于双闭合调用,可以传递子节点,在传递函数的props中,有一个children属性来存储这些节点。

    <DemoOne title="我是子组件" x={10} y={[10,20]} style={{width:'100px'}}>
      <h1>dddd</h1>
    </DemoOne>
  1. props传递过来的属性

    • 是只读的,只能获取不能修改。props.xxx 因为这个props对象是被冻结的。

    • 作用:可以让组件的复用性更强。基于props可以把不同的信息传递给子组件,子组件接受不同的属性,呈现出不同的效果。

    • import React from 'react'
      export default function DemoOne(props) {// 用props接收
        console.log(props); // 
        return (
          <div>DemoOne</div>
        )
      }
      
      
      • 虽然不能修改属性,但是可以做规则校验。

        #设置默
        认值
        DemoOne.defaultProps = {
          x:0
        }
        
        

        通过propTypes来进行格式校验

        安装 prop-type: yarn add prop-type
        
        import React from 'react'
        import PropTyope from 'prop-types' // 导入prop-types
        export default function DemoOne(props) {
          console.log(props);
          return (
            <div>DemoOne</div>
          )
        }
        
        DemoOne.defaultProps = {
          x:0
        }
        
        DemoOne.propTypes = {
        	title:PropTyope.string.isRequired  //PropTyope是导入的类型 string类型,而且是必输
        }
        
        
        
        // 其余的类型校验还有
          title: PropTypes.array,
          title: PropTypes.bigint,
          title: PropTypes.bool,
          title: PropTypes.func,
          title: PropTypes.number,
          title: PropTypes.object,
          title: PropTypes.string,
          title: PropTypes.symbol
        
        
        // 校验其中类型的一种
        optionalUnion: PropTypes.oneOfType([
            PropTypes.string,
            PropTypes.number,
            PropTypes.instanceOf(Message)
          ]),
        

        传递过来的属性会先做规则校验,不管检验成功还是失败,最后都会把属性传递给props,只不过如果不符合设定的规则,控制台会报错。

对象的规则设置

冻结对象

Object.freeze(obj) // 冻结对象,不可以删除属性

Object.isFrozen(obj) // 检测对象是否被冻结

被冻结的对象不可以删除成员值,不能修改成员值,新增成员,不能做劫持Object.definePropotype()

密封对象

密封对象:Object.seal(obj)

检测密封:Object.isSealed(obj)

密封特点:可以修改成员值,不能新增,删除成员,不能劫持。

扩展

不扩展对象: Object.preventExtensions(obj) // 把对象设置成不可扩展

不可扩展检测:Object.isExtensible(obj)

特性:除了不能新增以外,其余都可以。

插槽处理机制

子组件通过双闭合调用的方式,通过子标签传递一些内容,在子组件中用props.children来接收插槽。

  • 传递数据用属性
  • 传递html结构用插槽
import  Children  from './Children.jsx'

function App() {
    return <div className="App">
      <Children>
        <div>
         我是插槽传递的内容
        </div>
      </Children>
    </div>;
}

export default App;

 
import React from 'react'
export default function Children(props) {
  return (
    <div>
      {props.children}
    </div>
  )
}

当没有传递插槽的时候是undified,传递一个插槽是对象,传递多个插槽children是一个数组。

可以给予React.Children 的方法对props.children进行处理。内部的方法对children做了处理。

image-20230831140522532

import React from 'react'
export default function Children(props) {
  console.log(React);
  let children = React.Children.toArray(props.children)
  return (
    <div>
      {children.map(item=> item)}
    </div>
  )
}

具名插槽

具名插槽就是在插槽上添加一个slot属性

<div slot="three"> 我是具名插槽</div>

在调用组件的时候,通过名字来显示组件,不用理会组件的属性,通过slot的值来自己决定显示在什么地方。

image-20230831140659083

静态组件和动态组件

函数组件:最初的函数组件就是静态组件,函数组件第一次渲染完成之后,组件内容不会根据一些操作二更新组件,除非在父组件中重新调用组件。

类组件合hooks组件:第一次渲染完毕之后,基于内部的一些操作,可以更新视图,可以呈现出不同的效果。

类组件

组件状态

创建一个类组件 rcc

创建一个类,继承React.Component

必须给当前类设置一个render方法,放在原型上,在render方法中返回渲染的视图。

import React, { Component } from 'react'

export default class ClassCom extends Component {
  render() {
    return (
      <div>ClassCom</div>
    )
  }
}

一些类的基本知识

class Person {
  constructor(name,age){
    // this====>是绑定到实例上的
    this.name = name
    this.age = age

    // 设置私有方法
    this.add = function(parms) {
      
    }
  }
  // 在这里也是私有属性,类似与this.school = school
  school = 'xiaoxue'

  // 变量=xxx的都是私有属性
  add1 = function(){}

  // 这样写就是在原型上的方法
  sum(){}
}

let p = new Person("zhangsan",12)
console.log(p);

从调用组件发生的事情:

  1. 初始化属性&& 规则校验

​ 校验属性的方法也有两种

  • //第一种还是用组件名校验,跟函数组件一样
    // 属性校验 第一种
    ClassCom.propTypes = {
      title: PropTypes.number
    }
    
    // 第二中用static关键字修饰,用静态属性来校验
    import React, { Component } from 'react'
    import PropTypes from 'prop-types'
    export default class ClassCom extends Component {
      // 属性校验第二种
      static propTypes = {
        title: PropTypes.number
      }
      render() {
        return (
          <div>{this.props.title}</div>
        )
      }
    }
    
  • 方案一

    // 添加constrcutor一定要添加super()
    constructor(props){
        super(props)
    }
    
  • 方案二

    即使我们不用constrcuror处理,在constrcuror执行完毕之后React 内部也会吧props挂载到实例上,所以在内部使用时,只要保证this是react的实例,就可以用过this.props来得到属性。同样this.props也是冻结的对象。

    import React, { Component } from 'react'
    
    export default class ClassCom extends Component {
      render() {
        return (
          <div>{this.props.title}</div>
        )
      }
    }
    
  1. 初始化状态

状态:后期修改状态,组件会刷新重新渲染。

状态需要手动处理,如果我们不处理react会自动挂载一个state属性,this.state = null

初始化状态只需添加一个私有的state属性即可。

 state = {
    num:10,
  }

在页面上用this.state.xxx来获取值。通过this.setState({})来修改属性值,可以只添加我们修改的值。支持部分修改。

还可以通过ths.forceUpdate()来进行强制刷新。

import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class ClassCom extends Component {
  // 属性校验第二种
  static propTypes = {
    title: PropTypes.string
  }
    // 状态初始化
  state = {
    num:10,
  }
  add = function(){
      // 用setState来修改状态
    this.setState({
      num: this.state.num+1
    })
  }
  render() {
    return (
      <>
      <div>{this.props.title}</div>
      <div>{this.state.num}</div>
      <button onClick={()=>{this.add()}}>add</button>

      </>
    )
  }
}

生命周期函数

生命周期函数也叫钩子函数:程序运行到某一阶段,提供给我们一个方法,让开发者可以在这个阶段做一些事情。

常用的生命周期有这些,由于hooks的出现,class组件用的比较少了,只介绍常用的生命周期。

image-20230831140710136

import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class ClassCom extends Component {
  // 属性校验第二种
  static propTypes = {
    title: PropTypes.string
  }
  state = {
    num:10,
  }
  add = function(){
    this.setState({
      num: this.state.num+1
    })
  }
  render() {
    console.log('reander');
    return (
      <>
      <div>{this.props.title}</div>
      <div>{this.state.num}</div>
      <button onClick={()=>{this.add()}}>add</button>

      </>
    )
  }
componentDidMount(){
  console.log("componentDidMount");
}
componentDidUpdate(){
  console.log("componentDidUpdate");
}
componentWillUnmount(){
  console.log("componentWillUnmount");
}

组件第一次渲染的时候,会执行render函数和componentDidMount组件。

  • render执行完毕之后就完成了渲染
  • componentDidMoun是挂载之后,这个时候已经将虚拟dom变成了真实DOM

更新组件的时候会执行render函数和componentDidUpdate。

父子组件渲染顺序:深度优先原则 父willMount > 子render > 子willMount > 子render > 子didMounted > 父didMounted

PureComponent和Component

PureComponent 会默认给组件添加一个shouldComponentsUpdate周期函数,在这个函数中会对属性做一个浅比较,如果发现属性没有更新,则会返回false,就不会更新。

**浅比较:**对象的比较内存地址,而不是具体的值。

Ref的相关操作

受控组件:基于修改数据/状态,让视图更新。

非受控组件:基于ref获取dom元素,操作DOM元素实现需求。

**方式一:**不推荐使用

<div ref="titleName"></div>
this.refs.titleName

**方式二:**函数的方式

将ref设置成一个函数,函数的第一个参数就是DOM元素。

<div ref={x=this.titleName = x}></div>

this.titleName 来获取DOM
x是形参,存储的是当前的DOM元素,然后将这个DOM赋值给titleName属性

方式三:React.createRef()对象

refName = React.createRef()
<div ref={refName}></div>

this.refNamel来获取DOM

给元素设置ref可以获得DOM元素,给组件设置ref可以获取组件的实例。(函数组件不能设置ref,但是可以用React.fowardRef实现ref转发,获取组价内部的元素)

import React from 'react'

const Children = React.forwardRef(function(props,ref) {
  let children = React.Children.toArray(props.children)
  return (
    <div ref={ref}>
      {children.map(item=> item)}
    </div>
  )
})

export default Children


<Children ref={x=>this.childRef = x}>
</Children>

// this.childRef得到的就是Children组件中的div中的DOM元素

setState()进阶

this.setState([partialState],[callback])

partialState: 支持部分状态修改,无论多少个状态,我们只修改要修改的。

callback:在状态更改,视图更新完成之后触发执行,发生在componentDidupdate之后,只会在自己这个状态修改之后才会执行。

视图更新一次

handle = ()=>{
  this.setState({
    x:x+1,
    y:y+1,
    z:z+1 
  })

视图更新一次,就会发现每个setState是异步执行, 而不是同步执行的。React18中有一套更新机制,异步操作,实现批处理。可以减少视图更新的次数,降低渲染消耗的性能。让更新的逻辑和流程更加的清晰。

this.setState({x:x+1})// 不会立即更新状态和视图,而是加入到更新队列中。
this.setState({y:y+1})
this.setState({z:z+1})
  • 在产生的私有上下文中,代码自上而下执行,先添加到更新队列中。
  • 当上下文代码都处理完毕之后,会扔更新队列的任务,统一渲染/更新一次【批处理】

在react18中,this.setState是异步的,无论是在哪里执行,例如合成事件中,周期函数中,定时器中,setState都是异步的。

目的:实现状态的批处理,有效减少更新的次数,降低性能消耗。有效管理代码执行的逻辑顺序。

原理:利用更新队列【updater】机制来处理,在当前相同的时间段内,遇到setState会立即放入到更新队列当中,此时状态/视图还没更新。当前全部代码执行结束之后才回去更新视图,只触发一次视图的更新。

合成事件

onXxxxx = ()=>{}

点击事件

<div onClick={()=>{执行}}></div>

this丢失的问题

用箭头函数可以解决this丢失的问题。

基于React内部处理,如果我们给合成事件绑定一个普通函数,当事件触发行为,绑定行为函数,方法中的this会是undefined。

我们可以基于js中的bind方法进行解决,预处理函数中的this【用call和apply函数会立即执行,不符合要求,我们需要点击的时候才执行。】

import React, { Component } from 'react'

export default class ClassCom extends Component {
  handle(){
    console.log(this);// undefined
  }
  render() {
    return (
      <div>
        <button onClick={this.handle}>点击</button>
      </div>
    )
  }
}

也可以把绑定的函数设置成箭头函数,让其使用上下文中的this.要么在定义函数的时候定义成箭头函数,或者是在调用函数的时候用箭头函数来调用,其实道理是一样的。

import React, { Component } from 'react'

export default class ClassCom extends Component {
  handle(){
    console.log(this);// undefined
  }
    // 将函数定义成箭头函数
  handle1 = ()=>{
    console.log(this);
  }
  render() {
    return (
      <div>
         <button onClick={this.handle1}>点击</button>
         <button onClick={()=>{this.handle()}}>点击</button> // 用箭头函数的方式调用
      </div>
    )
  }
}

合成事件对象

  handle2(ev){
    console.log(ev);
  }  

合成事件对象(SyntheticBaseEvent ):我们在React合成事件触发的时候,也可以获取到事件对象,这个对象就是合成事件对象,也就是ev,

也包含了浏览器内置事件对象的一些属性和方法。

可以箭头函数来传递额外的参数


 handle2=(x,y,ev)=>{
    console.log(x,y);
    console.log(ev);
  }
<button onClick={(ev)=>{this.handle2(10,20,ev)}}>点击</button>

也可以用过bind来实现。用bind来得到合成事件对象值得注意的是,最后一个参数才是合成对象但是箭头函数的调用,可以任意位置,就像上面的代码一样,(ev)=>{this.handle(ev,10,10)} ev在第几个参数,拿在handle中就是第几个参数是合成对象。

 handle2=(x,y,ev)=>{
    console.log(x,y);
    console.log(ev);
  }
<button onClick={this.handle2.bind(this,10,20)}>点击</button>

事件和事件委托

事件具备的传播机制:点击最里层的

第一步:从最外层向里逐一查找(捕获阶段)

第二步:把事件源(被点击的元素)的点击行为触发(目标阶段)

第三步:按照捕获的阶段分析出来的路径,从里到外把每一个元素的点击事件触发(冒泡阶段)。

事件和事件绑定:

  • 事件是浏览器赋予元素的默认行为
  • 事件绑定是给这个行为绑定一个方法。

事件委托:

利用事件传播机制,实现一套事件绑定方案。只需要给容器做一个事件绑定,点击容器内部任何元素,根据事件传播机制,都会让容器点击的事件也触发。

React合成事件机制

React合成事件:

  1. 绝对不是给当前元素基于addEventListener单独做的事件绑定,react中的合成事件都是基于“事件委托”处理的。
    • 在React17后都是委托给#root这个容器,冒泡和捕获都做了委托;在17版本之前都是委托给document,只做了冒泡阶段的委托。
    • 对于没有实现事件传播机制的事件,才单独做的绑定[onMounseEnter......]
  2. 在组件渲染的时候发现jsx元素有onXxxxx/onXxxCaputer这样的属性,不会给当前元素直接做事件绑定,只是把绑定的方法赋值给元素的相关属性。然后对#root容器做了事件绑定[捕获和冒泡都做了]。
    • 因为组件中渲染的内容,最后都会插入到#root容器中,这样点击页面中的任何一个元素,最后都会把#root的点击行为触发。而在给#root绑定的方法当中,把之前给元素设置的onXxxxx/onXxxCaputer属性执行。

(react有内置的事件,当事件触发的时候,会将这个事件赋值给onXxxxx/onXxxCaputer,这个就是合成事件绑定。将react内置的事件机制同onXxxxx/onXxxCaputer属性绑定)

hooks组件

hooks中useXxxx的就是hooks函数。hooks函数只能写在函数组件的最外层,不能嵌入到代码中去执行。

useState

静态函数组件是没办法修改状态让视图更新的。hooks组件引入了useState

useState返回的是一个数组,传入的是状态的初始值initialState。

  • 第一个元素就是状态值xxx
  • 第二个属性是一个修改状态的函数,为了语义化我们一般命名为setXxx

函数组件不是类组件,没有实例的概念,就没有this,只是把函数执行,创建一个函数执行上下文而已。

import React from 'react'
import {useState} from 'react'
import {Button} from 'antd'
export default function FuncCom() {
    // useState
  let [num, setNum] = useState(0);
  return (
   <div>
     <div>{num}</div>
           // 修改状态
    <Button type="primary" onClick={()=>{setNum(num+1)}}>按钮</Button>
   </div>
  )
}

函数组件的每一次渲染或者是更新都是把函数的重新执行,每一次执行都会产生一个全新的私有上下文,内部的代码都需要重新执行一次。用setXxx执行的时候传递的不是初始值,而是最新的状态值。如果是普通变量,将没办法保持变量的最新状态。

每一次执行useState,只有第一次设置的初始值会生效,其余以后再执行,获取的状态都是最新的状态值,返回修改的方法返回的也是每次返回一个新的方法。而没有用useState的普通变量,每次更新状态之后都会变成初始值,因为函数成功执行了。

每次函数组件的执行都会产生一个上下文,当函数内部有方法执行的时候会产生一个私有上下文,这个私有上下文每次寻找的是上一次的状态;每次执行方法的时候就会重新执行函数组件,产生一个新的上下文。这个跟vue的双向绑定有很大的区别,不要用双向绑定的细想带入。你大爷已经不是你以前的大爷了,但是你现在依靠的还是之前的大爷,因为以后的大爷是属于以后的你。

  let [num, setNum] = useState(0);
  const changeNum = ()=>{
    setNum(num+1) // num 1
      console.log(num);// num 0
    setTimeout(() => {
        
      console.log(num); // num 0 
    }, 2000);
  }
// 这里打印num是0的原因是因为,setNum之后会,虽然函数组件会重新执行,会形成一个新的上下文,但是打印中的num依赖的还是上次执行形成的上下文,num的值是上一个上下文的值。

执行一次setState ,把需要的状态信息放在对象中统一管理。

​ 执行setState方法,传递什么值,就会把状态的整体修改成这个值,不支持部分状态的修改

怎么修改对象呢?修改对象的时候发现值修改其中的某一个值,会覆盖之前的值,导致结果变成了只有这一个值了,修改的时候可以通过解构把旧值先附上去,再修改。

  let stu = {
    name:'张三',
    age:'13',
    sch:{
      name:'小学'
    }
  }
  const [student, setStudent] = useState(stu);
const changeStudent = ()=>{
    setStudent({
      ...student,// 先将之前的值附上去。
      name:'李四', // 修改新值
      sch:{
      name:'中心'
    }
    })
  }

useState的同步异步:

setXxx更新的时候是异步去更新的,不是同步,也是利用更新队列,等同于类组件的this.setState,在任何地方都是异步的。

**flushSync()**可以立即刷新队列,让视图立即渲染。

flushSync(()=>{
    setX(x+1)
    setY(y+1)
})
setZ(z+1)

// 这就相当于刷新两次,x和y一起刷新,z单独更新

useState自带优化机制

  • 每次修改的状态值的时候,会拿最新要修改的值跟之前的状态值作比较,基于Object.is比较
  • 如果两次值是一样的,则不修改状态,也不会让视图更新

再循环中达到每次循环都可以加。

 for(let i = 0;i<10;i++){
      // pre 存储上一次的状态值。  返回信息是我们修改的状态值
      setNum(pre=>pre+1)
    }

如果我们的初始值需要经过一系列的计算才能得到

  let [total, setTotal] = useState(()=>{
    let x = 20,y = 10
    total = 0
    total = x+y
    return total
  });

useEffect

相当于在函数组件中使用生命周期函数。视图渲染之后才执行这个useEffect。

只有一个callback参数的时候,会在第一次渲染和更新的时候执行。在useEffect中可以获取最新的状态值,类似与didMount和didUpdate的结合。

useEffect(() => {
    console.log("dddddddddddddddd");
  });

添加第二个参数,[],只有第一次渲染完毕之后才会执行callback,更新状态的时候不会再去执行,相当于didiMount。

useEffect(() => {
    console.log("dddddddddddddddd");
  },[];

依赖多个状态,第一次渲染的执行,当依赖中的一个发生改变的时候,就不触发callback执行。如果依赖的状态没有变化,callback就不会执行。

useEffect(() => {
    console.log("num2",num);
},[num]);

也是类似于vue中watch,监听,监听的数据发生改变的时候执行。

怎么在useEffect中获取到修改之前的状态呢?

在callback中返回一个函数就好了。

useEffect(() => {
    return ()=>{
        console.log("num4",num);// 这里的num就是上一次状态值
    }
}

一些细节

  • useEffect只能在函数组件的最外层,不能嵌入到代码中去执行。

  • 如果设置返回值,则必须返回一个函数。返回一个实例也不行,必须是一个函数。可以用promise的.then不能将回调函数加上async,可以在callback的内部包一层小函数来执行就可以了。

    // 错误写法,也会有报错提示
    useEffect(async() => {
        const res = await Promise()
        },[num]);
    
    // 报错提示
     Effect callbacks are synchronous to prevent race conditions. Put the async function inside:
    useEffect(() => {
      async function fetchData() {
        // You can await here
        const response = await MyAPI.getData(someId);
        // ...
      }
      fetchData();
    }, [someId]); // Or [] if effect doesn't need props or state
    
    // 我们修改成报错的提示方式写法
    useEffect(() => {
        const fun = async()=>{
            const res =  await Promise()
            console.log(res,num);
        }
        fun()
    },[num]);
    

useLayoutEffect

img

useEffect实在虚拟DOM渲染成真是DOM之后才执行,而useLayoutEffect在创建虚拟DOM之后就执行了。如果在callback中又修改了状态值,对于useEffect来讲,第一次真是DOM已经渲染,组件更新会重新渲染真是的DOM,所以频繁切换的时候会出现内容闪烁。

对于useLatouyEffect来讲,第一次真实的DOM还没渲染,遇到callback中更新状态,视图立即更新,创建出来的虚拟DOM和上一次的虚拟DOM合并在一起渲染成真实DOM,也就是真实DOM就渲染了一次,不会出现内容的闪烁。

  • useLayoutEffect设置的callback要优先于useEffect去执行
  • 在两者设置的callback中,依然可以获取DOM元素,真是的DOM已经创建了,区别在于浏览器是否渲染
  • 如果在callback中又修改了状态,useEffect之前已经渲染了真是的DOM,会第二次去渲染;useLayoutEffect会合并两次虚拟DOM,合成一次去渲染真是的DOM。

视图更新步骤

  1. 基于babel-preset-react-app把jsx编译成createElement
  2. 把createElement执行,创建出虚拟DOM
  3. 基于root.render把虚拟DOM渲染成真是DOM。useLayoutEffect会阻塞第四步操作,先去执行cllback然后再去执行第四步(和第四不步是同步),而useEffect会和第四步同事执行(和第四步是异步的)
  4. 浏览器绘制成真是的额DOM对象

useRef

在函数组件中使用ref,基于ref = {(x)=>{refs1 = x}} 先定义一个变量来接受dom,但是这种方式不推荐用在函数组件中。

let refs1 // 定义一个变量
useEffect(() => {
    console.log(refs1);
});
<div ref={(x)=>{refs1 = x}}>  // 第一个参数是DOM元素
    {student.name}---{student.age}--{student.sch.name}
</div>

通过**React.createRef()**方法也可以

let box = React.createRef()
useEffect(() => {
    console.log(box);
});
<div ref={box}>
    {student.name}---{student.age}--{student.sch.name}
</div>

通过useRef()

useRef()创建的就是一个ref对象。

let useref = useRef(null)
useEffect(() => {
    console.log(useref);
});
<div ref={useref}>
    {student.name}---{student.age}--{student.sch.name}
</div>

在函数组件中尽量用useRef,因为性能比React.createRef好。

useImperativeHandle

useImperativeHandle可以配合React.forwardRef可以获取函数组件内部的状态和方法。

用React.forwardRef包裹组件 组件有两个参数是 props和ref,这个ref就是ref对象。

import React,{useImperativeHandle, useState} from 'react'
// 用React.forwardRef包裹组件  组件有两个参数是props和ref
const FunChild = React.forwardRef(function(props,ref){
    const [text, setText] = useState("张三");
    const submit = ()=>{
        console.log("我是submit");
    }
    // 用这个hooks将需要状态和方法返回
    useImperativeHandle(ref,()=>{
        return{
            text,
            submit
        }
    })
    return (
        <div>
            我的函数子组件
        </div>
    )
})
export default FunChild

在父组件中可以通过ref对象得到

  const childRef =  useRef(null)
  useEffect(() => {
    console.log(childRef); // 这里就可以得到子组件的状态和方法。
  });
<FunChild ref={childRef}></FunChild>

useMemo

函数组件的每次更新,都是把函数重新执行一次,内部代码也要重新执行一次。

第一次渲染组件的时候,callback会执行,后期只有依赖值发生改变的时候,callback才执行,每一次执行都会把callback返回的结果赋值给变量xxx。

const xxx = useMemo(()=>{return a+b })

useMemo有一个缓存效果,在依赖的状态值没有发生改变时,callback没有触发执行的时候,xxx得到的就是上一次计算出来的结果,和vue中的计算属性相似。相当于缓存结果值

const total =   useMemo(()=>{
   return num+num2
  },[num,num2])

useCallback

函数组件的每一次更新,都是把函数重新执行一遍,产生一个新的闭包,在闭包中所创建的函数操作,都会创建新的堆内存,也就是内部的函数都会重新创建。有些函数我们只需要执行一次就行了,没必要每次更新都去执行,怎么解决这个问题呢,可以用useCallback。

 const xxx =  useCallback(()=>{
  },[])
  • 组件第一次更新,useCallback执行,创建一个callback函数赋值给xxx

  • 组件后续更新,判断依赖的状态是否改变,如果改变则会重新创建函数,重新赋值给xxx,但是如果,依赖的状态值没有改变或者没有设置依赖值,则xxx获取的一直是第一次创建的函数,不会创建新的函数进来。

简单来说useCallback就是缓存函数的,当依赖发生变化的时候才重新创建函数。

用途

诉求:当父组件更新的时候,传递给子组件的函数不随着父组件更新而更新。

可以将这个传递给子组件的函数用useCallback()

  • 传递给子组件的属性,每一次需要相同的内存地址,基于callback处理
  • 在子组件内部也要做一个处理,验证父组件的属性是否改变,如果没有改变,则子组件不能更新,有变化才需要更新

自定义hooks

将某些组件的逻辑可以提取到可重用的函数中。

创建一个函数,名字需要是useXxx,后去可以在组件中调用这个方法。

const usePart = function (initValue) {
    const [state, setState] = useState(initValue);
    // 支持状态的改变
    const setPart = (initValue)=>{
        return setState(initValue)
    }
    return [state,setPart]
}

组件之间的通信

父子通信

父组件通过props中的属性来传递属性,子组件通过callback函数将子组件的信息传递给父组件。

父组件把信息传递给子组件--基于信息及即可

子组件想修改父组件的值,父组件提供一个方法,这个方法通过属性传递给子组件,子组件执行即可。

类组件

// 父组件
import React, { Component } from 'react'
import ClassChildren from './ClassChildren';
export default class ClassComMes extends Component {
  state  = {
    name:'张三',
    age:12
  }
  childTop = (value)=>{
    console.log(value);
  }
  render() {
    return (
      <div>
        ClassComMes
            {// 通过属性来传递信息,父组件定义 handle方法,子组件触发handle方法}
        <ClassChildren message={this.state} handle={(value)=>{this.childTop(value)}}/>
      </div>
    )
  }
}

// 子组件
import React, { Component } from 'react'
import {Button} from 'antd'
export default class ClassChildren extends Component {
  render() {
      // 将this.props解构出来,
    let {message,handle} = this.props
    console.log(this.props);
    return (
      <div>
    	{// 父组件传递的信息}
        {message.name}-{message.age}
        <div>
          {// 子组件通过父组件传递来的方法,执行这个方法传递参数,可以修改父组件的状态}
          <Button onClick={()=>{handle("我是子组件传递给父组件的消息")}}>子传父</Button>
        </div>
      </div>
    )
  }
}

如果想传递标签可以用前面讲的插槽。

函数组件的父子通信一样

// 父组件
import React,{useState,useCallback} from 'react'
import FuncChildren from './FuncChildren';
export default function FuncComMsg() {
  const [message, setMessage] = useState({
    name:'张三',
    age:11
  }); 
    {// 用useCallback 当值不发生变化的时候,子组件不会再渲染}
  const  change =useCallback( (value)=>{
      setMessage({
          ...message,
          name:value
   })
  },[message])
   return (
    <div>
      <FuncChildren message={message} change={(value)=>{change(value)}}></FuncChildren>
    </div>
  )
}

// 子组件
import React from 'react'
import PropTypes from 'prop-types'
 export default function FuncChildren(props) {
  let {message,change} = props
  console.log("子组件渲染次数");
  return (
    <div>
      {message.name}-----{message.age}
      <button onClick={()=>{change("李四")}}>富川字</button>
    </div>
  )
}
FuncChildren.propTypes = {
  message: PropTypes.object,
  change: PropTypes.func
}

用了useCallback子组件除了第一次渲染,后面都没有渲染了,useCallback一定不要乱用,如果没有设置任何依赖,函数只会在第一次渲染。

跨代通信(爷孙通信)

第一种:父传子,在子传子的子。

第二种:上下文

祖先组件需要把信息放在上下文中,后代组件直接去上下文中获取。

类组件

  1. 创建上下文对象。

    // ContextEvent.js中
    import React from React;
    // 创建上下文对象
    const Context = React.createContext();
    export default Context;
    
  2. 让祖先组件具备状态和修改状态的方法,同时把这些信息放在上下文中。

    基于上下文对象中提供了一个Provider组件,组件中通过value来存储要传递的信息。

    import React, { Component } from 'react'
    import ClassChildren from './ClassChildren';
    
    import ContextEvent from './ContextEvent.js'
    export default class ClassComMes extends Component {
      state  = {
        name:'张三',
        age:12
      }
      childTop = (value)=>{
        console.log(value);
      }
      render() {
        return (
          <div>
            {/* ContextEvent,这个value就是存放要传递的信息 */}
           <ContextEvent.Provider value={{...this.state,handle:this.childTop}}>
              <ClassChildren />
            </ContextEvent.Provider>
            
          </div>
        )
      }
    }
    
  3. 在后代组件中获取上线文信息。

    方法一:导入ContextEvent,ContextEvent中有一个Consumer组件,这个组件中有一个函数插槽,这个函数的第一个参数context就是祖先组件传递过来的内容,然后这个函数返回这个视图 <ContextEvent.Consumer >{(context)=>{rerturn (

    )}}</ContextEvent.Consumer >

    import React, { Component } from 'react'
    import ContextEvent from './ContextEvent'  // 导入这个context
    export default class ClassChildren extends Component {
        render() {
            return (
                <ContextEvent.Consumer >
                    {
                        (context)=>{ // 这个context就是祖先组件传递过来的值
                            let {name,age,changeHandle} = context // 将这个值解构出来
                            return(
                                <div>
                                    {name}--{age}
                                    <div>
                                        <button onClick={()=>{changeHandle("万物")}}>子传父</button>
                                    </div>
                                </div>
                            )
                        }
                    }
                </ContextEvent.Consumer>
    
            )
        }
    }
    
    // 方法二
    import React, { Component } from 'react'
    import ContextEvent from './ContextEvent' // 导入上下文对象
    export default class ClassChildrenChild extends Component {
      static contextType = ContextEvent  // 给定一个静态属性,将ContextEvent赋值给contextType,contextType是固定的写法
      render() {
        console.log(ContextEvent);
        let {name,age,changeHandle} = this.context // 传递的内容在context中,context也是固定的
        return (
          <div>
            {name}--{age}
          </div>
        )
      }
    }
    
    

    方法二:需要给定一个固定命名的contextType静态属性,将上下文对象赋值给contextType,然后获取this.context,传递的内容就在context中。

    函数组件

    第一步跟class组件一样的创建context

    第二步也是一样的,通过ContextEvent.Provider的value来传递值

    import React,{useCallback, useState} from 'react'
    import FuncChildren from './FuncChildren';
    import ContextEvent from './ContextEvent';
    export default function FuncComMsg() {
      const [message, setMessage] = useState({
        name:'张三',
        age:11
      }); 
      const  change =useCallback( (value)=>{
       setMessage({
        ...message,
        name:value
       })
      },[message.name])
       return (
        <div>
          <ContextEvent.Provider value={{...message,change:change}}>
              <FuncChildren message={message} change={(value)=>{change(value)}}></FuncChildren>
          </ContextEvent.Provider>
          
        </div>
      )
    }
    

    第三步获取上下文中的数据的时候第一种方式,通过ContextEvent.Consumer

    第二种方式通过useContext来得到context

    import React from 'react'
    import ContextEvent from './ContextEvent'
    export default function FuncChildrenChild(props,context) {
      return (
        <div>
          <ContextEvent.Consumer>
            {(context)=>{
              return (
                <div>
                  {context.name}
                </div>
              )
            }}
          </ContextEvent.Consumer>
        </div>
      )
    }
    
    // 方式二,通过hooks函数useContext
    import React,{useContext} from 'react'
    import ContextEvent from './ContextEvent'
    export default function FuncChildrenChild(props,context) {
      const {name,age,change} = useContext(ContextEvent); // 获取上下文信息
      return (
        <div>
          {name}
        </div>
      )
    }
    

React样式私有化处理

在组件化中,各个组件之间的样式可能会冲突。

内联样式

这样确实可以保证组件与组件之间的样式不会冲突。但是不利于样式的复用;不能用伪类;不利于优化。

<div style={{color:'red'}}>
    ssss
</div>

样式命名技巧

保证最外层类名不冲突,例如:路径-组件名-类名

后期组件内部的元素,其样式都基于less/scss嵌入到最外层容器的样式类名之下去编写。

通过xxx.module.css

可以基于css Modules实现样式私有化管理,把样式都写在xxx.module.css文件中,这样的文件不能写less/sass等预编译语言;

在组件中基于ES6Module导入样式。

高阶组件

利用js中的闭包【函数柯力化】实现组件的代理。

我们可以在代理组件中,经过业务逻辑处理,获取一些信息,最后基于属性等方案,传递给我们最终要渲染的组件。

// 父组件 
<FuncChildrenChild a={4} b={5}></FuncChildrenChild>
// 子组件
import React,{useContext} from 'react'
import ContextEvent from './ContextEvent'
 function FuncChildrenChild(props,context) {
  const {name,age,change} = useContext(ContextEvent);
  return (
    <div>
      {name}
    </div>
  )
}
// 执行这个方法,传递一个组件进来。
const proxyText = function proxyText(Component){
  return function HOC(props){
    console.log(props);
    return (
      <div>
        {/* 真实渲染的是FuncChildrenChild组件 */}
        <Component {...props}></Component>
      </div>
export default proxyText(FuncChildrenChild) // 把函数执行的返回结果,基于es6module规范导出,

Redux

redux流程

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。提供公共状态管理。

父子组件一般用props/ref/redux等,其他组件一般都是用redux。

  1. 创建全局公共容器,用来存储各组件需要的公共信息。

    const store = createStore(reducer)
    在创建的容器中分为两部分:公共状态和事件池(存放让状态更新的方法)
    公共状态一旦改变,会默认立即通过事件中方法执行,这些方法的执行只要就是通知组件的更新,就可以获取最新的状态信息。
    修改公共状态不能直接修改,必须基于dispatch派发,在reducer中进行更新。
    
  2. 在组件内部获取公共状态信息,然后渲染。

    store.getState()
    
  3. 把让组件可以更新的方法放在公共容器中。后期公共状态改变了,事件池的方法按照顺序依次执行,也就是对应的组件也更新,也就可以从store容器中获取最的状态进行渲染。

    store.subscribe(fun)
    
  4. 创建容器的时候需要一个reducer

    let init = {...}
    const reducer = function reducer(state,action){
        //state 容器的状态
        //action 派发的行为对象,必须具备type属
        switch (action,type){
                /根据传递的type值来修改不同的状态属性
        }
        return state // 返回的信息会替换store中的状态
    }
    
  5. p派发任务,通知reducer进行修改

    store.dispathch({type:xxxx,
                    })
    

redux示例

新建一个store/index.js文件

store/index.js

import { createStore } from "redux";
// 管理员:为了修改store容器中的公共状态,初始化状态值
let initial = {
    num: 10,
    age: 20,
};
const reducer = function (state = initial, action) {
    // state 就是存储store的公共状态,最开始没有状态就赋值为初始状态
    // action 每一次基于dispatch派发的时候,传递过来的行为对象,必须具备type属性,具备派发的属性

    // 基于派发的行为标识,修改容器中的公共状态信息
    newState = { ...state };
    switch (action.type) {
        case "add":
            state.num++; // 不能直接修改状态,等到return,故我们需要克隆一份
            newState.num++;
            break;

        default: // 第一次派发不会跟任何type匹配,就返回默认的状态
            return newState;
    }
    // 返回这个状态将原来的状态替换掉
    return newState;
};

// // 每次派发都会把reducer执行
// store.dispatch({
//     type: "add",
//     step: 10,
// });


const store = createStore(reducer);
export default store;

在组件中通过导入store/index.js 根据store.getState()来得到状态

函数组件

import React,{useState,useEffect} from 'react'
import store from './store/index'// 导入状态
export default function ReduxStydy() {
  let {num,age} = store.getState() // 通过store.getState()来获取状态
  console.log(store);

  // 函数组件只有状态改变的时候组件才会渲染、组件第一次渲染完毕之后,把组件更新的方法放在store事件池中
  const [num1, setNum1] = useState(0); // 让这个状态去触发函数组件的更新
  // 组件更新
   const updater = ()=>{
    setNum1(num1+1)
  }
  useEffect(() => {
    // let unsubcirbe = store.subscribe(让组件更新的方法)
    // 把组件更新的方法放在事件池中,返回的是一个方法,移除事件池方法的函数
  let  unsubscirbe = store.subscribe(updater)
  return ()=>{
    unsubscirbe() // 把上一次创建的updater移除掉
  }
  }, [num1]);

  // 改变公共状态的方法  点击按钮的时候改变
 const changeNum=()=>{
    store.dispatch({type:'add'})
  }
  return (
    <div>
      num:{num}
      <br />
      age:{age}
      <br />
      <button onClick={()=>{changeNum()}}>change</button>
    </div>
  )
}

类组件

import React, { Component } from 'react'
import store from './store/index'
export default class ReduxStydy extends Component {
    // 第一次渲染完毕之后绑定store事件池中更新组件的方法,this.forceUpdate()就可以更新组件
    componentDidMount(){
        store.subscribe(()=>{
            this.forceUpdate()
        })
    }
    changeNum = ()=>{
        store.dispatch({type:'add'})
    }
    render() {
        // 获取公共状态
        let  states = store.getState()
        let {num,age} = states
        return (
            <div>
                num:{num}
                <br />
                age:{age}       <br />
                <button onClick={()=>{this.changeNum()}}>change</button>
            </div>
        )
    }
}

总结一下

  • 创建一个公共的初始状态

    let initlal  = {
        num:10
    }
    
  • 创建reducer,创建reducer方法,有两个参数,第一个参数是state,默认值就是初始的公共状态,第二个状态是action,action 中有一个type属性,这个属性我理解的就是修改的状态的方法名,到时候页面修改的dispatch方法会传递一个type属性归来,在reducer中判断这个type属性值来决定怎么操作这个公共状态,是增加值?还是什么样的操作。值得注意的是,修改state不能直接修改,是在reducer方法最后返回新state替换之前的state,不能直接修改我们可以克隆一份state。

    const reducer = function (state = initial, action) {
        // state 就是存储store的公共状态,最开始没有状态就赋值为初始状态
        // action 每一次基于dispatch派发的时候,传递过来的行为对象,必须具备type属性,具备派发的属性
    
        // 基于派发的行为标识,修改容器中的公共状态信息
        newState = { ...state };
        switch (action.type) {
            case "add":
                state.num++; // 不能直接修改状态,等到return,故我们需要克隆一份
                newState.num++;
                break;
    
            default: // 第一次派发不会跟任何type匹配,就返回默认的状态
                return newState;
        }
        // 返回这个状态将原来的状态替换掉
        return newState;
    };
    
  • 创建公共状态仓库,将reducer函数穿进去。

    const store = createStore(reducer)
    export default store
    
  • 通过 store.getState() 来得到公共的状态

  • 将更新组件的的方法添加到store的事件池中,在函数组件中只有状态改变了才会重新渲染组件,所以只需要让状态更新的方法放入到组件中就可以了.然后通过副作用函数useEffect来添加方法,将事件更新到事件池的方法时store.subscribe(updateFunc),而且还需要再状态更新的时候执行,就给useEffect添加依赖,把上一次事件池中的更新事件清楚。

      // 函数组件只有状态改变的时候组件才会渲染、组件第一次渲染完毕之后,把组件更新的方法放在store事件池中
      const [num1, setNum1] = useState(0); // 让这个状态去触发函数组件的更新
      // 组件更新
       const updater = ()=>{
        setNum1(num1+1)
      }
      useEffect(() => {
        // let unsubcirbe = store.subscribe(让组件更新的方法)
        // 把组件更新的方法放在事件池中,返回的是一个方法,移除事件池方法的函数
      let  unsubscirbe = store.subscribe(updater)
      return ()=>{
        unsubscirbe() // 把上一次创建的updater移除掉
      }
      }, [num1]);
    
    • 然后时更新公共状态的方法:用store.dispatch({type:'xxxxx'})来更新,一般点击时候的执行这个方法。
    • 给事件池添加更新组件方法的原因:当公共状态修改了的时候,她会自动去调用更新组件的方法,组件更新了,得到的就是最新的公共状态,如果组件不更新,得到的还是原来的旧状态。

redux工程化

按照模块化把reducer进行单独管理,每个模块儿都有自己的reducer,最后我们还需要把所有的reducer进行合并,合并为一个。赋值给我们创建的store。

创建NumReducer

const inital = {
    num: 10,
};
export default function NumReducer(state, action) {
    const newState = { ...inital };
    if (action.type === "add") {
        newState.num++;
    } else {
        return newState;
    }
    return newState;
}

创建AgeReducer


const inital = {
    age: 20,
};
export default function AgeReducer(state = inital, action) {
    const newState = { ...state };
    if (action.type === "addAge") {
        newState.age++;
    } else {
        return newState;
    }
    return newState;
}

合并每个reducer

每个模块儿的reducer创建出一个总的reducer,,此时容器中的公共状态会按照我们设置的成员名字来管理。

c// 合并reducer
import { combineReducers } from "redux";
import NumReducer from "./NumReducer";
import AgeReducer from "./AgeReducer";

const reducer = combineReducers({
    NumReducer,
    AgeReducer,
});
export default reducer;

// 会像这样去管理,我们通过store.getState()获取的就是这个整个state,
//state = {
//     NumReducer: {
//         num: 10,
//     },
//     AgeRedcer: {
//         age: 20,
//     },
// };

创建容器

import { createStore } from "redux";
import reducer from "./reducer"; // 引入总的reducer
const store = createStore(reducer); 
export default store;

在组件中

store.getState()的得到的是一个对象,对象中有NumReducer和AgeReducer属性,然后每个属性是一个对像,就是真正的状态

import React,{useState,useEffect} from 'react'
import store from './store/index'// 导入状态
export default function ReduxStydy() {
  let {AgeReducer,NumReducer} = store.getState() // 通过store.getState()来获取状态
  // 函数组件只有状态改变的时候组件才会渲染、组件第一次渲染完毕之后,把组件更新的方法放在store事件池中
  const [num1, setNum1] = useState(0); // 让这个状态去触发函数组件的更新
  // 组件更新
   const updater = ()=>{
    setNum1(num1+1)
  }
  useEffect(() => {
    // let unsubcirbe = store.subscribe(让组件更新的方法)
    // 把组件更新的方法放在事件池中,返回的是一个方法,移除事件池方法的函数
  let  unsubscirbe = store.subscribe(updater)
  return ()=>{
    unsubscirbe() // 把上一次创建的updater移除掉
  }
  }, [num1]);

  // 改变公共状态的方法  点击按钮的时候改变
 const changeNum=()=>{
    store.dispatch({type:'addNum',list:[2,1]})
  }
  return (
    <div>
      num:{NumReducer.num}
      <br />
      age:{AgeReducer.age}
      <br />
      <button onClick={()=>{changeNum()}}>change</button>
    </div>
  )
}

派发行为标识宏管理

每一次dispatch派发的时候,都会去每个reducer中找一遍,把所有和派发行为标识匹配的逻辑进行执行。如果团队人很多,这样派发的type可能会冲突。怎么保证派发标识的唯一性呢?

新建一个文件 action-types.js

// 统一管理行为标识
// 为了不冲突,将整个项目的type值写在这里

export const addNum = "addNum";

在reducer页面,将写死的type替换掉我们统一管理的就好了

import { addNum } from "./action-types"; // 引入统一管理的
const inital = {
    num: 10,
};
export default function NumReducer(state = inital, action) {
    const newState = { ...state };
    if (action.type === addNum) { // 直接使用统一管理的变量
        newState.num++;
    } else {
        return newState;
    }
    return newState;
}

在组件中也是直接导入使用

import { addNum } from './store/action-types';
...
  // 改变公共状态的方法  点击按钮的时候改变
 const changeNum=()=>{
    store.dispatch({type:addNum,list:[2,1]})
  }
...

派发行为对象统一管理

在store下新建一个action/numAction.js

给每个模块创建xxxAction.js,对每个模块的派发行为进行管理。

import { addNum } from "../action-types";
// 每个版块儿都有自己派发的行为管理
const numAction = {
    addNum() {
        return {
            type: addNum,
        };
    },
    // otherFunc
};

export default numAction;


将各个模块儿的action进行统一整合

在action文件夹下创建index.js

把每个版块的action进行合并成一个action即可

import numAction from "./numAction";
import ageAction from "./ageAction";

const action = {
    num: numAction,
    age: ageAction,
};

export default action;

在组件中直接导入action就可以了

import action from './store/actions/index';
...
 // 改变公共状态的方法  点击按钮的时候改变
 const changeNum=()=>{
    store.dispatch(action.num.addNum())
  }
 ...

目前来看这个步骤太多余了,反而让书写的步骤变得更加复杂了,之前直接写就可以了,还要写在每个模块儿下的action中。这个操作有什么意义呢?为react-redux做铺垫。

redux模块化总结

将标识统一管理

actions-types.js

// 统一管理行为标识
// 为了不冲突,将整个项目的type值写在这里

export const addNum = "addNum";

创建每个模块的reducer

numReducer.js

import { addNum } from "./action-types"; // 引入宏管理标识
const inital = {
    num: 10,
};
export default function NumReducer(state = inital, action) {
    const newState = { ...state };
    if (action.type === addNum) {
        newState.num++;
    } else {
        return newState;
    }
    return newState;
}

将每个模块的reducer合并

reducer.js

// 合并reducer
import { combineReducers } from "redux";
import NumReducer from "./NumReducer";
import AgeReducer from "./AgeReducer"; // 其他模块的reducer

const reducer = combineReducers({
    NumReducer,
    AgeReducer,
});

export default reducer;

创建store容器

store.js

import { createStore } from "redux";
import reducer from "./reducer";
const store = createStore(reducer);
export default store;

创建派发行为统一管理

action/numAction.js

import { addNum } from "../action-types";
// 每个版块儿都有自己派发的行为管理
const numAction = {
    addNum() {
        return {
            type: addNum,
        };
    },
    // otherAction
};
export default numAction;

合并派发行为

action/index.js

import numAction from "./numAction";
import ageAction from "./ageAction";

const action = {
    num: numAction,
    age: ageAction,
};

export default action;

在组件中使用

import React,{useState,useEffect} from 'react'
import store from './store/index'// 导入状态
import action from './store/actions/index'; // 导入派发行为
export default function ReduxStydy() {
  let {AgeReducer,NumReducer} = store.getState() // 通过store.getState()来获取状态
  // 函数组件只有状态改变的时候组件才会渲染、组件第一次渲染完毕之后,把组件更新的方法放在store事件池中
  const [num1, setNum1] = useState(0); // 让这个状态去触发函数组件的更新
  // 组件更新
   const updater = ()=>{
    setNum1(num1+1)
  }
  useEffect(() => {
    // let unsubcirbe = store.subscribe(让组件更新的方法)
    // 把组件更新的方法放在事件池中,返回的是一个方法,移除事件池方法的函数
  let  unsubscirbe = store.subscribe(updater)
  return ()=>{
    unsubscirbe() // 把上一次创建的updater移除掉
  }
  }, [num1]);

  // 改变公共状态的方法  点击按钮的时候改变
 const changeNum=()=>{
    store.dispatch(action.num.addNum()) // 用统一管理的派发行为
  }
  return (
    <div>
      num:{NumReducer.num}
      <br />
      age:{AgeReducer.age}
      <br />
      <button onClick={()=>{changeNum()}}>change</button>
    </div>
  )
}

react-redux

react-redux 可以让redux使用起来更加方便。

主要是在组件运用中使用方便一些。

1.内部自己创建了上下文对象,可以把store放在了上下文对象,在组件中可以直接使用。

2.不用自己去创建一个状态来手动刷新函数组件了,会自动刷新。

import ReactReux from "./ReactReux"
import store from './store/index'
import { Provider } from 'react-redux'
export default function App() {
  return (
    <div>
      <Provider store={store}>
          <ReactReux />
      </Provider>
    </div>
  )
}

2.在组件中获取公共的状态信息绑定,无需自己基于上下文对象获取store,也无需自己getState获取,直接基于react-redux提供的connect函数处理即可,而且不需要我们手动让组件更新的方法放在事件池中,react-redux给我们处理了。

connect(mapStateToProps,mapDispatchToProps)(Component)

第一个参数:将公共状态映射给属性,第二个参数:将派发方法映射给属性。柯力化的参数是将这些属性赋值给Component组件

mapStateToProps是一个函数,函数的第一个参数state就是redux中的公共状态,只需要把公共状态作为属性返回回去即可

connect((state)=>{
    num:state.Numreducer.num // 将公共状态绑定给num属性,
},mapDispatchToProps)(Component)

mapDispatchToProps也是一个回调函数,第一个参数就是dispatch,就是执行需要派发的任务。返回的也是一个对象,在对象中需要绑定我们的函数属性。

import action from "./store/actions/index.js"
connect(null,(dispatch)=>{
    return{
        addNum(){ // 将派发的任务绑定到addNum函数
            dispatch(action.num.addNum()) // 需要派发的任务
        }
    }
})(Component)


上面的这种写法有些过时了。可以简化成这样去写
import action from "./store/actions/index.js"
connect(null,action.num)(Component) 

可以直接写成action.num就是因为我们在redux模块儿化的的时候,将每个模块抽取成单个的action之后,我们还将全部模块的action合并成了一个action

在组件中的完整使用

import React from 'react'
import { connect } from 'react-redux'
import action from './store/actions/index'
 function ReactReux(props) {
  console.log(props);
  return (
    <div>{props.num}
    <button onClick={
      ()=>{
        props.addNum()
      }
    }>加加</button>
    </div>
  )
}

export default connect(state=>{
  return {
    num:state.NumReducer.num
  }
},action.num)(ReactReux)

Redux中间件及处理机制

redux中间件:

  • redux-logger 每次派发在控制台输出开发日志,方便对redux进行调试
  • redux-thunk/redux-promise 实现异步派发。
import { createStore, applyMiddleware } from "redux";
import reduxLogger from "redux-logger";
import reducer from "./reducer";
const store = createStore(
    reducer,
    applyMiddleware(reduxLogger)
); // 使用中间件);

export default store;

image-20230909124649699

Object.defineProperty()

对象成员规则的限制:

  • Object.getOwnPropertyDescriptor(对象,成员):获取某个成员的规则
  • Object.getOwnPropertyDescriptor(对象):获取对象的全部规则

规则:

  • configgurable:是否可以删除
  • writable: 是否可以更改
  • enumerable:是否可枚举
  • value:成员值

Object.defineProperty(obj, x, {});设置规则

  1. 对某个成员设置规则
  • 如果成员已存在,则需改其规则

  • 如果成员不存在,则新增这个成员,并设置规则

    Object.defineProperty(obj, 'name', {
        enumerable: false,
        writabletrue: true,
        configurable: true,
        value: 100,
    });
    

数据劫持

Object.defineProperty(obj, "name", {
    get() {
        // 获取object.x成员信息的时候,就会触发get函数执行
        // 返回内容的是成员的值
        console.log("get被触发了");
        // return obj['name'];
    },
    set(val) {
        console.log("set触发");
    },
});

mobx

安装 yarn add mobx-react-lite mobx

import { makeAutoObservable } from "mobx";
class Store {
    // 公共状态
    num = 10;
    constructor() {
      // 将参数对象的属性设置成 observer state
      // 将参数对象中的方法设置成action
        makeAutoObservable(this);
    }
    // 修改公共状态的方法
    change() {
        this.num++;
    }
}
const store = new Store();
export default store;

在组件中

import React from 'react'
// 引入observer ,监听当前组件,当oberable state更新的时候,通知组件刷新
import { observer } from "mobx-react-lite"
import store from './mobx/index'
 function Mobx() {
  return (
    <div>
      {store.num}
      <div>
        <button onClick={()=>{store.change()}}>+</button>
      </div>
    </div>
  )
}
// 用observer将组件包裹
export default observer(Mobx)

react-router-dom

路由案例

路由页面

我们可以把BrowserRouter加在index.js或者是app.jsx中,然全局都被路由笼罩。

所有的路由规则放在中,每一条匹配规则还是基于

  • 路由匹配成功,不在基于Indexr控制渲染,而是基于element
  • 不在需要switch,默认一项匹配成功就不在往下匹配
  • 不再需要exact,每一项都是精准匹配
 <BrowserRouter>
     <Routes>
     	<Route path="/Home" element={<Home />} />
     </Routes>
 </BrowserRouter

标识history路由,还有一种是<HashRouter ?>哈希路由

Routes组件,将全部的标签都放在闭合标签中。基于单个路由匹配还是基于Route

<Route path="/Home" element={}/> path是访问的路径,element中的值是这个路径相匹配的组件

是路由出口,就相当于vue中的

重定向:<Route path="/" element={<Navigate to={{pathname:'/Home'}}>}> 输入/的时候会重定向到"/Home"中。

二级路由的话是直接在中使用闭合标签,在闭合标签中继续添加

如果是首页的话可以直接用index标记,就是首页 <Route index element={}>



root.render(<BrowserRouter ><App></App></BrowserRouter>);


import React from 'react'
import { BrowserRouter,Routes,Route,Navigate,Link, Outlet } from 'react-router-dom'
import My from './views/My'
import Center from './views/Center'
import Home from './views/Home'
import Home1 from './views/Home1'
export default function App() {
    // 在v6版本中移除了Switch\Redirect\withRoute
    return (
        <div>
            <BrowserRouter>
                <Link to="/">Home</Link> | 
                <Link to="/Center">Center</Link> | 
                <Link to="/My">my</Link> | 
                <Outlet></Outlet>
                <Routes>
                    {/* 重定向 */}
                    <Route path="/"  element={<Navigate to={{pathname:'/Home'}}></Navigate>}></Route>
                    <Route path="/Home"  element={<Home></Home>}>
                        {/* 二级路由 */}
                        <Route path="/Home/Home1" element={<Home1></Home1>}></Route>
                    </Route>
                    <Route path="/Center"  element={<Center></Center>}></Route>
                    <Route path="/My"  element={<My></My>}></Route>
                </Routes>
                <div>
                </div>
            </BrowserRouter>

        </div>
    )
}

路由传参

在v6版本中,即便组件是基于Route渲染的,也不会基于属性传递参数。想获取相关的参数就可以基于Hook函数处理。

  1. 确保使用的Hook的组件是在Router中的。
  2. 只要是基于Route内部包裹的组件,无论是否基于匹配渲染的,默认不在使用props传参,只能基于路由的hook获取

命令式导航

通过Link标签或者是Navigate标签来实现跳转

<Link to="/Center/4">Center</Link>  // 通过useParams来得到参数
<Link to="/My?name='张三'">my</Link>  // 通过useSearchParams来得到参数useSearchParams()[0].get('name')
<Link to={{pathname:'/Home/Home1'}}>Home1</Link>  // 通过useLocation来得到参数

编程式导航

通过useNavigate来实现跳转

import { useNavigate } from 'react-router-dom'

const navigate = useNavigate()
navigate('/c')


navigate({
    pathname:'/c',
    search:'?id=10'
}) 或者 navigate('/c?name=“zhangsan”') // 通过useSearchParams来获取值 useSearchParams()[0].get('name')


navigate('/c/id') // 通过useParams 

navigate('/c',{state:{a:10,b:20}}) //  // 通过useLocation来得到参数

路由表

创建router/routers.js文件夹

import { Navigate } from "react-router-dom";
import { lazy } from "react"; // 路由懒加载
import Home from "../views/Home";
import Home1 from "../views/Home1";
import Center from "../views/Center";
import My from "../views/My";
// 懒加载
const Home = lazy(() =>
    import("../views/Home")
);
const routes = [
    {
        path: "/",
        name: "/",
        element: () => {
            // 用函数的原因是,命中的时候才加载
            return <Navigate to="/Home"></Navigate>;
        },
    },
    {
        path: "/Home",
        name: "Home",
        element: <Home />,
        meta: {},
        // 二级路由
        children: [
            {
                path: "/Home/Home1",
                element: <Home1 />,
            },
        ],
    },
    {
        path: "/Center/:id",
        name: "Center",
        element: <Center></Center>,
        meta: {},
    },
    {
        path: "/My",
        name: "My",
        element: <My />,
        meta: {},
    },
];

export default routes;

创建一个router/index.js

import React, { memo } from "react";
import routes from "./routes";
import { Routes, useRoutes } from "react-router-dom";
const DefauleRoutes = memo(() => {
    return useRoutes(routes);
});
export default DefauleRoutes;

在App.js中

import React from 'react'
import {Link, Outlet,useNavigate } from 'react-router-dom'
import DefauleRoutes from './router/index'
export default function App() {
  return (
    <div>
        <Link to="/">Home</Link> | 
        <Link to="/Center/4">Center</Link> | 
        <Link to="/My?name='张三'">my</Link> | 
      <Outlet></Outlet>
        <DefauleRoutes></DefauleRoutes>
    </div>
  )
}

解决跨域

yarn add http-proxy-middleware

在src下面创建src/setupProxy.js

const proxy = require("http-proxy-middleware");

module.exports = function (app) {
    app.use(
        proxy.createProxyMiddleware("/api", {
            //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
            target: "http://app.cbbgs.com:8089", //配置转发目标地址(能返回数据的服务器地址)
            changeOrigin: true, //控制服务器接收到的请求头中host字段的值
            /*
      	changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
      	changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
      	changeOrigin默认值为false,但我们一般将changeOrigin值设为true
      */
            pathRewrite: { "^/api": "" }, //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
        }),
        // 配置多个
        proxy.createProxyMiddleware("/api2", {
            target: "http://localhost:5001",
            changeOrigin: true,
            pathRewrite: { "^/api2": "" },
        })
    );
};