React基礎

一. 安装React

  • npm install -g create-react-app

二. 创建项目

  • create-react-app 项目名称

三. 启动

  • yarn start

四. 查看webpack配置

  • yarn eject

五. 组件的定义方式

  1. 使用class关键字创建组件的特点:
    • 使用class关键字创建的组件,有自己的私有数据(this.state)和生命周期函数;
    • 用class关键字创建出来的组件叫做有状态组件【用的最多】
  2. 使用function关键字创建组件的特点:
    • 使用function创建的组件,只有props, 没有自己的私有数据和生命周期函数;
    • 用构造函数创建出来的组件:叫做无状态组件【无状态组件用的不多】

无关的东西

const flag = false; 区分 自动播放(false) 和 拖动播放(true)

if(!flag) 等价于 (flag === false) { //  if(flag) 拖动了  if(!flag)没有拖动

...逻辑

}

六.生命周期

(Mounting)挂载

  1. constructor

    • 通过给 this.state赋值对象来初始化内部的state;

    • 为事件绑定实例(this);

  2. rander

    这个钩子函数虽然是组件的渲染方法,但并不是真正意义上的把元素渲染成dom的方法,在它之后还有一个把元素渲染成dom的步骤,这一步只是return一个元素对象,这个并不是组件的执行渲染功能的函数,
    • 渲染组件 第一次进入时会触发
  3. componentDidMount

    componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。

    • 依赖于DOM的操作可以在这里进行;
    • 在此处发送网络请求最好的地方;(官方建议)
    • 可以在此处添加一些订阅(会在componentWillUnmount取消订阅)

    image-20210611163752592

(Updating)更新时

  1. componentDidUpdate

    componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。

    • 当组件更新后,可以在此处对 DOM 进行操作;
    • 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)

image-20210611164907067

(Unmounting)卸载

componentWillUnmount() 会在组件卸载及销毁之前直接调用。

  1. componentWillUnmount

    • 在此方法中执行必要的清理操作;
    • 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等;

    image-20210611170737790

不常用生命周期

除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数:

  • getDerivedStateFromProps:state 的值在任何时候都依赖于 props时使用;该方法返回一个对象来更新state;
  • getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置);
  • shouldComponentUpdate:该生命周期函数很常用,但是我们等待讲性能优化时再来详细讲解;

详细描述:

image-20210611170952660

image-20210611171216903

七. 父传子通信-父传子

  1. class类组件

    /*
    父组件: 通过自定义属性进行传值
    子组件: 通过this.props接收数据
     */
    
    // 子组件
    class Son extends Component {
      constructor(props) { // 这个constructor 可以有可以无, 因为内部有默认的constructor
        super(props);      // constructor接受到参数, 然后通过props传给父类,
        this.props = props // super虽然代表父类的构造函数, 但是返回的是子类的实例, 即super内部的this指的是子类
      }
      render() {
        let {name, age, sex} = this.props // 此时的 this指的是子类的this
        return (
          <div>
            <h2>子组件接受父组件传来的数据: {name + " " + age + " " + sex}</h2>
          </div>
        )
      }
    }
    
    // 父组件
    export default class App extends Component {
      render() {
        return (
          <div>
            <Son name="chenlong" age="23" sex="男"></Son>
          </div>
        )
      }
    }
    
  2. function组件

    // 子组件
    function Son(props) {
      const {name, age, sex} = props
      return (
        <div>
          <h2>{name + " " + age + " " + sex}</h2>
        </div>
      )
    }
    
    // 父组件
    export default class App extends Component {
      render() {
        return (
          <div>
            <Son name="chen" age="23" sex="男"></Son>
          </div>
        )
      }
    }
    

八. 父传子通信-属性验证

  • 使用 PropTypes 类型检查

    import PropTypes from 'prop-types';
    
  1. function写法

    function Son(props) {
      const { name, age, sex} = props
    
      return (
        <div>
          <h2>{name + age + sex}</h2>
        </div>
      )
    }
    
    Son.propTypes = {
      name: PropTypes.string,
      age: PropTypes.number,
      sex: PropTypes.string
    }
    
  2. class类写法

    class Son extends Component{
      static propTypes = { // 属性验证
        name: PropTypes.string,
        age: PropTypes.number,
        sex: PropTypes.string
      }
    
      static defaultProps = { // 默认数据
        name:'王小虎',
        age: '24',
        sex: '男'
      }
    
      render() {
        let {name, age, sex} = this.props
        return (
          <div>
            <h2>{name + age + sex}</h2>
          </div>
        )
      }
    }
    
  3. 验证没通过报错:

    image-20210612012548007

九一. 父传子通信-子传父

/* 
父组件: 给子组件传递一个属性名, 并且绑定自己的函数
子组件: 通过触发this.props.父组件传递的属性名('需要传的值')
*/

class Son extends PureComponent {
render() {
  return (
    <div>
      <button onClick={e => {this.handlePush()}}>我是子组件</button> // 1.子组件创建一个事件
    </div>
  )
}
  handlePush(){
    this.props.SonComment('我是子组件传来的值') // 3. 子组件调用父组件传来的SonComment函数, 并且携带需要传的值
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <Son SonComment ={info => this.submitComment(info)} /> // 2.父组件自定义一个属性名SonComment
      </div>
    )
  }
  submitComment(val){ // 4.由于SonComment 绑定了submitComment事件,所以可以拿到子组件传来的值
    console.log('父组件-----' + val)
  }
}

九二. 父传子通信-子调用父组件的方法

/* 
子组件: 通过接受父组件传递过来的事件名字, 给自己绑定上
父组件: 传递一个事件名字
*/

//写法1 子组件
class Son extends Component {
  render() {
    const { add } = this.props // 3.接受父组件传递过来的事件名字, 给自己绑定上 
    return (
      <div>
        <button onClick={add}>子组件调用父组件+1</button> // 4.绑定在自己身上 
      </div>
    )
  }
}

//写法2 子组件
class Son extends Component {
  render() {
    const { add } = this.props // 3.接受父组件传递过来的事件名字, 给自己绑定上 
    return (
      <div>
        <button onClick={this.addComment}>子组件调用父组件+1</button> // 4.绑定在自己身上 
      </div>
    )
  }
}


// 父组件
export default class App extends Component {
  constructor(props){
    super(props)

    this.state = {
      counter: 0
    }
  }
  render() {
    return (
      <div>
        <h2>{this.state.counter}</h2>
        <button onClick={e => {this.add()}}>父组件+1</button>    
        {/* 2.给子组件传递一个叫add 的值(其实传的是一个函数) */} 
        <Son add={e => {this.add()}}></Son>   
      </div>
    )
  }
  add(){ // 1.先创建一个事件
    this.setState({
      counter: this.state.counter+1
    })
  }
}

十. Slot组件插槽

  1. children实现
    • 每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容。

      比如

    • 如果只有一个元素,那么children指向该元素;

    • 如果有多个元素,那么children指向的是数组,数组中包含多个元素;

    • 弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;

    实现方式:

    // 父组件
    export default class App extends Component {
      render() {
        return (
          <div>
            <h2>父组件:</h2>
            <Navbar>
              <span>slot-left</span>
              <span>slot-center</span>
              <span>slot-right</span>
            </Navbar>
          </div>
        )
      }
    }
    
    // 子组件
    export default class Navbar extends Component {
      render() {
        return (
          <div className="nav-wrap">
            <div className="nav-item nav-left">
              {this.props.children[0]} {/* 通过 props.children取值 */}
            </div>
            <div className="nav-item nav-center">
            {this.props.children[1]}
            </div>
            <div className="nav-item nav-right">
            {this.props.children[2]}
            </div>
          </div>
        )
      }
    }
    
    

    源码分析:

    image-20210612131717171

  2. props实现
    • 通过具体的属性名,可以让我们在传入和获取时更加的精准;

    实现方式:

    // 父组件 App.js
    export default class App extends Component {
      render() {
        const slotLeft = <div>slot-left</div>
        const slotCenter = <div>slot-center</div>
        const slotRight = <div>slot-right</div>
        return (
          <div>
            <h2>父组件:</h2>
            <h2>props实现:</h2>
            <Navbar2 slotLeft={slotLeft} slotCenter={slotCenter} slotRight={slotRight}></Navbar2>
          </div>
        )
      }
    }
    
    // 子组件 Navbar.js
    export default class Navbar2 extends Component {
      render() {
        const { slotLeft, slotCenter, slotRight } = this.props
        return (
          <div className="nav-wrap">
            <div className="nav-item nav-left">
              {slotLeft}
            </div>
            <div className="nav-item nav-center">
              {slotCenter}
            </div>
            <div className="nav-item nav-right">
               {slotRight}
            </div>
          </div>
        )
      }
    }
    
    

十一.跨组件通信

  1. 中间组件层层传递

    • 但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)。
    • 如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。
    // 第二层组件
    function Navbar(props) {
      return (
        <div>
          <h2>用户昵称: {props.nickName}</h2>
          <h2>用户等级: {props.level}</h2>
        </div>
      )
    }
    // 第一层组件
    class Main extends Component {
      render() {
        return (
          <div>
            <Navbar nickName={this.props.nickName} level={this.props.level}></Navbar>
            <ul>
              <li>设置1</li>
              <li>设置2</li>
              <li>设置3</li>
              <li>设置4</li>
            </ul>
          </div>
        )
      }
    }
    // 父组件
    export default class App extends Component {
      constructor(props){
        super(props)
        this.state = {
          nickName : 'chen',
          level: 99
        }
      }
      render() {
        const { nickName, level } = this.state
        return (
          <div>
            <Main nickName={nickName} level={level}></Main>
            <h2>其他内容</h2>
          </div>
        )
      }
    }
    

    我这边顺便补充一个小的知识点:Spread Attributes

    属性展开:

    如果你已经有了一个 props 对象,你可以使用展开运算符 ... 来在 JSX 中传递整个 props 对象。以下两个组件是等价的:

image-20210612160421579

但是,如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:

  • React提供了一个API:Context;
  • Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props;
  • Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言
2. Context相关的API

2.1 React.createContext 第一步

const MyContext = React.createContext(defaultValue);
  • 创建一个需要共享的Context对象:

  • 如果一个组件订阅了Context, 那么这个组件会从离自身最近的那个匹配的 Provider中读取到当前的context值

  • defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值(可以在里面放对象)

    2.2 Context.Provider 第二步

<MyContext.Provider value={/* 某个值 */}> // value: 代表需要共享的值
  • 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:

  • Provider 接收一个 value 属性,传递给消费组件;

  • 一个 Provider 可以和多个消费组件有对应关系;

  • 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;

  • 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;

    2.3 Class.contextType 第三步

挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:

  • 这能让你使用 this.context 来消费最近 Context 上的那个值;
  • 你可以在任何生命周期中访问到它,包括 render 函数中;
MyClass.contextType = MyContext // myclass组件要订阅--> MyContext上的context(也就是共享出来的值)

源码

image-20210612175623209

由于context是源码里已经声明了, 所以可以直接使用这个属性

↑↑↑ 以上是class式组件订阅context的写法

2.4 Context.Consumer 函数式组件订阅context的方法

  • 这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。

  • 这里需要 函数作为子元素(function as child)这种做法;

  • 这个函数接收当前的 context 值,返回一个 React 节点;

    <MyContext.Consumer>
      {value => /* 基于 context 值进行渲染*/}
    </MyContext.Consumer>
    
3. Context使用过程
  • class写法:
    import React, { Component } from 'react'
    
    // 1.创建一个共享的Context对象:
    const UserContext = React.createContext({ 
      nickName: '小陈', // 默认值
      level: -1
    })
    
    // 孙组件
    class Navbar extends Component{
      render() {
        console.log(this.context)
        return (
          <div>
            <h2>用户昵称: {this.context.nickName}</h2> // 消费context里面的值
            <h2>用户等级: {this.context.level}</h2>
          </div>
        )
      }
    }
    
    Navbar.contextType = UserContext; // 3.把 <UserContext.Provider>组件 value绑定的值,传给Navbar组件, 然后Navbar可以通过 context拿到传来的数据
    
    // 子组件
    class Main extends Component {
      render() {
        return (
          <div>
            <Navbar ></Navbar>
            <ul>
              <li>设置1</li>
              <li>设置2</li>
              <li>设置3</li>
              <li>设置4</li>
            </ul>
          </div>
        )
      }
    }
    
    // 父组件
    export default class App extends Component {
      constructor(props){
        super(props)
        this.state = {
          nickName : 'chen',
          level: 99
        }
      }
      render() {
        return (
          <div>
            {/* 2.接受一个 value属性, 传递个消费组件 */}
            <UserContext.Provider value={this.state}>
              <Main></Main> 
            </UserContext.Provider>
            <h2>其他内容</h2>
          </div>
        )
      }
    }
    
    
  • function函数组件写法:

    什么时候使用Context.Consumer呢?

    • 1.当使用value的组件是一个函数式组件时;
    • 2.当组件中需要使用多个Context时;
    演练一:
    import React, { Component } from 'react'
    
    // 1.创建一个共享的Context对象:
    const UserContext = React.createContext({ 
      nickName: '小陈', // 默认值
      level: -1
    })
    
    // 孙组件
    function Navbar() { 
      return (            /*3.使用 Consumer消费订阅的值  */
        <UserContext.Consumer> 
          {
            value => { /* 4.基于context进行渲染 */
              return (
                <div>
                  <h2>用户昵称: {value.nickName}</h2>
                  <h2>用户等级: {value.level}</h2>
                </div>
              )
            }
          }
        </UserContext.Consumer>
      )
    }
    
    // 子组件
    class Main extends Component {
      render() {
        return (
          <div>
            <Navbar ></Navbar>
            <ul>
              <li>设置1</li>
              <li>设置2</li>
              <li>设置3</li>
              <li>设置4</li>
            </ul>
          </div>
        )
      }
    }
    
    // 父组件
    export default class App extends Component {
      constructor(props){
        super(props)
        this.state = {
          nickName : 'chen',
          level: 99
        }
      }
      render() {
        return (
          <div>
            {/* 2.接受一个 value属性, 传递个消费组件 */}
            <UserContext.Provider value={this.state}>
              <Main></Main> 
            </UserContext.Provider>
            <h2>其他内容</h2>
          </div>
        )
      }
    }
    
    
    演练二:
    import React, { Component } from 'react'
    
    // 1.创建一个共享的Context对象:
    const UserContext = React.createContext({ 
      nickName: '小陈', // 默认值
      level: -1
    })
    
    // 2.创建第二个共享的Context对象
    const ColorContext = React.createContext({
      color:'pink'
    })
    
    // 孙组件
    function Navbar() { 
      return (            /*4.使用 Consumer消费订阅的值  */
        <UserContext.Consumer> 
          {
            value => { /* 5.多个contenxt进行渲染 */
              return (
                <ColorContext.Consumer>
                  {
                    item =>{
                      return (
                        <div>
                          <h2>用户昵称: {value.nickName}</h2>
                          <h2>用户等级: {value.level}</h2>
                          <h2>颜色: {item.color}</h2>
                        </div>
                      )
                    }
                  }
                </ColorContext.Consumer>
              )
            }
          }
        </UserContext.Consumer>
      )
    }
    
    // 子组件
    class Main extends Component {
      render() {
        return (
          <div>
            <Navbar ></Navbar>
            <ul>
              <li>设置1</li>
              <li>设置2</li>
              <li>设置3</li>
              <li>设置4</li>
            </ul>
          </div>
        )
      }
    }
    
    // 父组件
    export default class App extends Component {
      constructor(props){
        super(props)
        this.state = {
          nickName : 'chen',
          level: 99
        }
      }
      render() {
        return (
          <div>
            {/* 3.把值, 传递个消费组件 */}
              <UserContext.Provider value={this.state}>
                <ColorContext.Provider value={{color: "red"}}>
                  <Main></Main> 
                </ColorContext.Provider>
              </UserContext.Provider>
            <h2>其他内容</h2>
          </div>
        )
      }
    }
    
    

image-20210713231700465

十二. setState详细解析 和 React性能优化

1.setState

image-20210612201645992

解决控制台打印异步问题:
this.setState({
  list: [...this.state.list,info]
},() =>{
 console.log(this.state.list)
})
2.SCU优化
  • 在App中,我们增加了一个计数器的代码;

  • 当点击+1时,会重新调用App的render函数;

  • 而当App的render函数被调用时,所有的子组件的render函数都会被重新调用;

    import React, { Component } from 'react';
    
    function Header() {
      console.log("Header Render 被调用");
      return <h2>Header</h2>
    }
    
    class Main extends Component {
      render() {
        console.log("Main Render 被调用");
        return (
          <div>
            <Banner/>
            <ProductList/>
          </div>
        )
      }
    }
    
    function Banner() {
      console.log("Banner Render 被调用");
      return <div>Banner</div>
    }
    
    function ProductList() {
      console.log("ProductList Render 被调用");
      return (
        <ul>
          <li>商品1</li>
          <li>商品2</li>
          <li>商品3</li>
          <li>商品4</li>
          <li>商品5</li>
        </ul>
      )
    }
    
    function Footer() {
      console.log("Footer Render 被调用");
      return <h2>Footer</h2>
    }
    
    export default class App extends Component {
      constructor(props) {
        super(props);
    
        this.state = {
          counter: 0,
          message:'hello world'
        }
      }
    
    /*  shouldComponentUpdate(nextProps, nextState) {
        if (nextState.counter !== this.state.counter) {
          return true;
        }
      
        return false;
      }
     */
    
      render() {
        console.log("App Render 被调用");
    
        return (
          <div>
            <h2>当前计数: {this.state.counter}</h2>
            <button onClick={e => this.increment()}>+1</button>
            <button onClick={e => this.changeText()}>改变文本</button>
            <Header/>
            <Main/>
            <Footer/>
          </div>
        )
      }
    
      increment() {
        this.setState({
          counter: this.state.counter + 1
        })
      }
      changeText() {
        this.setState({
          message: "你好啊,李银河"
        })
      }
    }
    

    image-20210614180428122

    在以后的开发中,我们只要是修改了App中的数据,所有的组件都需要重新render,进行diff算法,性能必然是很低的:
    • 事实上,很多的组件没有必须要重新render;
    • 它们调用render应该有一个前提,就是依赖的数据(state、props)发生改变时,再调用自己的render方法;
    如何来控制render方法是否被调用呢?
    • 通过shouldComponentUpdate方法即可;
2.1 shouldComponentUpdate

如何来控制render方法是否被调用呢?

  • 通过shouldComponentUpdate方法即可;
shouldComponentUpdate(nextProps, nextState) {
  if (nextState.counter !== this.state.counter) {
    return true;
  }

  return false;
}

这个时候,我们可以通过实现shouldComponentUpdate来决定要不要重新调用render方法:

  • 这个时候,我们改变counter时,会重新渲染;
  • 如果,我们改变的是message,那么默认返回的是false,那么就不会重新渲染;
2.2 PureComponent和memo
  • 类组件使用 pureComponent来实现

如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。

我们来设想一下shouldComponentUpdate中的各种判断的目的是什么?

  • props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false;

事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现呢?

  • 将class基础自PureComponent。

把所有 Component 改成 PureComponent :

import React, { PureComponent } from 'react';

function Header() {
  console.log("Header Render 被调用");
  return <h2>Header</h2>
}

class Main extends PureComponent {
  render() {
    console.log("Main Render 被调用");
    return (
      <div>
        <Banner/>
        <ProductList/>
      </div>
    )
  }
}

image-20210614190159189

PureComponent的原理是什么呢?

  • 对props和state进行浅层比较;

查看PureComponent相关的源码:

react/ReactBaseClasses.js中:

  • 在PureComponent的原型上增加一个isPureReactComponent为true的属性

image-20210614190659937

React-reconcilier/ReactFiberClassComponent.js:

image-20210614191900072

这个方法中,调用 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState),这个shallowEqual就是进行浅层比较:

  • function组件使用memo来实现

我们需要使用一个高阶组件memo:

import React, { PureComponent,memo } from 'react';

const MemoHeader = memo(function Header() {
  console.log("Header Render 被调用");
  return <h2>Header</h2>
})

image-20210614194143768

十三. setState不可变的力量

PureComponent使用了性能优化, 如果数据没发生变化, rander不会重新更新

  handleName(val){
    const newData = [...this.state.list]
    newData[val].age +=1
    this.setState({
      list: newData
    })
  }

十四. 全局事件传递(bus)

兄弟组件传值

yarn add events
/*
 * @Date: 2021-06-23 22:07:26
 */
import React, { Component, PureComponent } from 'react'
import { EventEmitter} from 'events'

// 事件总线: event bus
const eventBus = new EventEmitter()

class  SonOne extends PureComponent {

  componentDidMount(){// 页面加载监听的事件, 以及参数
    // eventBus.addListener('getName',(name,age) => { 
    // })
    eventBus.addListener('getName', this.handleGetNameListener) // 常用写法
  }

  componentDidUpdate(){ // 页面卸除 取消事件监听
    eventBus.removeListener("getName",this.handleGetNameListener)
  }

  handleGetNameListener(name,age){
    console.log(name,age)
  }

  render() {
    return (
      <div>
        sonOne
      </div>
    )
  }
}
class  SonTwo extends PureComponent {
  render() {
    return (
      <div>
        SonTwo
        <button onClick={e =>this.emmitEvent()}>点击了SonTwo</button>
      </div>
    )
  }
  emmitEvent(){
    eventBus.emit("getName","陈龙",23) // 触发一个消息
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <SonOne></SonOne>
        <SonTwo></SonTwo>
      </div>
    )
  }
}

十五. 全局事件传递(bus)

1. 通过ref操作DOM
import React, { createRef, PureComponent } from 'react'

export default class App extends PureComponent {
  constructor(props){
    super()

    this.titleRef = createRef()
    this.titleEl = null
  }
  render() {
    return (
      <div>
        {/* 方法一: ref */}
        <h2 ref="title">String Ref</h2>

        {/* 方式二: React.createRef() */}
        <h2 ref={this.titleRef}>Hello Create Ref</h2>

        {/* 方式三: 传入一个函数 */}
        <h2 ref={element => this.titleEl = element}>改变文本</h2>

        <button onClick={e => this.changeText()}>改变文本</button>
      </div>
    )
  }
  changeText(){
    this.refs.title.innerHTML = "你好啊,猜猜猜"
    this.titleRef.current.innerHTML = "你好啊,猜猜猜"
    this.titleEl.innerHTML = "你好啊,猜猜猜"
  }
}
2. 通过ref触发子组件的方法
export default class App extends PureComponent {
  constructor(props) {
    super(props);

    this.counterRef = createRef()
  }

  render() {
    return (
      <div>
        <Counter ref={this.counterRef}/>
        <button onClick={e => this.increment()}>app +1</button>
      </div>
    )
  }

  increment() { // 父组件调用子组件的方法
    this.counterRef.current.increment();
  }
}

函数式组件是没有实例的,所以无法通过ref获取他们的实例:

  • 但是某些时候,我们可能想要获取函数式组件中的某个DOM元素;
  • 这个时候我们可以通过 React.forwardRef ,后面我们也会学习 hooks 中如何使用ref;
3.受控组件

在 HTML 中,表单元素(如<input><textarea><select>)之类的表单元素通常自己维护 state,并根据用户输入进行更新。

而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。

  • 我们将两者结合起来,使React的state成为“唯一数据源”;
  • 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作;
  • 被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”;

例如,如果我们想让前一个示例在提交时打印出名称,我们可以将表单写为受控组件:

image-20210627235635592

十五.高阶组件

image-20210629153033677

1.1 高阶组件的使用
import React, { PureComponent } from 'react'

class App extends PureComponent { // 函数
  render() {
    return (
      <div>
        app {this.props.name}
      </div>
    )
  }
}

/* 高阶组件函数方式 */
function enhanceComponent(WrapperComponent){ // 1.高阶组件
  function NewComponent(props){
      return <WrapperComponent {...props}/>
  }
  return NewComponent
}

const EnhanceComponent = enhanceComponent(App) // 3. 调用高阶组件

export default EnhanceComponent
1.2 . 利用高阶组件, 实现跨组件通信
const UserContext = React.createContext({
  nickName: '小陈', // 这里代表默认数据
  level: -1
})

/* 封装的一个高阶函数, 专门实现夸组件通信 */
function withUser(WrapperCpn){
  return function(props){
    return (
      <UserContext.Consumer>
        {
          value => {
            return <WrapperCpn {...props} {...value} />
          }
        }
      </UserContext.Consumer>
    )
  }
}

/* class类的写法 */
class Home extends PureComponent {
  render(){
    return <h2> Home组件 {"昵称" + this.props.nickName + '年龄' + this.props.age + '性别' + this.props.gander}</h2>
  }
}

/* 函数写法 */
function Footer(props){
  const { nickName, age, gander} = props
  return <h2> Footer函数组件 {"昵称" + nickName+ '年龄' + age + '性别' + gander}</h2>
}

const UserHome = withUser(Home)
const UserFooter = withUser(Footer)
1.3 渲染判断鉴权(类似访问权限)
/* 高阶函数鉴权 */
function withAuto(WrapperComponent){
const NewCpn =  function(props){
    if(props.isLogin){ // 如果权限为真,就显示传来的组件页面
      return <WrapperComponent {...props}/> // 接受到的组件
    }else{
      return <Login/>
    }
  }
  NewCpn.displayName = "AutoCpn" // 组件重命名
  return NewCpn 
}

/* class写法 */
class Login extends PureComponent { // 登录
  render(){
    return <h2>请先登录!!!</h2>
  }
}

const AutoCartPage = withAuto(Cart) // 任何需要鉴权的页面都可以通过  调用 withAuto来鉴权

<AutoCartPage isLogin={true}/>{/* 购物车页面 */}

image-20210629215954300

1.4 声明周期劫持(获取组件渲染时间)
function withTime(WrapperComponent){
  return class extends PureComponent{
    UNSAFE_componentWillMount(){ // 即将渲染获取一个时间 begin
      this.begin = Date.now()
    }
    componentDidMount(){ // 渲染完成再获取一个时间 end
      this.end = Date.now()
      const interval = this.end - this.begin
      console.log(`${WrapperComponent.name}渲染的时间为:${interval}`)// 获取传来的组件名字
    }
    render() {
      return (
        <div>
          <WrapperComponent {...this.props}/>
        </div>
      )
    }
  }
}

高阶函数(HOC)的意义

image-20210629224914373

十六.组件内容补充

1.1 ref的转发 forwardRef

image-20210629230703498

/* function组件可以使用 forwardRef高阶组件 */
const Son  = forwardRef(function(props,ref){
  return (
    <h2 ref={ref}>我是function函数组件使用ref</h2>
  )
})
1.2 Portals的使用

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

image-20210629233550337

例子: 全局弹窗modal

1.添加一个节点

image-20210629234624827

2.使用ReactDom渲染节点

import React, { PureComponent } from 'react'
import ReactDom from 'react-dom'

class Modle extends PureComponent {
  constructor(props){
    super(props)
  }
  render() {
    return ReactDom.createPortal(
      this.props.children, // 参数1: props.children 可以拿到组件里放的所有子节点
      document.getElementById('modal') // 参数2: 是要渲染到哪一个DOM上
    )
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <Modle>
         <h2>我是全局弹窗</h2>
        </Modle>
      </div>
    )
  }
}

1.3 Fragment (类似于vue 的template )

Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点

      <div>
        <p>我是节点,有div包裹</p>
        <> {/* <> 是 Fragment的简写 短语发  */}
          <h2>下面的使用了 Fragment没有div包裹</h2>
          <div>
            {
              list.map((item,index) => {
                return (
                  <Fragment key={item.name}>
                    <div>{item.name}</div>
                    <p>{item.age}</p>
                    <hr/>
                  </Fragment>
                )
              })
            }
          </div>
        </>
      </div>
1.4 StrictMode

StrictMode 是一个用来突出显示应用程序中潜在问题的工具。

  • Fragment 一样,StrictMode 不会渲染任何可见的 UI;
  • 它为其后代元素触发额外的检查和警告;
  • 严格模式检查仅在开发模式下运行;它们不会影响生产构建

可以为应用程序的任何部分启用严格模式:

  • 会对 HeaderFooter 组件运行严格模式检查;
  • 但是,ComponentOneComponentTwo 以及它们的所有后代元素都将进行检查;
import React from 'react';

function ExampleApplication() {
  return (
    <div>
      <Header />
      <React.StrictMode>
        <div>
          <ComponentOne />
          <ComponentTwo />
        </div>
      </React.StrictMode>
      <Footer />
    </div>
  );
}

image-20210630210430356

十七.React中的样式选择

1.1 相比而言,React官方并没有给出在React中统一的样式风格:
  • 由此,从普通的css,到css modules,再到css in js,有几十种不同的解决方案,上百个不同的库;

  • 最好的或者说最适合自己的CSS方案,

    • 方案一:内联样式的写法;
      • 内联样式的优点:
        • 1.内联样式, 样式之间不会有冲突
        • 2.可以动态获取当前state中的状态
      • 内联样式的缺点:
        • 1.写法上都需要使用驼峰标识
        • 2.某些样式没有提示
        • 3.大量的样式, 代码混乱
        • 4.某些样式无法编写(比如伪类/伪元素)
    • 方案二:普通的css写法;
      • 如果我们按照普通的网页标准去编写,那么也不会有太大的问题;
      • 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响;
      • 但是普通的css都属于全局的css,样式之间会相互影响;
    • 方案三:css modules;
      • 但是这种方案也有自己的缺陷:
        • 引用的类名,不能使用连接符(.home-title),在JavaScript中是不识别的;
        • 所有的className都必须使用{style.className} 的形式来编写;
        • 不方便动态来修改某些样式,依然需要使用内联样式的方式;
    • 方案四:css in js(styled-components);
      • 原理就是创建一个样式组件 ,该组件渲染之后是一个自己定义的标签

      • Wrapper组件跟其余的react组件一样,只不过现在他们有了自己的样式

      • CSS-in-JS通过JavaScript来为CSS赋予一些能力,包括类似于CSS预处理器一样的样式嵌套、函数定义、逻辑复用、动态修改状态等等;

      • 依然CSS预处理器也具备某些能力,但是获取动态状态依然是一个不好处理的点;

      • 所以,目前可以说CSS-in-JS是React编写CSS最为受欢迎的一种解决方案;

这里选择方案四做演示:

1.2 css in js使用方法

1.2.1 标签模板字符串(介绍)

可以通过护板字符串的方式对一个函数进行调用

    const name = "chen"
    const age = "23"
    function test(...args){
      console.log(args)
    }

    test`aaa` //  ["aaa", raw: Array(1)]

    test`my name is ${name} , age is ${age}`

image-20210630232119281

1.2.2 安装styled-components:
yarn add styled-components
1.2.3 styled-components

1.vscod css提示插件: vscode-styled-components

image-20210630233856081

1.2.4 styled-components语法
  1. 基本语法
/*
 * @Date: 2021-06-30 23:01:53
 */
import React, { PureComponent } from 'react'
import styled from 'styled-components'

const HomeWrapper =  styled.div`
  font-size:50px;
  color: red;
  .banner {
    background-color: pink;
    span {
      background-color: green;
      margin-right: 20px;
      cursor: pointer;
      &.avtive{
        background-color: red;
        color: #fff;
      }
      &:hover{
        color: #fff;
      }
    }
  }

`

const TitleWrapper = styled.h4` /* 1.设置一个h2的标签样式(类名如果重复可以用此方法解决) */
  text-decoration: underline;
`

export default class Home extends PureComponent {
  render() {
    return (
      <HomeWrapper>
        <TitleWrapper>我是home标题</TitleWrapper>{/* 2.使用专属标签 */}
        <div className="banner">
          <div>我是轮播图</div>
          <span className="avtive">1轮播图</span>
          <span>2轮播图</span>
          <span>3轮播图</span>
          <span>4轮播图</span>
        </div>

      </HomeWrapper>
    )
  }
}

  1. 一些标签自带的属性用法

    第一种写法:

    直接在标签上写属性

    import styled from 'styled-components'
    
    const InputWrapp = styled.input`
      background-color: lightblue;
      border-color: red;
    `
    export default class About extends PureComponent {
      render() {
        return (
          <div>
            <h2>我是about标题</h2>
            <InputWrapp placeholder="请输入文字"></InputWrapp> {/* 直接在标签上写属性  */}
          </div>
        )
      }
    }
    

    第二种写法进阶:

    好处:

    • props穿透
    • attrs的使用
    • 传入state作为props属性

    可以使用 this.state对象的属性

    import styled from 'styled-components'
    
    const InputWrapp = styled.input.attrs({ // attributes:的缩写, 属性的意思
      placeholder:'请输入密码',
      bColor:'blue' // 定义一个属性值
    })`
      background-color: lightblue;
      border-color: ${props =>props.bColor}; // 1.可以拿到 attrs里面定义的 属性值, 
      color:${props => props.color};// 2. 可以动态拿到 this.state对象里面的属性
    `
    export default class About extends PureComponent {
      constructor(props){
        super(props)
        this.state = {
          color: 'red'
        }
      }
      render() {
        return (
          <div>
            <h2>我是about标题</h2>
            <InputWrapp placeholder="请输入文字"></InputWrapp>
            <br/>
            <InputWrapp color={this.state.color}></InputWrapp>
          </div>
        )
      }
    }
    
    1. 继承样式

      image-20210701002946445

      const CLbutton = styled.button`
        padding:  10px 20px;
        border-radius: 10px;
        border-color: red;
        color: red;
      `
      
      const CLbutton2 = styled(CLbutton)` // 继承CLbutton的样式
        border-color: yellow;
      `
      
      export default class App extends PureComponent {
        render() {
          return (
            <div>
              <h2>App</h2>
              <Home/>
              <About/>
              <p>样式的继承</p>
              <CLbutton>我是按钮</CLbutton>
              <CLbutton2>我是按钮2</CLbutton2>
            </div>
          )
        }
      }
      
      

    4.主题设置(全局设置样式)

    import styled, { ThemeProvider} from 'styled-components' // 1.引用 ThemeProvider主题提供器
    
    const CLbutton = styled.button`
      padding:  10px 20px;
      border-radius: 10px;
      border-color: red;
      color: red;
    `
    
    const CLbutton2 = styled(CLbutton)` // 继承CLbutton的样式
      border-color: yellow;
      color:${props => props.theme.themeColor}; // 3. 使用全局属性
    `
    
    export default class App extends PureComponent {
      render() {
        return (
          <ThemeProvider theme={{themeColor:'#01bd83',fontSize:'30px'}}> {/*2. 定义全局属性 */}
            <h2>App</h2>
            <Home/>
            <About/>
            <p>样式的继承</p>
            <CLbutton>我是按钮</CLbutton>
            <CLbutton2>我是按钮2</CLbutton2>
          </ThemeProvider>
        )
      }
    }
    

十八.Classnames组件通过透出一个对象来匹配配置类名

React动态添加样式

npm install classnames --save
yarn add classnames // 或者
import classname from 'classnames'

<h4 className={classname("AA","BB","CC","DD")}>动态添加类名</h4>
<h4 className={classname({"active":isActive, "bar": isBar}, "wrap")}>动态添加类名</h4>
<h4 className={classname("AA",errClass,{"active":isActive})}>动态添加类名</h4>

十九. AntDesign UI

1.2. AntDesign的安装
npm install antd --save
import {Button ,DatePicker } from 'antd' // 组件
import 'antd/dist/antd.css' // 样式
1.3 高级配置
1.3.1修改create-react-app 的默认配置.

那么如何来进行修改默认配置呢?社区目前有两个比较常见的方案:

  • react-app-rewired + customize-cra;(这个是antd早期推荐的方案)
  • craco;(目前antd推荐的方案)
1.3.2 第一步: 安装creco
yarn add @craco/craco
1.3.3 第二部: 修改package.json文件
  • 原本启动时,我们是通过react-scripts来管理的;
  • 现在启动时,我们通过craco来管理;
"scripts": {
-   "start": "react-scripts start",
-   "build": "react-scripts build",
-   "test": "react-scripts test",
+   "start": "craco start",
+   "build": "craco build",
+   "test": "craco test",
}
1.3.4 第三部: 在根目录下创建craco.config.js文件用于修改默认配置 (类似于vue.config.js)
module.exports = {
  // 配置文件
}
1.3.5 配置主题

按照 配置主题 的要求,自定义主题需要用到类似 less-loader 提供的 less 变量覆盖功能:

  • 我们可以引入 craco-less 来帮助加载 less 样式和修改变量;

1.安装 craco-less

yarn add craco-less

2.修改craco.config.js中的plugins:

  • 使用modifyVars可以在运行时修改LESS变量;
const CracoLessPlugin = require('craco-less');

module.exports = {
  plugins: [
    {
      plugin: CracoLessPlugin,
      options: {
        lessLoaderOptions: {
          lessOptions: {
            modifyVars: { '@primary-color': '#1DA57A' },
            javascriptEnabled: true,
          },
        },
      },
    },
  ],
}

3.引入antd的样式时,引入antd.less文件:

// import 'antd/dist/antd.css'
import 'antd/dist/antd.less';

image-20210702220257434

1.3.6 配置别名 @

在项目开发中,某些组件或者文件的层级会较深,

  • 如果我们通过上层目录去引入就会出现这样的情况:../../../../components/button
  • 如果我们可以配置别名,就可以直接从根目录下面开始查找文件:@/components/button,甚至是:components/button

配置别名也需要修改webpack的配置,当然我们也可以借助于 craco 来完成:

const path = require('path'); // path模块是node.js中处理路径的核心模块
const resolve = dir => path.resolve(__dirname,dir) // __dirname当前craco.congig.js的路径, dir指传传过来的路径, 进行拼接

module.exports = {
...
,
  webpack: {
    alias: { // 别名
      '@': resolve("src"), // @代表: __dirname(当前文件的路径) + src  所以=>   @ 等价于 /scr
      'components':resolve("src/components")// components代表: 使用components 等价于 components/src
    }
  }
}

在导入时就可以按照下面的方式来使用了:

import HYTitle from '../../title' // 原来的做法
import HYTitle from '@/components/title' // 使用 @

二十. React网络请求选择

1.Fetch Api

image-20210703102333675

2.Axios的基本使用

2.1 安装 axios

npm i axios

2.2 二次封装axios

请求拦截加token , 响应拦截 判断后端返回的 响应码

二十一. react-transition-group

react-transition-group用来给一个组件的显示和消失添加某种过渡动画

1.0. 介绍

react-transition-group主要包含四个组件:

  • Transition

    • 该组件是一个和平台无关的组件(不一定要结合CSS);
    • 在前端开发中,我们一般是结合CSS来完成样式,所以比较常用的是CSSTransition;
  • CSSTransition

    • 在前端开发中,通常使用CSSTransition来完成过渡动画效果
  • SwitchTransition

    • 两个组件显示和隐藏切换时,使用该组件
  • TransitionGroup

    • 将多个动画组件包裹在其中,一般用于列表中元素的动画;

2.0 CSSTransition使用

2.1 CSSTransition是基于Transition组件构建的:
  • 自己通过 className="自定义一个类名" "chen-enter"// 就是显示时的class名字

  • CSSTransition执行过程中, 有三个状态: appear, enter, exit

    动画流程 页面首次加载触发的动画 显示时触发的动画 隐藏时触发的动画
    动画开始 -appear -enter exit
    动画执行 -appear-active -enter-active -exit-active
    动画结束 -appear-done -enter-done -exit-done
  • 然后自己根据这些类名写 需要的CSS动画样式

2.2 CSSTransition常见对应的属性:
  • in:触发进入或者退出状态(显示和隐藏的时候才要in)

    • 如果添加了unmountOnExit={true},那么该组件会在执行退出动画结束后被移除掉;
    • 当in为true时,触发进入状态,会添加-enter、-enter-acitve的class开始执行动画,当动画执行结束后,会移除两个class,并且添加-enter-done的class;
    • 当in为false时,触发退出状态,会添加-exit、-exit-active的class开始执行动画,当动画执行结束后,会移除两个class,并且添加-enter-done的class;
  • classNames:动画class的名称

    • 决定了在编写css时,对应的class名称:比如card-enter、card-enter-active、card-enter-done;
  • timeout:

    • 过渡动画的时间
  • unmountOnExit:

    • unmountOnExit = {true}

    • 是否开始 隐藏DOM 整个节点都隐藏

  • appear:

    • 是否在初次进入添加动画(需要和in同时为true)需要配置 -appear, 所以最好的方法是 -appear 和 -enter 都设置成一个动画 .chen-appear , .chen-enter{}
  • 其他属性可以参考官网来学习:

2.3. CSSTransition对应的钩子函数:

主要为了检测动画的执行过程,来完成一些JavaScript的操作

  • onEnter:在进入动画之前被触发;

  • onEntering:在应用进入动画时被触发;

  • onEntered:在应用进入动画结束后被触发;

  • onExit:退出动画前

  • onExiting:退出动画;

  • onExited:退出动画后

    image-20210704140215381

3. SwitchTransition

3.1 SwitchTransition可以完成两个组件之间切换的炫酷动画:
  • 比如我们有一个按钮需要在on和off之间切换,我们希望看到on先从左侧退出,off再从右侧进入;
  • 这个动画在vue中被称之为 vue transition modes;
  • react-transition-group中使用SwitchTransition来实现该动画;
3.2 SwitchTransition中主要有一个属性:mode,有两个值
  • in-out:表示新组件先进入,旧组件再移除;
  • out-in:表示就组件先移除,新组建再进入;
3.3如何使用SwitchTransition呢?
  • SwitchTransition组件里面要有CSSTransition或者Transition组件,不能直接包裹你想要切换的组件;
  • SwitchTransition里面的CSSTransition或Transition组件不再像以前那样接受in属性来判断元素是何种状态,取而代之的是key属性;

我们来演练一个按钮的入场和出场效果:

image-20210704142927286

4. TransitionGroup

  • 当一个React组件添加或者移除一个DOM的时候可以轻易的实现CSS动画
  • 你必须为CSSTransitionGroup的子级元素添加一个key属性,即使是渲染一个唯一的子元素的时候。React通过key来判断那个子级元素进入视野或者离开视野(显示或者隐藏)

image-20210704150142463

image-20210704150318893

二十二. React中的纯函数

  • 确定的输入,一定会有产生确定的输出 (输出得到的结果,是确定中的值)
  • 函数在执行过程中,不能产生副作用

1.0 纯函数需要满足一下几点

  • 相同输入总是会返回相同的输出。
  • 不产生副作用。
  • 不依赖于外部状态。
例子1: 纯函数
function sum(num1, num2){
      return num1 + num2
    }
    sum(20,30)
    sum(20,40)
例子2: 不是纯函数
/* 不是纯函数,在执行中产生副作用 */
    function print(info){
      info.name = "xxx" // 修改了值
      console.log(info.name)
    }

image-20210704175149803

二十三. Redux

1.0 整体做法

/* 整体写法 */

// const redux =  require('redux')
import redux from 'redux' // node.js V13.2才支持

// 1.默认数据
const initialState = { 
  counter: 0
}

// 2.创建一个 reducer纯函数
function reducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return {...state,counter: state.counter + 1}
      case 'DECREMENT':
        return {...state,counter: state.counter - 1}
    default:
      state;
  }
}

// 3. store(创建的时候需要传入一个reducer)
const store = redux.createStore(reducer)

// 5.订阅store的修改
store.subscribe(() => {
  console.log('counter', store.getState().counter)
})


// 4. 创建actions
const action1 = {type: "INCREMENT"};
const action2 = {type: "DECREMENT"};

// 5.派发actions
store.dispatch(action1)
store.dispatch(action2)

1.2 封装写法(开发模式)

actionCreators.js 派发的函数
/* 3. 创建actions派发的函数
 * 导入全局常量派发名字 constants.js
 * @Date: 2021-07-04 23:10:05
 */
import {ADD_NUMBER,SUB_NUMBER} from "./constants.js"

export function addAction(num) {
  return {
    type: ADD_NUMBER, // 4.这里派发的名字是由constants.js管理
    num
  }
}

export function subAction(num) {
  return {
    type: SUB_NUMBER, 
    num
  }
}
constants.js 放一些常量
/* 4.在这里用常量保存 axion派发的名字(统一方便管理)
 * @Date: 2021-07-04 23:16:33
 */
export const ADD_NUMBER = "ADD_NUMBER"
export const SUB_NUMBER ="SUN_NUMBER"
index.js主要文件移入 redux
/* 1.做中间键(主要文件)
   2.引入redux包, 
   3.导入默认值state 和 纯函数(处理逻辑) reducer.js
 * @Date: 2021-07-04 22:48:29
 */
import redux from 'redux'

import reducer from './reducer.js'

/* store(创建的时候需要传入一个reducer)纯函数 */
const store = redux.createStore(reducer)

export default store
reducer.js 用来处理state的文件
/* 2.创建一个默认值 和 纯函数(加需要处理的逻辑)
 * @Date: 2021-07-04 22:51:27
 */

import { ADD_NUMBER, SUB_NUMBER} from './constants.js'

// 创建一个默认值
const defaultState = {
  counter: 0
}

// 创建一个 reducer纯函数
function reducer(state = defaultState, action) {
  switch(action.type) {
    case ADD_NUMBER:
      return {...state, counter:state.counter + action.num}
    case SUB_NUMBER:
      return {...state, counter:state.counter - action.num}
    default:
    return state
  }
}

export default reducer
index2.js 业务文件(需要使用redux的文件)
/* 主要使用strore文件  
   1.导入store  index.js
   2.导入派发的函数 actionCreators.js
   3.订阅store的值
 * @Date: 2021-07-04 23:02:03
 */
import store from './store/index.js'

import {addAction, subAction} from './store/actionCreators.js'

// 订阅store的修改
store.subscribe(() => {
  console.log('counter', store.getState().counter)
})

// 派发函数
store.dispatch(addAction(10))
store.dispatch(subAction(8))

正常操作: constants .js ===>actionCreators .js ===> reducer.js ===> 业务文件(需要调用redux)派发action

1.3 Redux使用流程

image-20210705202548553

1.4 脚手架里使用Redux

脚手架需要在 componentDidMount里 订阅 store.subscribe
/*
 * @Date: 2021-07-05 21:01:27
 */
import React, { PureComponent } from 'react'

import store from '../store'

import { addAction,subAction } from '../store/actionCreators'

export default class Home extends PureComponent {
  constructor(prost) {
    super(prost)
    this.state = {
      counter: store.getState().counter
    }
  }

  componentDidMount() { // 订阅(只有订阅了 视图才会更新)
    this.unsubscribue = store.subscribe(() => {
      this.setState({
        counter: store.getState().counter
      })
    })
  }

  componentWillUnmount() { // 取消订阅
    this.unsubscribue()
  }

  render() {
    const { counter } = this.state
    return (
      <div>
        <span>home</span>
        <h2>当前计数: {counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
        <button onClick={e => this.addNumber(5)}>+5</button>
      </div>
    )
  }
  increment(){
    store.dispatch(addAction())
  }

  addNumber(num){
    store.dispatch(subAction(num))
  }
}

1.5 react-redux 库使用

本库并不是 Redux 内置,需要单独安装。因为一般会和 Redux 一起使用

1.0 下载
npm install --save react-redux
1.1 使用
import store from './store'
import { connect, Provider } from 'react-redux'
 ReactDOM.render(
   <Provider store={store}> // 固定属性store
     <App4 />
   </Provider>
 ,document.getElementById('root'));
1.3源码解析

image-20210706140726913

ReactReduxContext来自另外一个文件:

image-20210706140917458

connect函数最终调用的是connectHOC:

image-20210706143354027

1.6 redux 异步请求

在组件中异步请求

image-20210706162932483

1.7 redux异步请求 + 中间件redux-thunk(重要)

1.0 在redux中发送异步请求

image-20210706163222661

1.1 使用步骤
  • 1.先下载 redux-thunk中间件

    yarn add redux-thunk
    
  • 2.在store里引用中间件

image-20210706172606546

  • 3.在组件里 调用一个派遣函数

image-20210706190754605

  • 4 派遣一个action

image-20210706190929029

  • 中转派遣

image-20210706191316602

两个参数 第一个派遣, 第二个可以拿到 store里面的数据

image-20210801233319549

1.8 redux-saga

1.saga介绍

image-20210708002441271

image-20210708001533706

  • takeEvery:可以传入多个监听的actionType,每一个都可以被执行(对应有一个takeLastest,会取消前面的)
  • put:在saga中派发action不再是通过dispatch,而是通过put;
  • all:可以在yield的时候put多个action;
2. 安装
yarn add redux-saga
3. 使用
import { takeEvery, put, all } from 'redux-saga/effects';
import axios from 'axios'
import { FETCH_HOME_MULTIDATA } from './constants';
import { bologAction } from './actionCreators'

  function* fetchHomeMultdata(action) { //
    const res = yield   axios({
      url: 'https://c1998.cn/apis/get/',
      params: {
        page: 1,
        per_page: 10
      }
    })
    const data = res.data.data
    yield put(bologAction(data)) // put 代替了 dispatch
  }

  function* mySaga() {
    // yield takeEvery(FETCH_HOME_MULTIDATA, fetchHomeMultdata)

    yield all([
      yield takeEvery(FETCH_HOME_MULTIDATA, fetchHomeMultdata)
    ]);
  }

export default mySaga

1.9 reducer代码拆分

1.0 我们来看一下目前我们的reducer:
  • 当前这个reducer既有处理counter的代码,又有处理home页面的数据;
  • 后续counter相关的状态或home相关的状态会进一步变得更加复杂;
  • 我们也会继续添加其他的相关状态,比如购物车、分类、歌单等等;

如果将所有的状态都放到一个reducer中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。

我们先抽取一个对counter处理的reducer:

// 1.我们先抽取一个对counter处理的reducer:
const initialCounterState = { // 默认值
  counter: 0
}
function counterReducer(state = initialCounterState, action) {
  switch(action.type) {
    case ADD_NUMBER:
      return {...state, counter:state.counter + 1}
    case SUB_NUMBER:
      return {...state, counter:state.counter + action.num}
    case Mult_NUMBER:
      console.log('xxx')
      return {...state, counter:state.counter * action.num}
    default:
      return state;
  }
}

再抽取一个对home处理的reducer:

// 再抽取一个对home处理的reducer:
const initialHomeState = {
  bologList:[]
}
function homeReducer(state = initialHomeState, action) {
  switch(action.type) {
    case BOLOG_LISTS:
      console.log('执行了kk',action)
      return {...state, bologList: action.bologList}
    default:
    return state
  }
}

合并起来

// 如果将它们合并起来呢?
function reducer(state = {}, action) {
  return {
    counterInfo: counterReducer(state.counterInfo, action),
    homeInfo: homeReducer(state.homeInfo, action)
  }
}

使用

image-20210708221413296

image-20210708221514245

1.1 合并reducers

combineReducers: 合并 redux

image-20210708232856108

image-20210708232727661

2.0 ImmutableJS

2.1 React中的state如何管理

image-20210708233007455

React的作者自己的建议

image-20210708233630880

具体代码看 06-text-react pages8(终极版本)

FAQ:

单向数据流:

image-20210708234654454

二十四. Router路由原理

1.1前端路由原理

URL的hash
<a href="#/home">home</a>
<a href="#/about">about</a>
<div class="router-view"></div>

// 1.获取router-view
const routerViewEl = document.querySelector(".router-view");

// 2.监听hashchange,来显示对应页面的内容
window.addEventListener("hashchange", () => {
switch(location.hash) {
  case "#/home": 	
    routerViewEl.innerHTML = "home";
    break;
  case "#/about":
    routerViewEl.innerHTML = "about";
    break;
  default:
    routerViewEl.innerHTML = "default";
}
})
  • URL的hash也就是锚点(#), 本质上是改变window.location的href属性;
  • 我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新;
  • hash的优势就是兼容性更好,在老版IE中都可以运行,但是缺陷是有一个#,显得不像一个真实的路径。

1.2 HTML5的History

  const routerViewEl = document.querySelector(".router-view");

  // 2.监听所有的a元素
  const aEls = document.getElementsByTagName("a");
  for (let aEl of aEls) {
    aEl.addEventListener("click", (e) => {
      e.preventDefault();
      const href = aEl.getAttribute("href");
      console.log(href);
      history.pushState({}, "", href);
      historyChange();
    })
  }
  
   // 3.监听popstate和go操作
  window.addEventListener("popstate", historyChange);
  window.addEventListener("go", historyChange);
  
   // 4.执行设置页面操作, 来显示对应的页面内容
  function historyChange() {
    switch(location.pathname) {
      case "/home":
        routerViewEl.innerHTML = "home";
        break;
      case "/about":
        routerViewEl.innerHTML = "about";
        break;
      default:
        routerViewEl.innerHTML = "default";
    }
  }

history接口是HTML5新增的, 它有l六种模式改变URL而不刷新页面:

  • replaceState:替换原来的路径;
  • pushState:使用新的路径;
  • popState:路径的回退;
  • go:向前或向后改变路径;
  • forword:向前改变路径;
  • back:向后改变路径;

二十五. react-router

yran add react-router-dom

1.react-router最主要的API是给我们提供的一些组件:

  • BrowserRouter或HashRouter

    • Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件;
    • BrowserRouter使用history模式;
    • HashRouter使用hash模式;
  • Link和NavLink:

    • 通常路径的跳转是使用Link组件,最终会被渲染成a元素;
    • NavLink是在Link基础之上增加了一些样式属性(后续学习);
    • to属性:Link中最重要的属性,用于设置跳转到的路径;
  • Route:

    • Route用于路径的匹配;
    • path属性:用于设置匹配到的路径;
    • component属性:设置匹配到路径后,渲染的组件;
    • exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;

在App中进行如下演练:

import React, { PureComponent } from 'react'
import {BrowserRouter, Route, Link} from 'react-router-dom'
import Home from './Home'
import About from './About'
import Profile from './Profile'

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <BrowserRouter>
        <Link to="/">首页</Link>
        <Link to="about">关于</Link>
        <Link to="/profile">我的</Link>

        <Route exact path="/" component={Home}></Route>
        <Route path="/about" component={About}/>
        <Route path="/profile" component={Profile}/>
        </BrowserRouter>
      </div>
    )
  }
}

2. NavLink的使用

路径选中时,对应的a元素变为红色

这个时候,我们要使用NavLink组件来替代Link组件:

  • activeStyle:活跃时(匹配时)的样式;
  • activeClassName:活跃时添加的class;
  • exact:是否精准匹配;

先演示activeStyle:

{/* 自定义router选中高亮类名 */}
<NavLink exact to="/" activeClassName="link-active">首页</NavLink>
<NavLink to="/about" activeClassName="link-active">关于</NavLink>
<NavLink to="/profile" activeClassName="link-active">我的</NavLink>

3. Switch的作用

1.我们来看下面的路由规则:
  • 当我们匹配到某一个路径时,我们会发现有一些问题;
  • 比如/about路径匹配到的同时,/:userid也被匹配到了,并且最后的一个NoMatch组件总是被匹配到;

原因是什么呢?默认情况下,react-router中只要是路径被匹配到的Route对应的组件都会被渲染;

2.但是实际开发中,我们往往希望有一种排他的思想:
  • 只要匹配到了第一个,那么后面的就不应该继续匹配了;
  • 这个时候我们可以使用Switch来将所有的Route进行包裹即可;

image-20210709224111419

4. Redirect(重定向)

Redirect用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中:

我们这里使用这个的一个案例:

  • 用户跳转到User界面;

  • 但是在User界面有一个isLogin用于记录用户是否登录:

    • true:那么显示用户的名称;
    • false:直接重定向到登录界面;

App.js中提前定义好Login页面对应的Route:

export default class uesr extends PureComponent {
  constructor(props){
    super(props)
    this.state = {
      isLogin: true
    }
  }
  render() {
    return this.state.isLogin ? (
      <div>
        <h2>user</h2>
        <h2>用户名:__</h2>
      </div>
    ):<Redirect to="/login"/> // 重定向
  }
}

5.路由嵌套

这里我们假设about页面中有两个页面内容:

  • 商品列表和消息列表;
  • 点击不同的链接可以跳转到不同的地方,显示不同的内容;
export default class About extends PureComponent {
  render() {
    return (
      <div>
        <NavLink exact to="/about" activeClassName="link-active">电器</NavLink>
        <NavLink exact to="/about/culture" activeClassName="link-active">衣服</NavLink>
        <NavLink exact to="/about/contact" activeClassName="link-active">食品</NavLink>

        <Switch>
          <Route exact path="/about" component={AboutProduct}></Route>
          <Route path="/about/culture" component={AboutMessagea}></Route>
          <Route path="/about/contact" component={AboutMessageb}></Route>
        </Switch>
      </div>
    )
  }
}

image-20210709232233814

6.手动跳转路由

目前我们实现的跳转主要是通过Link或者NavLink进行跳转的,实际上我们也可以通过JavaScript代码进行跳转。

但是通过JavaScript代码进行跳转有一个前提:必须获取到history对象。

如何可以获取到history的对象呢?两种方式

  • 方式一:如果该组件是通过路由直接跳转过来的,那么可以直接获取history、location、match对象
  • 方式二:如果该组件是一个普通渲染的组件,那么不可以直接获取history、location、match对象;

那么如果普通的组件也希望获取对应的对象属性应该怎么做呢?

  • 前面我们学习过高阶组件,可以在组件中添加想要的属性;
  • react-router也是通过高阶组件为我们的组件添加相关的属性的;

如果我们希望在App组件中获取到history对象,必须满足以下两个条件:

  • App组件必须包裹在Router组件之内;
  • App组件使用withRouter高阶组件包裹;

index.js代码修改如下:

该组件是通过路由直接跳转过来的: 可以直接跳转

<button onClick={e => this.pushToProfile()}>前往我的</button>
  pushToProfile(){
    this.props.history.push("/user")
  }

该组件是一个普通渲染的组件: 不能直接跳转会报错

image-20210709234503939

解决:

使用 withRouter()高阶函数 导出的组件可以直接拿到 history

import { Route, Switch, NavLink, withRouter } from 'react-router-dom';
...省略其他的导入代码

class App extends PureComponent {
  render() {
    console.log(this.props.history);

    return (
      <div>
        ...其他代码
        <button onClick={e => this.pushToProfile()}>我的</button>

        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
          <Route path="/profile" component={Profile} />
          <Route path="/:userid" component={User} />
          <Route component={NoMatch} />
        </Switch>
      </div>
    )
  }

  pushToProfile() {
    this.props.history.push("/profile");
  }
}

export default withRouter(App); // 使用 withRouter()导出路由

image-20210709235010393

7.动态路由(传递参数)

传递参数有三种方式:

  • 动态路由的方式;
  • search传递参数;
  • to传入对象;
1. 第一种: 动态路由传递 (拼接传递)
  • 我们可以直接通过match对象中获取id;
  • 这里我们没有使用withRouter,原因是因为Detail本身就是通过路由进行的跳转;
// 1.传递 app.js
<NavLink to={`/detail/${id}`} activeClassName="link-active">动态路由</NavLink>

<Route path="/detail/:id" component={Detail}/>
// 2. 接受参数detail.js
export default class Detail extends PureComponent {
  render() {
    const match = this.props.match; // 接受参数
    console.log(match)
    return (
      <div>
        <h2>Detail: {this.props.match.params.id}</h2>
      </div>
    )
  }
}
2. search传递参数 (query传递 ) /?name=whty&, 已经不推荐了
  • 我们在跳转的路径中添加了一些query参数;
1.传递 app.js
<NavLink to="/detail2?name=why&age=18" activeClassName="link-active">search(query)传递</NavLink>

<Route path="/detail2" component={Detail2}/>

export default class Detail extends PureComponent {
  render() {
    const match = this.props.location.search; // 接受参数
    console.log(this.props.location.search)
    return (
      <div>
        <h2>detail2: {match}</h2>
      </div>
    )
  }
}

image-20210710132713134

3.to传递参数

to可以直接传入一个对象

 const info = {name:'chen',age:'23', gander: '男'}
 // app.js
<NavLink to={{
  pathname:'/detail3',
  search:"?name=abc",
  state:info
}} activeClassName="link-active">to(params)传递</NavLink>
export default class Detail extends PureComponent {

  render() {
    const match = this.props.location.state; // 接受参数
    console.log(this.props.location.state)
    return (
      <div>
        <h2>detail3: {match.name}</h2>
      </div>
    )
  }
}

image-20210710132848171

8.react-router-config 路由统一配置 (类似vue路由配置文件)***

目前我们所有的路由定义都是直接使用Route组件,并且添加属性来完成的。

但是这样的方式会让路由变得非常混乱,我们希望将所有的路由配置放到一个地方进行集中管理:

  • 这个时候可以使用react-router-config来完成;
1.安装react-router-config:
yarn add react-router-config
2.创建router/index.js文件:
import Home from '../pages/Home'
import About,{AboutProduct, AboutMessageb, AboutMessagea} from '../pages/About'
import Profile from '../pages/Profile'

const routes = [
  {
    path:'/',
    exact: true,
    component: Home
  },
  {
    path:'/about',
    component: About,
    routes:[ // 子路由
      {
        path:'/about',
        exact: true,
        component: AboutProduct
      },
      {
        path:'/about/culture',
        component: AboutMessagea
      },
      {
        path:'/about/contact',
        component: AboutMessageb
      }
    ]
  },
  {
    path:'/profile',
    component: Profile
  },

]

export default routes
3. 在app配置路由: 使用路由占位符
import router from './router'
import  { renderRoutes } from 'react-router-config'

{renderRoutes(router)}  // 类似路由占位符
4. 如果是子组件: 使用路由占位符
  • 在跳转到的路由组件中会多一个 this.props.route 属性;
  • route属性代表当前跳转到的路由对象,可以通过该属性获取到 routes
import  { renderRoutes } from 'react-router-config'

{renderRoutes(this.props.route.routes)}

9.路由懒加载(优化)

好处: 只有等当前路由使用了, 才去加载当前的页面或组件
1.router.js配置以下代码:
- import CLDdiscover from "@/pages/discover"
+ const CLDdiscover = React.lazy(_ => import("../pages/discover"));
2.APP.js配置以下代码:

image-20210803160201336

3.通过懒加载打包, 里面的js会 分开打

image-20210803160805950

二十六. React Hooks

1. 简单总结一下hooks:

  • 它可以让我们在不编写class的情况下使用state以及其他的React特性;
  • 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;

Hook的使用场景:

  • Hook的出现基本可以代替我们之前所有使用class组件的地方(除了一些非常不常用的场景);
  • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
  • Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用;
export default function CounterHook() {
 
   // 写法一
  // const arr = useState(0)
  // const state = arr[0]
  // const setState = arr[1]

  // 写法二
  const [count, setCount] = useState(0)

  return (
    <div>
      <h2>当前计数: {count}</h2>
      <button onClick={e => setCount(count + 1)}>+!</button>
    </div>
  )
}

2. useState解析

image-20210712211202646

HOOK指的是useState,useEffect这样的函数

Hooks是对这类函数的统称

3.认识useState

image-20210712211745621

4.useEffect (代替生命周期)

类似vue里面的 created()

1.0 目前我们已经通过hook在函数式组件中定义state,那么类似于生命周期这些呢?
  • Effect Hook 可以让你来完成一些类似于class中生命周期的功能;
  • 事实上,类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects);
  • 所以对于完成这些功能的Hook被称之为 Effect Hook;
2.0 useEffect简介

useEffect,字面意思可以理解为"执行副作用操作",对比于以前react class的写法,可以把 useEffect看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

3.0 使用方法

每次都触发

// 相当于 componentDidMount 和 componentDidUpdate
useEffect(() => {
  console.log(`You clicked ${count} times`);
});

只需要触发一次

 // 由于第二个参数传的是空数组,所以这里相当于componentDidMount
 useEffect(() => {
    console.log('订阅事件')
  },[])

当属性值发生变化才触发

export default function HookUseEffect() {
  const [count, setCount] = useState(0);

 // 只有当count发生变化的时候才会触发,show发生变化不会触发
  useEffect(() => {
    console.log('修改Dom', count)
  },[count])

  return (
    <div>
      <h2>{count}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
    </div>
  )
}
触发, 然后销毁
import React, { useEffect, useState } from 'react'

export default function CustomScorllHook() {
  const [count, setCount] = useState(0);
  useEffect(() => {  // 触发
    setCount(10)
    return () => { // 销毁
      setCount(0) 
    }
  })

  return (
    <div>
      <h2 style={{padding: "1000px 0"}}>当前的位置: {count}</h2>
    </div>
  )
}

useState默认值如果需要计算 ,可以写成一个回调函数
const [name, setName] = useState(() =>{
return JSON.stringify(window.localStorage.getItem('name'))
})

5.useContext(跨组件通信)

在之前的开发中,我们要在组件中使用共享的Context有两种方式:

  • 类组件可以通过 类名.contextType = MyContext方式,在类中获取context;
  • 多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context;

但是多个Context共享时的方式会存在大量的嵌套:

  • Context Hook允许我们通过Hook来直接获取某个Context的值(不需要嵌套);
App.js
import React, { PureComponent } from 'react'

import ContextHookDemo2 from './04_useContext的使用/02.useContext写法'
 
export const UserContext = React.createContext();
export const ThemContext = React.createContext();

export default class App extends PureComponent {
  render() {
    return (
      <div>
         <UserContext.Provider value={{name:'chen',age:18}}>
           <ThemContext.Provider>
             {/* <ContextHookDemo></ContextHookDemo>自己的组件 */}
             <ContextHookDemo2/>
           </ThemContext.Provider>
         </UserContext.Provider>
      </div>
    )
  }
}

在对应的函数式组件中使用Context Hook:

import React,{useContext} from 'react'
import {UserContext,ThemContext } from '../App'

export default function App() {
  const user = useContext(UserContext)
  return (
    <div>
      <span>{user.name}</span>
    </div>
  )
}

6.useRedux

import React,{useState, useReducer} from 'react'

import reducer from './reducer'

export default function useReduxProfile() {

  const [state, dispatch] = useReducer(reducer, {counter: 0})

  return (
    <div>
        <h2>当前计数: {state.counter}</h2>
        <button onClick={e => {dispatch({type: "increment"})}}>+1</button>
        <button onClick={e => {dispatch({type: "decrement"})}}>-1</button>
    </div>
  )
}

很多人看到useReducer的第一反应应该是redux的某个替代品,其实并不是。

useReducer仅仅是useState的一种替代方案:

  • 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分;
  • 或者这次修改的state需要依赖之前的state时,也可以使用;

单独创建一个reducer/counter.js文件:

数据是不会共享的,它们只是使用了相同的counterReducer的函数而已。

所以,useReducer只是useState的一种替代品,并不能替代Redux。

7.useCallback性能优化

子组件依赖的函数没有被改变,就不执行(组件的回调函数常用)

1.0 useCallback实际的目的是为了进行性能的优化.

如何进行性能的优化呢?

  • useCallback会返回一个函数的 memoized(记忆的) 值;
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
2.0 useMemo 和 useCallback 接收的参数都是一样,第一个参数为回调 第二个参数为要依赖的数据

在将一个组件中的函数,传递给子元素进行回调函数使用时, 使用useCallback对函数镜像处理

image-20210714230059285

8.useMemo性能优化

依赖的函数没有发生变化就不会执行

  • useCallbackuseMemo的语法糖

例子1:

image-20210714233235136

例子2:

image-20210714234532518

9.useCallback 和 useMemo的区别

1.0 共同作用:

仅仅 依赖数据 发生变化, 才会重新计算结果,也就是起到缓存的作用。

2.0 两者区别:
  • useMemo计算结果是 return回来的值, 主要用于 缓存计算结果的值 ,应用场景如: 需要 计算的状态

  • useCallback 计算结果是 函数, 主要用于 缓存函数,应用场景如: 需要缓存的函数,因为函数式组件每次任何一个 state 的变化 整个组件 都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。

image-20210714235336948

10. useRef

useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变。

最常用的ref是两种用法:

  • 用法一:引入DOM(或者组件,但是需要是class组件)元素;
  • 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变;
1. 0 用法一: 引用dom
import React,{useRef,PureComponent} from 'react'

class Son extends PureComponent{ 
  render(){
    return <h2>SON</h2>
  }
}

export default function useDomHook1() {

  const titleRef = useRef()
  const inputRef = useRef()
  const SonRef = useRef()

  function changeDom() {
    titleRef.current.innerHTML = "Hello world"
    inputRef.current.focus()
    console.log(SonRef.current,'xxx') // 打印组件的内容 只支持class组件
  }

  return (
    <div>
      <h2 ref={titleRef}>RefHookDome1</h2>
      <input type="text" ref={inputRef}/>
      <button onClick={ changeDom}>DOM替换title</button>
      <Son ref={SonRef}/>
    </div>
  )
}

1.1 用法二:使用ref保存上一次的某一个值
  • useRef可以想象成在ref对象中保存了一个.current的可变盒子;
  • useRef在组件重新渲染时,返回的依然是之前的ref对象,但是current是可以修改的;
export default function RefHooksDome2() {
  const numRef = useRef(10) // red可以默认传入一个值 
  const [count, setCount] = useState(0)

  // 利用useEffect的效果 , 只有count改变了 才执行 numRef.current赋值(记录上一次的数据)
   useEffect(() => { 
     numRef.current = count
   },[count])

  function addInter() {
    setCount(count + 10)
 }

  return (
    <div>
      <h2>RefHooksDome2: {numRef.current}</h2>
      <h2>计算: {count}</h2>
      <button onClick={addInter}>+10</button>
    </div>
  )
}

11.forwardRef 和useImperativeHandle

1.0 forwardRef
  • forwardRef可以用 ref 操作函数组件的所有dom方法
import React, {useRef, forwardRef } from 'react'

  const HyInput = forwardRef((props, ref) =>{
    return <input ref={ref} type="text" />
  })

export default function ForwardRedDome1() {

  const inputRef = useRef()

  return (
    <div>
      <HyInput ref={inputRef}/>
     <button onClick={e => inputRef.current.focus()}>操作function组件</button>
    </div>
  )
}
1.2 useImperativeHandle

useImperativeHandle也是用来操作函数组件的

forwardRef 和 useImperativeHandle区别

  • forwardRed是将子组件的DOM直接暴露给了父组件
  • useImperativeHandle可以只暴露固定的操作:
    • 比如只允许父组件可以操作focus,其他并不希望它随意操作
    • 相当于子组件暴露一个你只能修改color, 父组件通过ref只能修改color
import React, { useRef, forwardRef, useImperativeHandle } from 'react'

const HyInput = forwardRef((props, ref) => {
  const inputRef = useRef()

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus()
    }
  }))
  return <input ref={inputRef} type="text" />
})

export default function ForwardRedDome2() {

  const inputRef = useRef()

  return (
    <div>
      <HyInput ref={inputRef} />
      <button onClick={e => inputRef.current.focus()}>操作function组件</button>
    </div>
  )
}

12. useLayoutEffect

useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:

  • useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
  • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;

如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect。

例子1:

通过 点击更新 count, 此时执行一次dom更新, useEffect监听到count发生变化也会执行内部的setCount来修改DOM, 从而导致两次刷新两次dom操作,出现白屏

useLayoutEffect,可以代替useEffect, 等所有代码执行完毕后, 再进行DOM操作

import React, { useEffect, useLayoutEffect, useState } from 'react'

export default function UseLayoutEffectDom() {

  const [count, setCount] = useState(10)
  
  // 点击修改 count为:0 后会去更新dom, useEffect里面监听到count发生改变,也会更新dom 从而导致闪屏
  // useEffect(() => { 
  //   if(count == 0){
  //     setCount(Math.random()*50)
  //   }
  // },[count])

  // 使用 useLayoutEffect就不会出现闪屏, useLayoutEffect会等所有代码执行完毕后,再进行DOM的更新;
  useLayoutEffect(() => {
    if(count == 0){
      setCount(Math.random()*50)
    }
  },[count])

  return (
    <div>
      <h2>计数: {count}</h2>
      <button onClick={e => {setCount(0)}}>随机数</button>
    </div>
  )
}

14. 自定义hooks

1.0练习一: Context的共享
// app.js 创建需要共享的值
<UserContext.Provider value={{name:'chen'}}>
<ThemContext.Provider value={{value:'777'}}>
 <UseHooks/>
</ThemContext.Provider>
</UserContext.Provider>

// userHooks.js 创建自定义hook 封装Context 
import { useContext } from 'react'
import { UserContext, ThemContext} from '../App'

 function useUserContext() {
  const user = useContext(UserContext)
  const token = useContext(ThemContext)
  
  return [user, token] // 自定义hook返回两个值
 }

 export default useUserContext
 
 // cont.js 需要使用的共享数据的组件
 import React from 'react'
import useUserContext from '../hooks/user-hooks' // 引入自定义的hooks

export default function UserHooks() {
const [user, token] = useUserContext() // 直接获取自定义hook的函数
  return (
    <div>
      <h2>useHooks</h2>
      <h4>{user.name}</h4>
      <h4>{token.value}</h4>
    </div>
  )
}
1.1 练习二: 获取滚动距离
// use-scroll.js  创建自定义hooks
import { useEffect, useState} from "react";

function useScrollHook() {
  const [scollNumber, setScollNumber] = useState(0)

  const getScorll = () => {
    setScollNumber(window.scrollY)
  }

  useEffect(() =>{
    
    document.addEventListener('scroll',getScorll)
    return () => {
      document.removeEventListener('scroll',getScorll)
    }
  },[])
  return scollNumber
}

export default useScrollHook

// 使用自定义hooks
import React, { useEffect, useState } from 'react'
import useScrollHook from '../hooks/user-scroll-hook' // 导入自定义hooks

export default function CustomScorllHook() {

  const scrollTop = useScrollHook()
  console.log(scrollTop)

    return (
    <div style={{padding: "1000px 0"}}>
      <h2 style={{position: "fixed", left:0 , top:0}}>当前的位置:{scrollTop}</h2>
    </div>
    )
    }

1.2 练习三: 动态设置缓存
// user-store-hook.js 创建自动以hooks
import { useEffect, useState } from "react";

function useLocalStorage(key) { // 传入缓存名字
  const [name, setName] = useState(() => {
    return JSON.parse(window.localStorage.getItem('name'))
  })

  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(name))
  },[name])

  return [name,setName] // 导出setName是为了好调用这个方法
}

export default useLocalStorage

// 使用自定义hooks
import useLocalStorage  from '../hooks/user-store-hook' // 导入自定义hooks

export default function CustomDataLocal() {

  // 使用自定义hook封装

  const [name, setName] = useLocalStorage('name')

  return (
    <div>
       <h2>CustomDataStoreHook: {name}</h2>
      <button onClick={e => setName("大哥666")}>设置</button>
    </div>
  )
}

二十七: 实战项目使用 redux

1.先下载依赖包

yarn add redux react-redux redux-thunk

2.配置目录:

image-20210722085209287

3.先配置 最大的store

image-20210722085550904

4.配置一个: 合并所有子reudex 的 reducer文件

导入子redux: 这里是配置好了一个 bander , 所以先引用了:

image-20210722085849765

5.通过Provider共享所有 store

image-20210722144820503

6. 配置需要的子redux

  • 创建一个bander文件夹 ,并且在文件夹内创建以下四个文件

  • 创建一个 Module模块, 里面放左右子 redux; 方便好找

  • 子redux 暴露出一个 reducer , 最外层的 reducer 利用 combineTeduers 对此进行合并

image-20210722091720382

7.配置子redux

有两种写法:
第一种: 是通过connect()
  • mapStateToProps:更新props————>作为输入源。返回一个对象,key为UI界面对应的名称,value为state处理的结果
  • mapDispatchToProps:更新action————>作为输出源。触发action更新reducer,进而更新state, 引起UI的变化
  • 缺点: 每次都需要写 mapStateToProps 和 mapDispatchToProps
第二种: 通过useSelector()
  • useSelector可以订阅store, 当action被dispatched的时候,会运行selector。可以直接拿到stort
  • 优点: 代码简单明了,
  • 缺点: useSelector 使用的是 === 来比较, 比较的是两个引用类型 , 所以每次都会没 re-render重新加载(浪费性能)
  • 解决: 可以引用一个 shallowEqual(), 让 useSelector进行浅层比较;
整体流程图:

image-20210722164818843

1.对应的代码流程图(connect写法):

image-20210722170546546

2. 对应的代码流程图(hooks写法) 常用!!!!

useSelector()有个bug: 比较的是两个引用地址, 所以 需要 加上 shallowEqual

shallowEqual: 是进行浅层比较(比较的是值)

但是下图没有写: 需要自己加上

image-20210722175333522

8.解决 hooks里使用 useSelector() 的方法

  • useSelector()将对前一个选择器结果值和当前结果值进行引用比较。 如果它们不同,则将强制重新渲染组件如果它们相同,则组件将不会重新渲染
1.例子:
  • 当我在 home组件里 更改redux里面的值 , about组件被重新渲染了(about组件没有依赖home更改的值)

    image-20210722213415227

    about组件被重新加载了(这是非常浪费性能的)

image-20210722212644937

2.原因?

因为useSelector是 === 比较两个引用类型, 所以每次都会被重新

image-20210722212614941

3.解决办法: shallowEqual()

​ shallowEqual比较值相等,或者对象含有相同的属性、且属性值相等。(浅层比较)

image-20210722214003262

源码: 因为 shallowEqual 是浅层比较 比较的是 值相等

image-20210722213646645

二十八. 数据的可变引起的问题 (优化)

1.介绍 Immutable

Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。

Map() : 是浅层比较只会比较一层对象,

fromJS(): 是深层比较

image-20210725211249536

immutable.js

image-20210725211259258

2.为什么要在redux里使用

  • 因为在存函数里操作这个默认值,必须要进行浅拷贝一下(然后进行替换赋值)0

  • 但是如果数据量很大, 并且全部进行拷贝那就很影响性能了

image-20210725115757968

3.使用 Immutable

1. 下载
yarn add immutable
2.在redux.js 用Map包裹默认值, 并且设置一个key

image-20210725122347018

3.使用 get() 拿刚刚设置的key

image-20210725122456161

4.最后的优化 redux-immutable

Immutable 可以给应用带来极大的性能提升

1.下载
yarn add redux-immutable
2.使用

在最外层的 redux里使用

  • 在 最外层的 reducer.js修改一下配置

image-20210725211700160

  • 使用

image-20210725213717447

原文地址:https://www.cnblogs.com/cl1998/p/15172125.html