React18 + React-Router v6 + Redux + React Hooks

一、React基础

1.React的特点
  • 声明式编程:Vue也是这个模式,它允许我们只需要维护自己的状态,当状态改变的时候,React可以根据最新的状态去渲染我们的UI界面;
  • 组件化开发;
  • 多平台适配:ReactNative,ReactVR。
2.React的开发依赖

(1) 开发React必须依赖三个库:react、react-dom、babel;

  • react:包含react所必须的核心代码;
  • react-dom:react渲染到不同平台所需要的代码;
  • babel:将jsx转化为react代码的工具。

(2) 在React 0.14版本之前是没有react-dom这个概念的,所有的功能都包含在react里面;至于为什么要拆分,主要是因为react-native,react包含了react web和react-native所共同拥有的核心代码;react-dom针对web和native所完成的事情不同:web端react-dom会将jsx最终渲染成真实的dom,显示在浏览器中;native端:react-dom会将jsx最终渲染成原生的控件(Android,ios)。

(3) React和Babel的关系:

默认情况下其实可以不使用babel,但前提是我们自己使用React.createElement来编写源代码,它的编写十分繁琐,可读性也很差;所以我们就是用jsx语法,babel就是将jsx转化为React.createElement。

3.React组件化开发
<div class="root"></div>
<script src="./lib/react.js"></script>
<script src="./lib/react-dom.js"></script>
<script src="./lib/babel.js"></script>
​
















<script type="text/babel">
    class App extends React.Component {
        constructor() {
            super()
            this.state = {
                message: "hello world"
            }
            this.btnClick = this.btnClick.bind(this)
        }
​







        btnClick() {
            this.setState({
                message: "hello react"
            })
        }
​








        render() {
            return (

                <div>

                    <h2>{this.state.message}</h2>
                    <button onClick={this.btnClick}>修改文本</button>
                </div>
            )
        }
    }
    const root = ReactDOM.createRoot(document.querySelector(".root"))
    root.render(<App />)
</script>
4.事件绑定

在类中定义一个函数,直接将其绑定在元素的onClick事件上,这个函数的this指向的是undefined:这是因为在正常的DOM操作中,监听点击,监听函数的this其实是节点对象(比如说button);React并不是直接渲染成真实的DOM,我们所编写的button其实只是一个语法糖,它的本质是React的Element对象;那么,在发生监听的时候,react在执行函数时并没有绑定this,默认情况下就是一个undefined。

我们在绑定的函数中,可能想要使用当前的象,比如执行this.setState函数,就必须拿到当前对象的this,我们就需要在传入函数时,给这个函数绑定this。

二、JSX

1.JSX是什么?

JSX是js的语法扩展,它用于描述我们的UI界面,并且完全可以和js融合在一起使用,它不同于vue的模板语法,不用单独学习模板语法的一些指令。

2.JSX的书写规范

(1) JSX的顶层只能有一个根元素,所以我们很多时候会在外层包裹一个div(或者使用Fragment);

(2) 我们通常在jsx的外层包裹一个小括号(),这样可以方便阅读,并且jsx可以进行换行书写;

(3) jsx中的标签可以是单标签也可以是双标签。

3.jsx的使用

(1) jsx的注释:

{ /* JSX的注释写法 */ }

(2) jsx嵌入变量作为子元素:

  • 当变量是Number、String、Array类型时,可以直接显示;
  • 当变量是null、undefined、Boolean,内容为空,如果要显示需要转化为字符串;
  • Object类型不能作为子元素。

(3) jsx嵌入表达式:

  • 运算表达式
  • 三元表达式
  • 执行一个函数
<script type="text/babel">



    class App extends React.Component {



        constructor() {



            super()



            this.state = {



                movies: ["梦幻西游", "大话西游", "西游记", "红楼梦"],
                num: 10,
                str: "yuan",
                arr: [1, 2, 3, 4, 5],
                aaa: null,
                bbb: undefined,
                ccc: true,
                friend: {
                    name: "yuan"
                }
            }
        }
​










        render() {
            const { num, str, arr, aaa, bbb, ccc, friend, movies } = this.state
            return (
                <div>
                    <h2>{num}</h2>
                    <h2>{str}</h2>
                    <h2>{arr}</h2>
                    <h2>{String(aaa)}</h2>
                    <h2>{bbb + ""}</h2>
                    <h2>{ccc.toString()}</h2>
                    <h2>{friend.name}</h2>
                    <h2>{Object.keys(friend)[0]}</h2>
                    <h2>{10 + 20}</h2>
                    <h2>{10 > 20 ? "yes" : "no"}</h2>
                    <ul>
                        {
                            movies.map(item => {
                                return <li key={item}>{item}</li>
                            })
                        }
                    </ul>
                    <ul>{this.getMovies()}</ul>
                </div>
            )
        }
​

        getMovies() {
            const liEl = this.state.movies.map(movies => <li key={movies}>{movies}</li>)
            return liEl
        }
    }
    const root = ReactDOM.createRoot(document.querySelector(".root"))
    root.render(<App />)
</script>

(4) jsx中绑定属性:

<script type="text/babel">



    class App extends React.Component {



        constructor() {



            super()



            this.state = {



                title: "哈哈哈",
                imgUrl: "https://ts1.cn.mm.bing.net/th/id/R-C.95bc299c3f1f0e69b9eb1d0772b14a98?rik=W5QLhXiERW4nLQ&riu=http%3a%2f%2f20178405.s21i.faiusr.com%2f2%2fABUIABACGAAgoeLO-wUo4I3o2gEw8Qs4uAg.jpg&ehk=N7Bxe9nqM08w4evC2kK6yyC%2bxIWTjdd6HgXsQYPbMj0%3d&risl=&pid=ImgRaw&r=0",
                href: "https://www.baidu.com",
                isActive: true,
                objStyle: { color: "red", fontSize: "30px" }
            }
        }
​








        render() {
            const { title, imgUrl, href, isActive, objStyle } = this.state
            // 1.字符串拼接
            const className = `abc cba ${isActive ? 'active' : ''}`
            // 2.将所有的class放到数组中
            const classList = ['abc', 'cba']
            if (isActive) classList.push("active")
            // 3.使用第三方库:classnames
​








            return (

                <div>

                    <img src={imgUrl}  />
                    <h2 title={title}>我是h2元素</h2>
                    <a href={href}>百度一下</a>
                    <h2 style={objStyle}>你好</h2>
                    <h2 className={className}>className</h2>
                    <h2 className={classList.join(" ")}>哈哈哈哈</h2>
                </div>
            )
        }
    }
    const root = ReactDOM.createRoot(document.querySelector(".root"))
    root.render(<App />)
</script>

三、React事件绑定

前面已经说了事件绑定为什么要绑定this,这里就直接列举出事件绑定的三种方案:

(1) 在构造函数中显示绑定this;

(2) 声明函数的时候使用箭头函数;

(3) 事件监听的时候传入一个箭头函数;

<script type="text/babel">
    class App extends React.Component {
        constructor() {
            super()
            this.state = {
                counter: 100
            }

​















            this.btn1Click = this.btn1Click.bind(this)
        }

​















        btn1Click() {
            this.setState({
                counter: this.state.counter + 1
            })
        }
​













        btn2Click = () => {
            this.setState({
                counter: this.state.counter + 1
            })
        }
​







        btn3Click() {
            this.setState({
                counter: this.state.counter + 1
            })
        }
​










        render() {
            const { counter } = this.state
​







            return (
                <div>
                    <h2>当前计数:{counter}</h2>
                    {/* 1.this绑定方式一: bind绑定 */}
                    <button onClick={this.btn1Click}>btn1</button>
                    {/* 2.this绑定方式二: ES6 class fields */}
                    <button onClick={this.btn2Click}>btn2</button>
                    {/* 3.this绑定方式三: 直接传入一个箭头函数(重要) */}
                    <button onClick={() => this.btn3Click()}>btn3</button>
                </div>
            )
        }
    }
    const root = ReactDOM.createRoot(document.querySelector(".root"))
    root.render(<App />)
</script>

(4) 事件参数的传递

<script type="text/babel">



    class App extends React.Component {



        constructor() {



            super()



            this.state = {



​











            }

        }
​















        btn1Click(event, name, age) {
            console.log("event", event);
            console.log(name, age);
        }
​








        render() {
​










            return (
                <div>
                    {/* 1.event参数的传递 */}
                    <button onClick={this.btn1Click.bind(this)}>btn1</button>
                    <button onClick={(event) => this.btn1Click(event)}>btn2</button>
                    {/* 1.额外参数的传递 */}
                    <button onClick={this.btn1Click.bind(this, "yuan", 18)}>btn3</button>
                    <button onClick={(event) => this.btn1Click(event, "yuan", 18)}>btn4</button>
                </div>
            )
        }
    }
    const root = ReactDOM.createRoot(document.querySelector(".root"))
    root.render(<App />)
</script>

四、React条件渲染

(1) 条件判断语句:适合逻辑较多的情况;

(2) 三元运算符:适合逻辑简单的情况;

(3) 与运算符&&:适合条件成立渲染组件,不成立什么内容也不显示。

<script type="text/babel">



    class App extends React.Component {



        constructor() {



            super()



            this.state = {



                message: "hello world",
                flag: true,
                friend: undefined
            }
        }

​















        render() {
            const { message, flag, friend } = this.state
            // 1.条件判断方式一:使用if判断
            let showEl = null
            if (flag) {
                showEl = <h2>要显示的内容1</h2>
            } else {
                showEl = <h2>要显示的内容2</h2>
            }
​








            return (
                <div>
                    <div>{showEl}</div>
                     {/* 2.方式二: 三元运算符 */}
                    <div>{flag ? <button>按钮1</button> : <button>按钮2</button>}</div>
                    {/* 3.方式三: &&逻辑与运算 */}
                    <div>{friend && <div>朋友</div>}</div>
                </div>
            )
        }
    }
    const root = ReactDOM.createRoot(document.querySelector(".root"))
    root.render(<App />)
</script>

五、React列表渲染

使用map、filter等数组方法实现

<div className="list">
  {
    students.filter(item => item.score > 100).slice(0, 2).map(item => {
      return (
        <div className="item" key={item.id}>
          <h2>学号: {item.id}</h2>
          <h3>姓名: {item.name}</h3>
          <h1>分数: {item.score}</h1>
        </div>
      )
    })
  }




</div>

六、React脚手架

create-react-app project-name

npm start启动项目

npm eject:将webpack相关的配置显示,此过程是不可逆的

七、React组件化开发

1.React组件的分类

(1) 根据组件的定义方式,可以分为函数组件和类组件;

(2) 根据组件内部是否有需要维护的状态,可以分为:无状态组件和有状态组件;

(3) 根据组件的职责不同,还可以分为展示型组件和容器型组件;

2.类组件

(1) 类组件的定义要求:

  • 组件的名称必须是大写字符开头(无论是类组件还是函数组件);
  • 类组件必须继承自React.Component;
  • 类组件必须实现render函数。

(2) 使用class定义一个组件:

  • constructor是可选的,我们可以在constructor中初始化一些数据;
  • this.state中维护的是我们组件内部的数据;
  • render()方法是组件中唯一必须要实现的方法;
import { Component } from "react";
​









































class App extends Component {
  constructor() {
    super();
    this.state = {};
  }




​















  render() {
    return (

      <>
        <div>App</div>
      </>
    );
  }



}


​













export default App;
3.函数组件

函数组件有自己的特点(后面的hooks就不一样了):没有生命周期,但是会被更新挂载;this关键字不能指向组件实例,因为没有组件实例;没有内部的状态(state)。

export default function App() {
  return (
    <div>
      <h2>我是h2</h2>
      <h2>hello func</h2>
    </div>
  );
}
4.组件的生命周期

(1) constructor():如果不初始化state或不进行方法的绑定,则不需要为React组件实现构造函数;constructor通常只做两件事:通过this.state赋值对象来初始化内部的state,为事件绑定实例(this)。

(2) componentDidMount():会在组件挂载后立即调用。

在componentDidMount中通常进行的操作:对DOM进行操作;发送网络请求(官方建议);在此处添加一些订阅(可以在componentWillUnMount取消订阅)。

(3) componentDidUpdate(prevProps,prevState,snapShot):会在更新后调用,初次渲染不会调用;在组件更新后,可以在此处对DOM进行操作;如果你对更新前后的props进行了比较,也可以在此处发送网络请求(如果props没有发生改变就不会发送网络请求)。

(4) componentWillUnMount():会在组件卸载及销毁之前调用:可以在此方法执行必要的清除操作,例如清除定时器、取消网络请求或者取消订阅。

(5) getDerivedStateFromProps():该方法返回一个对象来更新state。

(6) getSnapshotBeforeUpdate():在react更新DOM前回调的一个函数,可以获取到DOM更新前的一些数据。

(7) shouldComponentUpdate():必须返回true才能更新组件,可用于性能优化,PureComponent自动实现该方法。

挂载阶段(Mounting):

  • constructor
  • getDerivedStateFromProps
  • render
  • componentDidMount

更新阶段(Updating):

  • getDerivedStateFromProps
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

卸载阶段(UnMounting):

  • componentWillUnMount
import React from "react";




​









































class HelloWorld extends React.Component {
  constructor(props) {
    super(props);
    this.state = {




      message: "hello world",

      name: "yuan",
    };
    console.log("constructor");
  }






​












  changeText() {
    this.setState({ message: "hello react" });
  }



​










  render() {
    console.log("render");
    const { message, name } = this.state;
    return (


      <div>

        <h2>
          {message}--{name}
        </h2>
        <button onClick={() => this.changeText()}>修改文本</button>
      </div>
    );


  }




​










  componentDidMount() {
    console.log("componentDidMount");
  }


​




  componentDidUpdate(prevProps, prevState, snapShot) {
    console.log("componentDidUpdate", prevProps, prevState, snapShot);
  }


​




  shouldComponentUpdate() {
    return true;
  }
​

  getSnapshotBeforeUpdate() {
    console.log("getSnapshotBeforeUpdate");
    return {
      aihao: "xiao",
    };
  }
​


  static getDerivedStateFromProps(props, state) {
    console.log("getDerivedStateFromProps");
    return {
      //   name: state.name,
    };
  }
​
  componentWillUnmount() {
    console.log("componentWillUnmount");
  }
}
​
export default HelloWorld;

App:

import React from "react";




import HelloWorld from "./HelloWorld";
​















class App extends React.Component {
  constructor() {

    super();

    this.state = {

      showFlag: true,
      name: "xiao",
    };




  }






​












  btnClick() {
    this.setState({

      showFlag: !this.state.showFlag,
    });

  }





​










  render() {


    const { showFlag, name } = this.state;
    return (



      <div>


        {showFlag && <HelloWorld name={name}></HelloWorld>}
        <button onClick={() => this.btnClick()}>显示隐藏</button>
      </div>


    );



  }





}



​










export default App;



除此之外,React还有一些过期的生命周期函数,这些周期函数已经不推荐使用了,如componentWillMount、componentWillUpdate。

5.组件之间的通信

(1) 父传子:父组件通过属性=值的形式传递给子组件数据,子组件通过props参数获取父组件传递过来的数据。

{/*类组件*/}
import React from "react";




​















class Children extends React.Component {
  constructor(props) {

    super(props);

    this.state = {};

  }







​















  render() {


    const { name } = this.props;
    return <div>{name}</div>;
  }



}


​







export default Children;
​













{/*函数组件*/}
export default function ChildrenFunc(props) {
  const { name } = props;
  return (


    <div>

      <h2>{name}</h2>
    </div>
  );

}

​





{/*父组件*/}
import React from "react";
import Children from "./Children";
import ChildrenFunc from "./Children-func";
​







class Parent extends React.Component {
  constructor() {
    super();
    this.state = {};
  }
​




  render() {
    return (
      <div>
        <Children name="yuan"></Children>
        <ChildrenFunc name="xiao"></ChildrenFunc>
      </div>
    );
  }

}
​


export default Parent;

(2) 父传子-参数验证:propsTypes

如果你的项目中默认继承了flow或者ts,那么就可以直接进行类型的验证;如果没有,那可以使用prop-types库来进行参数验证;

import React from "react";
import PropTypes from "prop-types";
​















class Children extends React.Component {
  //   static defaultProps = {
  //     name: "默认姓名",
  //   };
​















  constructor(props) {
    super(props);
    this.state = {};
  }




​








  render() {
    const { name } = this.props;
    return <div>{name}</div>;
  }





}
​



Children.propTypes = {
  name: PropTypes.string,
};
​







Children.defaultProps = {
  name: "默认姓名",
};
​





export default Children;

(3) 子传父:在Vue中是通过自定义事件来完成的,在React中是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可。

{/*父组件*/}
import React from "react";




import AddCounter from "./AddCounter";
​






















class App extends React.Component {
  constructor() {






    super();






    this.state = {


      counter: 0,
    };




  }






​












  addCounter(count) {
    this.setState({

      counter: this.state.counter + count,
    });

  }





​










  render() {


    const { counter } = this.state;

    return (



      <div>


        <h2>{counter}</h2>
        <AddCounter addClick={(count) => this.addCounter(count)}></AddCounter>
      </div>


    );



  }





}



​










export default App;



{/*子组件*/}
import React from "react";
​




class AddCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {};
  }

​


  addCounter(count) {
    this.props.addClick(count);
  }

​


  render() {
    return (
      <div>
        <button onClick={(e) => this.addCounter(1)}>+1</button>
      </div>
    );
  }

}
​
export default AddCounter;
6.React中的插槽

有两种方案可以实现:

  • 组件的children子元素
  • 通过props传递React元素
import React, { Component } from "react";
import NavBar from "./nav-bar";
import NavBarTwo from "./nav-bar-two";
​






















export class App extends Component {
  render() {

    const btn = <button>按钮2</button>;
​















    return (


      <div>


        {/* 1.使用children实现插槽 */}
        {/* 只传一个就是children */}
        <NavBar>
          <button>按钮</button>
          <h2>哈哈哈</h2>
          <i>斜体文本</i>
        </NavBar>
​










        {/* 2.使用props实现插槽 */}
        <NavBarTwo
          leftSlot={btn}
          centerSlot={<h2>呵呵呵</h2>}
          rightSlot={<i>斜体2</i>}
        />
      </div>


    );



  }





}



​










export default App;



​





{/*NavBar*/}
import React from "react";
​



class NavBar extends React.Component {
  constructor(props) {

    super(props);

    this.state = {};

  }


​



  render() {

    const { children } = this.props;
    return (
      <div>
        {children[0]}
        {children[1]}
      </div>
    );

  }

}

​


export default NavBar;
​

{/*TabBar*/}
import React from "react";
​

class TabBar extends React.Component {
  constructor(props) {
    super(props);
    this.state = {};

  }

​

  render() {

    const { btn, divEl } = this.props;
    return (

      <div>

        {btn}
        {divEl}
      </div>
    );
  }
}
​

export default TabBar;
7.非父子组件传值-context

(1) 如果组件的层级比较深,需要一层一层传递数据,这样代码是十分冗余的,因此React提供了一个API-Context来实现,Context设计的目的是为了共享那些对于一个组件树而言是全局的数据。

(2) Context相关的API:

  • React.createContext(defaultValue)

    • 创建一个需要共享的Context对象;
    • 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的Provider中读取到当前的Context值;
    • defaultValue是组件在顶层的查找过程中没有找到对应的Provider,那么使用默认值。
  • Context.Provider

    • 每一个Context对象都会返回一个Provider React组件,它允许组件订阅context的变化;
    • Provider接受一个value属性,传递给消费组件;
    • 一个Provider可以和多个消费组件有对应关系;
    • 多个Provider也可以嵌套使用,里层的会覆盖外层的数据;
    • 当Provider的value值发生改变的时候,它内部的所有消费组件都会重新渲染。
  • Class.contextType

    • 挂载在class上的contextType属性会被重新赋值为一个由React.createContext()创建的Context对象,可以让你使用this.context来消费最近Context上的那个值;
    • 你可以在任何一个生命周期中访问它,包括render函数中。
  • Context.Consumer

    • 订阅Provider到context的变更,可以在函数式组件中使用;
    • 这里需要函数作为子元素;
    • 这个函数接受当前的context的值,返回一个React节点。
//theme-context.js
import React from "react";




const ThemeContext = React.createContext();
export default ThemeContext;
//user-context.js
import React from "react";
const UserContext = React.createContext();
export default UserContext;
{/*App*/}
import React from "react";




import Home from "./Home";

import UserContext from "./context/user-context";
import ThemeContext from "./context/theme-context";
​











class App extends React.Component {
  constructor() {
    super();
    this.state = {
      info: {
        name: "yuan",
        age: 30,
      },
    };

  }





​













  render() {

    const { info } = this.state;
    return (


      <div>

        <ThemeContext.Provider value={{ color: "red", fontSize: "30px" }}>
          <UserContext.Provider value={{ nickname: "ayuan", aihao: "美女" }}>
            <Home {...info}></Home>
          </UserContext.Provider>
        </ThemeContext.Provider>
      </div>

    );


  }



}


​





export default App;


{/*Home*/}
import React from "react";
import UserContext from "./context/user-context";
import ThemeContext from "./context/theme-context";
import Func from "./Func";
​




class Home extends React.Component {
  constructor() {
    super();
    this.state = {};
  }
​

  render() {
    const { name, age } = this.props;
    const { color, fontSize } = this.context;
    console.log(this.context);
    return (
      <div>
        <h2>
          {name} - {age}
        </h2>
        <UserContext.Consumer>
          {(value) => {
            return (
              <h2 style={{ color, fontSize }}>
                {value.nickname}-{value.aihao}
              </h2>
            );
          }}
        </UserContext.Consumer>
        <Func></Func>
      </div>
    );
  }
}
​
Home.contextType = ThemeContext;
export default Home;
{/*Func*/}
import ThemeContext from "./context/theme-context";
​

export default function Func(props) {
  return (
    <div>
      <ThemeContext.Consumer>
        {(value) => {
          return (
            <div style={{ color: value.color, fontSize: value.fontSize }}>
              你好,div
            </div>
          );
        }}
      </ThemeContext.Consumer>
    </div>
  );
}
8.非父子通信-eventbus

(1) npm i mitt

(2) 新建一个eventbus文件

import mitt from "mitt";
​









































const emitter = mitt();
export default emitter;

(3) 使用

{/*User组件*/}
import React from "react";




import emitter from "./eventbus/index";
​






















class User extends React.Component {
  constructor() {






    super();






    this.state = {};


  }






​









  pub() {
    emitter.emit("hello", { name: "yuan", age: 18 });
  }



​








  render() {
    return (

      <div>

        <button onClick={() => this.pub()}>发布</button>
      </div>
    );
  }

}
​







export default User;
​





{/*Home组件*/}
import React from "react";
import emitter from "./eventbus/index";
​










class Home extends React.Component {
  constructor() {
    super();
    this.state = {};
  }
​


  render() {
    return (

      <div>

        <h2>home</h2>
      </div>
    );
  }

​


  componentDidMount() {
    emitter.on("hello", (value) => {
      console.log(value);
    });
  }
}
​

export default Home;
9.setState的使用

(1) react18,setState设计为异步的,可以显著提高性能,如果每次调用setState都进行一次更新,那么render函数会被频繁调用,界面都要重新渲染,这样的效率是很低的,最好的办法就是获取到多个更新,之后进行批量更新。

(2) setState调用的三种方式:

import React from "react";




​









































class App extends React.Component {
  constructor() {




    super();




    this.state = {




      message: "hello world",

    };


  }






​









  btn1Click() {
    // 1.普通调用
    // this.setState({ message: "hello react" });
    // 2.可以传入一个回调函数:可以在回调函数中写新的state逻辑;可以将之前的state和props传递进来
    // this.setState((state, props) => {
    //   console.log(state, props);
    //   return {
    //     message: "hello react",
    //   };
    // });
    // 3.setState在react事件中是一个异步调用,如果希望在更新state后可以获取对应的结果,可以传入第二个参数callback
    this.setState({ message: "hello react" }, () => {
      console.log(this.state.message);
    });

  }
​

  render() {
    const { message } = this.state;
    return (
      <div>
        <h2>{message}</h2>
        <button onClick={() => this.btn1Click()}>按钮1</button>
      </div>
    );



  }



}



​




export default App;

(3) 在react18之前,在组件的生命周期和合成事件中setState是异步的;在定时器或者原生的DOM事件中,setState是同步的。

(4) 如果我们希望代码同步,则需要执行特殊的flushSync操作

import { flushSync } from "react-dom";
​









































flushSync(() => {
  this.setState({ message: "hello react" });
});
console.log(this.state.message);
10.React性能优化SCU

(1) React渲染机制:jsx –> 虚拟DOM –> 真实DOM

(2) React更新机制:props/state改变 –> render函数重新执行 –> 产生新的DOM树 –> 新旧DOM树进行对比 –> 计算出差异进行更新 –> 更新到真实的DOM

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

(4) shouldComponentUpdate:

  • 该方法有两个参数:

    • 参数一:nextProps:最新的props属性
    • 参数二:nextState:最新的state属性
  • 该方法返回值是一个Boolean类型:

    • 返回值为true,那么就需要调用render方法;
    • 返回值为false,那么就不需要调用render方法;
    • 默认返回的是true,也就是只要state发生改变,就会调用render方法。
shouldComponentUpdate(newProps, nextState) {
    // 自己对比state是否发生改变: this.state和nextState
    if (this.props.message !== newProps.message) {
      return true
    }
    return false
}

(5) 如果所有的类都需要我们手动的实现shouldComponentUpdate会增加很多的工作量,react已经考虑到了这一点,所以react默认帮我们实现好了,我们只需要将class继承自PureComponent

{/*App组件*/}
import { PureComponent } from "react";
import Home from "./Home";

import Profile from "./Profile";
​
















class App extends PureComponent {
  constructor() {
    super();
    this.state = {


      message: "hello world",
    };

  }




​








  changeText() {
    this.setState({ message: "hello home" });
  }





​













  render() {

    const { message } = this.state;
    return (


      <>
        <Home message={message}></Home>
        <Profile></Profile>
        <button onClick={() => this.changeText()}>修改home文本</button>
      </>
    );



  }





}



​










export default App;



​





{/*Home组件*/}
import { PureComponent } from "react";
​



class Home extends PureComponent {
  constructor(props) {

    super(props);

    this.state = {};

  }


​



  render() {

    console.log("home render");
    const { message } = this.props;
    return (
      <>
        <h2>{message}</h2>
      </>
    );

  }

}

​


export default Home;
​

{/*Profile组件*/}
import { PureComponent } from "react";
​

class Profile extends PureComponent {
  constructor() {
    super();
    this.state = {};

  }

​

  render() {

    console.log("Profile render");
    return (

      <div>

        <h2>Profile</h2>
      </div>
    );
  }
}
​
export default Profile;

(6) 针对类组件我们使用的是PureComponent,那么对于函数式组件我们使用的是高阶函数memo

import { memo } from "react"
​









































const Profile = memo(function(props) {
  console.log("profile render")
  return <h2>Profile: {props.message}</h2>
})
​











export default Profile
11.ref获取DOM和组件

(1) 创建ref来获取对应的DOM,目前有三种方式:

  • 方式一:传入一个字符串,使用this.refs.传入的字符串获取;
  • 方式二:传入一个对象,对象是通过React.createRef()方式创建出来的,使用时获取到创建对象其中有一个current属性就是对应的元素;
  • 方式三:传入一个函数:该函数会在DOM被挂载时进行回调,这个函数会传入一个元素对象,我们可以自己保存,使用时直接拿到之前保存的元素对象即可。
import React, { PureComponent, createRef } from "react";


​









































class App extends PureComponent {



  constructor() {




    super();




    this.state = {};
    this.titleRef = createRef();
    this.titleEl = null;
  }






​









  getNativeDom() {
    // 1.方式一:在React元素上绑定一个ref字符串 (不推荐)
    // console.log(this.refs.yuan);
    // 2.提前创建好ref对象,createRef(),将创建出来的对象绑定到元素
    // console.log(this.titleRef.current);
    // 3.方式三: 传入一个回调函数, 在对应的元素被渲染之后, 回调函数被执行, 并且将元素传入
    console.log(this.titleEl);
  }




​



  render() {
    return (



      <div>


        <h1 ref="yuan">hello world</h1>
        <h2 ref={this.titleRef}>hello yuan</h2>
        <h3 ref={e => this.titleEl = e}>hello title</h3>
        <button onClick={(e) => this.getNativeDom()}>获取DOM</button>
      </div>

    );


  }



}


​





export default App;


(2) ref获取组件实例:

import React, { PureComponent, createRef } from "react";


​









































class HelloWorld extends PureComponent {
  test() {
    console.log("test...");
  }
​











  render() {

    return (


      <div>


        <h2>hell world</h2>
      </div>
    );
  }


}


​










class App extends PureComponent {
  constructor() {
    super();
    this.state = {};
    this.hwRef = createRef();
  }
​







  getComponent() {
    console.log(this.hwRef.current);
    this.hwRef.current.test();
  }





​


  render() {
    return (

      <div>
        <HelloWorld ref={this.hwRef}></HelloWorld>
        <button onClick={(e) => this.getComponent()}>获取组件实例</button>
      </div>
    );

  }


}

​




export default App;

(3) 函数式组件获取DOM:forwardRef

import React, { PureComponent, createRef, forwardRef } from "react";
​









































const HelloWorld = forwardRef(function (props, ref) {
  return (
    <div>
      <h1 ref={ref}>hello world</h1>
      <p>啦啦啦</p>
    </div>
  );



});





​















class App extends PureComponent {
  constructor() {
    super();
    this.state = {};
    this.hwRef = createRef();
  }





​










  getComponent() {
    console.log(this.hwRef.current);
  }

​








  render() {
    return (
      <div>
        <HelloWorld ref={this.hwRef}></HelloWorld>
        <button onClick={(e) => this.getComponent()}>获取组件实例</button>
      </div>
    );

  }


}


​







export default App;

12.受控组件和非受控组件

(1) 在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存一些内部的state。

  • 比如下面的HTML表单元素:

    这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;

    在React中,并没有禁止这个行为,它依然是有效的;

    但是通常情况下会使用js函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;

    实现这种效果的标准方式是使用”受控组件”。

import React, { PureComponent } from "react";











​









































class App extends PureComponent {



  constructor() {




    super();




    this.state = {




      username: "yuan",
    };


  }






​









  inputChange(e) {
    console.log(e.target.value);
    this.setState({ username: e.target.value });
  }


​







  render() {
    const { username } = this.state;
    return (
      <div>
        {/* 受控组件 */}
        <input
          type="text"
          value={username}
          onChange={(e) => this.inputChange(e)}
        />
        {/* 非受控组件 */}
        <input type="text" />
        <h2>username: {username}</h2>
      </div>

    );

  }

}

​




export default App;

(2) 受控组件:

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

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

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

  • 由于在表单元素上设置了value属性,因此显示的值始终为this.state.value,这使得React的state成为唯一的数据源;

  • 由于inputChange在每次按键时都会执行并更新React的state,因此显示的值将随着用户输入而更新。

(3) 非受控组件:

  • React推荐大多数情况下使用受控组件来处理表单数据:

    在受控组件中,表单数据是由React组件来管理的;另一种替代方案是使用非受控组件,这时表单数据将交由DOM节点来处理;

  • 如果要使用非受控组件中的数据,那么我们需要使用ref来从DOM节点中获取表单数据;

  • 在非受控组件中通常使用defaultValue来设置默认值;

  • checkbox、radio支持defaultChecked,select和textarea支持defaultValue。

import React, { PureComponent, createRef } from "react";


​









































class App extends PureComponent {



  constructor() {




    super();




    this.state = {




      username: "",
      password: "",
      checked: false,
      hobbies: [
        { value: "sing", text: "唱", isChecked: false },
        { value: "dance", text: "跳", isChecked: false },
        { value: "rap", text: "rap", isChecked: false },
      ],

      fruit: ["orange"],
      info: "哈哈哈",
    };
​










    this.info = createRef();
  }

​








  submitClick(e) {
    // 1.阻止默认行为
    e.preventDefault();
    // 2.获取到所有表单的数据
    console.log(
      this.state.username,
      this.state.password,
      this.state.checked,
      this.state.fruit,
      this.state.info
    );
    const hobbies = this.state.hobbies
      .filter((item) => item.isChecked)
      .map((item) => item.value);
    console.log("获取的爱好:" + hobbies);
    // 3.发送网络请求
  }

​


  //   inputChange(e) {
  //     this.setState({ username: e.target.value });
  //   }
​


  inputChange(e) {
    this.setState({ [e.target.name]: e.target.value });
  }

​

  checkboxChange(e) {
    this.setState({ checked: e.target.checked });
  }

​


  hobbiesChange(e, index) {
    const hobbies = [...this.state.hobbies];
    hobbies[index].isChecked = e.target.checked;
    this.setState({ hobbies });
  }
​
  fruitChange(e) {
    // console.log(e.target.selectedOptions);
    const options = Array.from(e.target.selectedOptions);
    const values = options.map((item) => item.value);
    this.setState({ fruit: values });
  }

​
  render() {
    const { username, password, checked, hobbies, fruit, info } = this.state;
​
    return (
      <div>
        <form onSubmit={(e) => this.submitClick(e)}>
          {/* input */}
          <label htmlFor="username">
            用户:
            <input
              id="username"
              type="text"
              name="username"
              value={username}
              onChange={(e) => this.inputChange(e)}
            />
          </label>
​
          <label htmlFor="password">
            密码:
            <input
              id="password"
              type="password"
              name="password"
              value={password}
              onChange={(e) => this.inputChange(e)}
            />
          </label>
​
          {/* checkbox单选 */}
          <label htmlFor="argee">
            <input
              type="checkbox"
              id="argee"
              checked={checked}
              onChange={(e) => this.checkboxChange(e)}
            />
          </label>
​
          {/* checkbox多选 */}
          <div>
            您的爱好:
            {hobbies.map((item, index) => {
              return (
                <label htmlFor={item.value} key={item.value}>
                  <input
                    type="checkbox"
                    id={item.value}
                    checked={item.isChecked}
                    onChange={(e) => this.hobbiesChange(e, index)}
                  />
                  <span>{item.text}</span>
                </label>
              );
            })}
            {/* select */}
            <select
              value={fruit}
              onChange={(e) => this.fruitChange(e)}
              multiple
            >
              <option value="apple">苹果</option>
              <option value="orange">橘子</option>
              <option value="banana">香蕉</option>
            </select>
          </div>
​
          {/* 非受控组件 */}
          <input type="text" defaultValue={info} ref={this.info} />
​
          <button type="submit">注册</button>
        </form>
      </div>
    );
  }
}
​
export default App;
13.高阶组件

在学习高阶组件之前,我们先回顾一下高阶函数,什么是高阶函数呢?

高阶函数的维基百科定义:至少满足以下条件之一:

接受一个或多个函数作为输入;

输出一个函数。

js中常见的filter、map、reduce都是高阶函数。

那么什么是高阶组件呢?

官方定义:高阶组件是参数为组件,返回值为新组件的函数。

我们可以进行如下的解析:

首先,高阶函数本身不是一个组件,而是一个函数;

其次,这个函数的参数是一个组件,返回值也是一个组件。

(1) 高阶组件的定义:

定义类组件返回

// 定义一个高阶组件

// 1.高阶组件会接收一个组件作为参数

function hoc(Cpn) {
  
  class NewCpn extends PureComponent {
​











  }




  
  // 2.并且返回一个新的组件

  return NewCpn
}


定义函数组件返回

// 定义一个高阶组件

// 1.高阶组件会接收一个组件作为参数

function hoc(Cpn) {
​






















  function NewCpn() {
​











  }




​















  // 2.并且返回一个新的组件

  return NewCpn
}


(2) 高阶组件的编写和调用过程:

import React, { PureComponent } from "react";




​









































// 定义一个高阶组件
function hoc(Cpn) {
  // 1.定义一个类组件
  class NewCpn extends PureComponent {
    render() {
      return <Cpn></Cpn>;
    }
  }

​















  return NewCpn;
}


​








class HelloWorld extends PureComponent {
  render() {
    return <h1>hello world</h1>;
  }




}




​









const HelloWorldHOC = hoc(HelloWorld);
​








class App extends PureComponent {

  render() {
    return (
      <div>
        <HelloWorldHOC></HelloWorldHOC>
      </div>
    );

  }


}


​







export default App;

(3) 组件的名称可以通过displayName来修改;高阶组件并不是React Api的一部分,它是基于React的组合特性而形成的设计模式。

(4) 高阶组件的应用场景:

应用一:props增强

import React, { PureComponent } from "react";




​









































function enhancedProps(Cpn) {
  class NewCpn extends PureComponent {
    constructor(props) {
      super(props);
      this.state = {
        name: "yuan",
        age: 18,
      };
    }
​












    render() {
      return <Cpn {...this.state} {...this.props}></Cpn>;
    }
  }





​













  return NewCpn;
}




​









export default enhancedProps;
import React, { PureComponent } from "react";




import enhancedProps from "./hoc/enhanced_props";
​















class Home extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {};

  }







​















  render() {

    return (

      <div>

        <h2>
          {this.props.name}-{this.props.age}-{this.props.sex}
        </h2>
      </div>
    );
  }




}




​









const HomeEnhancedProps = enhancedProps(Home);
​








class App extends PureComponent {

  constructor() {
    super();
    this.state = {};
  }





​


  render() {
    return (

      <div>
        <HomeEnhancedProps sex="男"></HomeEnhancedProps>
      </div>
    );



  }



}



​




export default App;

应用二:共享context

import React from "react";




​









































const theme = React.createContext();
​






















export default theme;
import ThemeContext from "../context/theme";
​









































function withTheme(Cpn) {
  return (props) => {
    return (

      <ThemeContext.Consumer>
        {(value) => {
          return <Cpn {...value} {...props}></Cpn>;
        }}
      </ThemeContext.Consumer>
    );

  };

}


​








export default withTheme;
import React, { PureComponent } from "react";











import withTheme from "./hoc/with_theme";
import ThemeContext from "./context/theme";
​






















class Product extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {};


  }






​









  render() {

    const { color, size, name } = this.props;
    return (
      <div>
        <h2>
          {color} - {size} - {name}
        </h2>
      </div>
    );

  }

}



​








const ProductEnhanced = withTheme(Product);
​
class App extends PureComponent {
  constructor() {
    super();
    this.state = {};
  }



​




  render() {
    return (
      <div>
        <ThemeContext.Provider value={{ color: "red", size: "30px" }}>
          <ProductEnhanced name="yuan"></ProductEnhanced>
        </ThemeContext.Provider>
      </div>
    );
  }


}
​

export default App;

应用三:渲染判断鉴权

function loginAuth(Cpn) {
  return (props) => {
    const token = localStorage.getItem("token");
    if (token) {
      return <Cpn {...props}></Cpn>;
    } else {
      return <h2>请先登录</h2>;
    }
  };
}




​















export default loginAuth;
import React, { PureComponent } from "react";




import loginAuth from "./hoc/loginAuth";
​















class About extends PureComponent {
  constructor() {
    super();
    this.state = {};

  }







​















  render() {

    return (

      <div>

        <h2>about</h2>
      </div>
    );
  }





}

​










const AboutEnhanced = loginAuth(About);
​









class App extends PureComponent {
  constructor() {
    super();
    this.state = {};
    // localStorage.setItem("token", "token");
  }

​





  render() {
    return <AboutEnhanced></AboutEnhanced>;
  }


}


​







export default App;

应用四:劫持生命周期

14.Portals的使用

某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元素上的);

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

  • 第一个参数是任何可渲染的React的子元素,例如一个元素、字符串或fragment;
  • 第二个参数是一个DOM元素。
React.createPortals(child,container)

通常来讲,当你从组件的render方法返回一个元素时,该元素将被挂载到DOM节点中离其最近的父节点;

然而,有时候将子元素插入到DOM节点中的不同位置也是有好处的。

import React, { PureComponent } from "react";




import { createPortal } from "react-dom";
​















class App extends PureComponent {
  render() {
    return (
      <div className="app">
        <h1>app h1</h1>
        {createPortal(<h2>app h2</h2>, document.querySelector("#yuan"))}
      </div>
    );

  }




}


​








export default App;
15.Fragment

在之前的开发中,我们总是在一个组件返回内容时包裹一个div,我们希望可以不渲染这个div可以怎么做呢?

使用Fragment,Fragment允许你将子列表分组,而无需向DOM添加额外的节点;

Fragment的短语法:<></>,但是我们需要在Fragment添加key就不能使用短语法。

16.strictMode

严格默认:

  • 识别不安全的生命周期函数;

  • 检测过时的ref API的使用;

  • 检查以外的副作用:

    • 这个组件的constructor会被调用两次;
    • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用;
    • 在生产环境中,是不会被调用两次的;
  • 检测使用废弃的findDOMNode方法:在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了

  • 检测过时的context API:早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的,目前这种方式已经不推荐使用

八、React动画的实现

1.react-transition-group的介绍
  • 在开发中,我们想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验。
  • 当然,我们可以通过原生的CSS来实现这些过渡动画,但是React社区为我们提供了react-transition-group用来完成过渡动画。
  • React曾为开发者提供过动画插件 react-addons-css-transition-group,后由社区维护,形成了现在的 react-transition-

group。

  • 这个库可以帮助我们方便的实现组件的 入场 和 离场 动画,使用时需要进行额外的安装:npm install react-transition-group

  • react-transition-group主要组件:

    • Transition:该组件是一个和平台无关的组件(不一定要结合CSS);在前端开发中,我们一般是结合CSS来完成样式,所以比较常用的是CSSTransition;
    • CSSTransition:在前端开发中,通常使用CSSTransition来完成过渡动画效果;
    • SwitchTransition:两个组件显示和隐藏切换时,使用该组件;
    • TransitionGroup: 将多个动画组件包裹在其中,一般用于列表中元素的动画。
2.CSSTransition
  • CSSTransition是基于Transition组件构建的;

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

  • 它们有三种状态,需要定义对应的CSS样式:

    • 开始状态:对于的类是-appear、-enter、exit;
    • 执行动画:对应的类是-appear-active、-enter-active、-exit-active;
    • 执行结束:对应的类是-appear-done、-enter-done、-exit-done;
  • CSSTransition常见对应的属性:

    • 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:过渡动画的时间

    • appear:是否在初次进入添加动画(需要和in同时为true)

    • unmountOnExit:退出后卸载组件

    • 其他属性可以参考官网来学习:reactcommunity.org/react-trans…

    • CSSTransition对应的钩子函数:主要为了检测动画的执行过程,来完成一些JavaScript的操作

      • onEnter:在进入动画之前被触发;
      • onEntering:在应用进入动画时被触发;
      • onEntered:在应用进入动画结束后被触发;
.yuan-appear,
.yuan-enter {
  opacity: 0;

}



​
















.yuan-appear-active,
.yuan-enter-active {
  opacity: 1;


  transition: opacity 2s ease;
}




​















.yuan-exit {
  opacity: 1;
}


​







.yuan-exit-active {
  opacity: 0;
  transition: opacity 2s ease;
}




import React, { PureComponent, createRef } from "react";
import { CSSTransition } from "react-transition-group";
import "./App.css";
​






















class App extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      isShow: true,
    };
​















    this.sectionRef = createRef();
  }



​








  render() {
    const { isShow } = this.state;
    return (
      <div>
        <button onClick={(e) => this.setState({ isShow: !isShow })}>
          切换
        </button>
​








        <CSSTransition
          nodeRef={this.sectionRef}
          in={isShow}
          unmountOnExit={true}
          timeout={2000}
          classNames="yuan"
          appear
          onEnter={(e) => console.log("开始进入动画")}
          onEntering={(e) => console.log("执行进入动画")}
          onEntered={(e) => console.log("执行进入结束")}
          onExit={(e) => console.log("开始离开动画")}
          onExiting={(e) => console.log("执行离开动画")}
          onExited={(e) => console.log("执行离开结束")}
        >
          <div className="section" ref={this.sectionRef}>
            <h2>哈哈哈</h2>
            <p>我是内容,哈啊哈哈</p>
          </div>
        </CSSTransition>
      </div>
    );
  }

}

​

export default App;
3.SwitchTransition
  • SwitchTransition可以完成两个组件之间切换的炫酷动画:

    • 比如我们有一个按钮需要在on和off之间切换,我们希望看到on先从左侧退出,off再从右侧进入;
    • 这个动画在vue中被称之为 vue transition modes;
    • react-transition-group中使用SwitchTransition来实现该动画;
  • SwitchTransition中主要有一个属性:mode,有两个值:

    • in-out:表示新组件先进入,旧组件再移除;
    • out-in:表示就组件先移除,新组建再进入;
  • 如何使用SwitchTransition呢?

    • SwitchTransition组件里面要有CSSTransition或者Transition组件,不能直接包裹你想要切换的组件;

    • SwitchTransition里面的CSSTransition或Transition组件不再像以前那样接受in属性来判断元素是何种状态,取而代之的是

      key属性;

.login-enter {
  opacity: 0;
  transform: translateX(100px);
}



​
















.login-enter-active {
  transform: translateX(0);

  opacity: 1;


  transition: all 1s ease;

}




​















.login-exit {
  transform: translateX(0);

  opacity: 1;

}


​










.login-exit-active {
  transform: translateX(-100px);
  opacity: 0;

  transform: all 1s ease;
}



import React, { PureComponent } from "react";











import { CSSTransition, SwitchTransition } from "react-transition-group";
import "./App.css";

​






















class App extends PureComponent {
  constructor() {






    super();






​















    this.state = {


      isLogin: true,
    };

  }




​








  render() {
    const { isLogin } = this.state;
    return (

      <div>

        <SwitchTransition mode="out-in">
          <CSSTransition
            key={isLogin ? "exit" : "login"}
            classNames="login"
            timeout={1000}
          >
            <button onClick={(e) => this.setState({ isLogin: !isLogin })}>
              {isLogin ? "退出" : "登录"}
            </button>
          </CSSTransition>
        </SwitchTransition>
      </div>

    );

  }

}

​




export default App;

4.CSSTransition

当我们有一组动画时,需要将这些CSSTransition放入到一个TransitionGroup中来完成动画:

.book-enter {
  transform: translateX(150px);
  opacity: 0;

}



​
















.book-enter-active {
  transform: translateX(0);

  opacity: 1;


  transition: all 1s ease;

}




​















.book-exit {
  transform: translateX(0);

  opacity: 1;

}


​










.book-exit-active {
  transform: translateX(150px);
  opacity: 0;

  transition: all 1s ease;
}



import React, { PureComponent } from "react";











import { TransitionGroup, CSSTransition } from "react-transition-group";
import "./App.css";

​






















export class App extends PureComponent {
  constructor() {






    super();






​















    this.state = {


      books: [
        { id: 111, name: "你不知道JS", price: 99 },
        { id: 222, name: "JS高级程序设计", price: 88 },
        { id: 333, name: "Vuejs高级设计", price: 77 },
      ],

    };

  }





​













  addNewBook() {
    const books = [...this.state.books];
    books.push({
      id: new Date().getTime(),
      name: "React高级程序设计",
      price: 99,
    });

    this.setState({ books });
  }

​





  removeBook(index) {
    const books = [...this.state.books];
    books.splice(index, 1);
    this.setState({ books });
  }


​




  render() {
    const { books } = this.state;
​

    return (

      <div>

        <h2>书籍列表:</h2>
        <TransitionGroup component="ul">
          {books.map((item, index) => {
            return (
              <CSSTransition key={item.id} classNames="book" timeout={1000}>
                <li>
                  <span>
                    {item.name}-{item.price}
                  </span>
                  <button onClick={(e) => this.removeBook(index)}>删除</button>
                </li>
              </CSSTransition>
            );
          })}
        </TransitionGroup>
        <button onClick={(e) => this.addNewBook()}>添加新书籍</button>
      </div>
    );
  }
}
​
export default App;

九、React中的CSS的编写方式

1.React中CSS的概述

前面说过,整个前端已经是组件化的天下,而CSS的设计就不是为组件化而生的,所以在目前组件化的框架中都在寻找一种合适的编写CSS的解决方案。

在组件化中选择合适的CSS解决方案应该符合以下条件:

可以编写局部css: 组件内部的css具备自己的局部作用域,不会随意污染其他组件内的元素;

可以编写动态的css: 可以获取当前组件的一些状态,根据状态的变化生成不同的css样式;

支持所有的css特性: 伪类、动画、媒体查询等;

编写起来简洁方便、最好符合一贯的css风格特点;

等等…

2.React中的CSS

事实上,css一直是React的痛点,也是被很多开发者吐槽、诟病的一个点。

在这一点上,Vue做的要好于React:

  • Vue通过在.vue文件中编写 <style> 标签来编写自己的样式;
  • 通过是否添加 scoped 属性来决定编写的样式是全局有效还是局部有效;
  • 通过 lang 属性来设置你喜欢的 less、sass等预处理器;
  • 通过内联样式风格的方式来根据最新状态设置和改变css;
  • 等等…

Vue在CSS上虽然不能称之为完美,但是已经足够简洁、自然、方便了,至少统一的样式风格不会出现多个开发人员、多个项目采用不一样的样式风格。

相比而言,React官方并没有给出在React中统一的样式风格:

  • 由此,从普通的css,到css modules,再到css in js,有几十种不同的解决方案,上百个不同的库;
  • 大家一致在寻找最好的或者说最适合自己的CSS方案,但是到目前为止也没有统一的方案;
3.内联样式的写法

(1) 内联样式是官方推荐的一种css样式的写法:

  • style 接受一个采用小驼峰命名属性的 JavaScript 对象,,而不是 CSS 字符串;
  • 并且可以引用state中的状态来设置相关的样式;

(2) 内联样式的优点:

  • 样式之间不会有冲突;
  • 可以动态获取当前state中的状态

(3) 内联样式的缺点:

  • 写法上都需要使用驼峰标识;
  • 某些样式没有提示;
  • 大量的样式, 代码混乱;
  • 某些样式无法编写(比如伪类/伪元素)

所以官方依然是希望内联合适和普通的css来结合编写。

4.普通的CSS

普通的css我们通常会编写到一个单独的文件,之后再进行引入。

这样的编写方式和普通的网页开发中编写方式是一致的:

  • 如果我们按照普通的网页标准去编写,那么也不会有太大的问题;
  • 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响;
  • 但是普通的css都属于全局的css,样式之间会相互影响;

这种编写方式最大的问题是样式之间会相互层叠掉;

4.css modules

css modules并不是React特有的解决方案,而是所有使用了类似于webpack配置的环境下都可以使用的。如果在其他项目中使用它,那么我们需要自己来进行配置,比如配置webpack.config.js中的modules: true等。

React的脚手架已经内置了css modules的配置:.css/.less/.scss 等样式文件都需要修改成 .module.css/.module.less/.module.scss 等;之后就可以引用并且进行使用了;

css modules确实解决了局部作用域的问题,也是很多人喜欢在React中使用的一种方案。

这种方案也有自己的缺陷:

  • 引用的类名,不能使用连接符(.home-title),在JavaScript中是不识别的;
  • 所有的className都必须使用{style.className} 的形式来编写;
  • 不方便动态来修改某些样式,依然需要使用内联样式的方式;

如果你觉得上面的缺陷还算OK,那么你在开发中完全可以选择使用css modules来编写,并且也是在React中很受欢迎的一种方式。

.title {
  font-size: 32px;
  color: green;
}



​
















.content {
  font-size: 22px;
  color: orange;
}
import React, { PureComponent } from 'react'
import Home from './home/Home'
import Profile from './profile/Profile'
​






















import appStyle from "./App.module.css"
​











export class App extends PureComponent {
  render() {

    return (


      <div>


        <h2 className={appStyle.title}>我是标题</h2>
        <p className={appStyle.content}>我是内容, 哈哈哈哈</p>
​








        <Home/>
        <Profile/>
      </div>
    )
  }




}




​









export default App
5.less
6.css in js

(1) 认识css in js:

“CSS-in-JS” 是指一种模式,其中 CSS 由 JavaScript 生成而不是在外部文件中定义;注意此功能并不是 React 的一部分,而是由第三方库提供;

事实上CSS-in-JS的模式就是一种将样式(CSS)也写入到JavaScript中的方式,并且可以方便的使用JavaScript的状态;

(2) 目前比较流行的CSS-in-JS的库有哪些呢?

styled-components、emotion、glamorous

目前可以说styled-components依然是社区最流行的CSS-in-JS库,所以我们以styled-components为主;

(3) 安装styled-components:npm i styled-components@5.3.5

(4) ES6模板字符串:

ES6中增加了模板字符串的语法,这个对于很多人来说都会使用。

但是模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)。

正常情况下,我们都是通过 函数名() 方式来进行调用的,其实函数还有另外一种调用方式,就是标签模板字符串。

function foo(...args) {
  console.log(args);
}


​






















const yuan = "yuan";
​











foo(1, 2, 3); //[ 1, 2, 3 ]
​















foo`hello world`; //[ [ 'hello world' ] ]
​









foo`hello ${yuan}`; // [ [ 'hello ', '' ], 'yuan' ]

在styled-component中,就是通过这种方式来解析模块字符串,最终生成我们想要的样式的

(5) styled-components的使用

import styled from "styled-components";
​









































// 1.基本使用
export const AppWrapper = styled.div`
  .footer {
    border: 1px red solid;
  }
`;
​















export const SectionWrapper = styled.div.attrs((props) => ({
  tColor: props.color || "blue",
}))`
  border: 1px yellow solid;
  .title {
    font-size: ${(props) => props.size}px;
    color: ${(props) => props.tColor};
  }
`;
import React, { PureComponent } from "react";











import { AppWrapper, SectionWrapper } from "./style";
​















class App extends PureComponent {
  constructor() {

    super();

    this.state = {

      size: 30,
      color: "red",
    };




  }






​












  render() {
    const { size, color } = this.state;
    return (

      <AppWrapper>
        <SectionWrapper size={size} color={color}>
          <h2 className="title">我是h2</h2>
          <p className="content">我是p</p>
        </SectionWrapper>
​








        <div className="footer">
          <p>免责声明</p>
          <p>版权声明</p>
        </div>

      </AppWrapper>
    );


  }




}

​




export default App;


7.React中添加class

使用classnames库

<h2 className={`aaa ${isbbb ? 'bbb': ''} ${isccc ? 'ccc': ''}`}>哈哈哈</h2>
<h2 className={classname}>呵呵呵</h2>
<h2 className={classNames("aaa", { bbb:isbbb, ccc:isccc })}>嘿嘿嘿</h2>
<h2 className={classNames(["aaa", { bbb: isbbb, ccc: isccc }])}>嘻嘻嘻</h2>

十、redux

1.为什么需要redux?

(1) JavaScript开发的应用程序,已经变得越来越复杂了:

  • JavaScript需要管理的状态越来越多,越来越复杂;

  • 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示

    加载动效,当前分页;

(2) 管理不断变化的state是非常困难的:

  • 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;
  • 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;

(3) React是在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理:

  • 无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享;
  • React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定;

(4) Redux就是一个帮助我们管理State的容器:Redux是JavaScript的状态容器,提供了可预测的状态管理;

(5) Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb)

2.redux的核心理念

(1) store:

  • Redux的核心理念非常简单;

  • 比如我们有一个朋友列表需要共享到多个页面进行管理:

    • 如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的;
    • 比如页面的某处通过friends.push的方式增加了一条数据;
    • 比如另一个页面通过friends[0].age = 25修改了一条数据
  • 整个应用程序错综复杂,当出现bug时,很难跟踪到底哪里发生的变化;

    因此我们可以定义一些数据, 将数据存放在store中供其他页面使用

(2) action:

  • Redux要求我们通过action来更新数据;

    所有数据的变化,必须通过派发(dispatch)action来更新;

    action是一个普通的JavaScript对象,用来描述这次更新的type和content;

    强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的;

    当然,目前我们的action是固定的对象;

    真实应用中,我们会通过函数来定义,返回一个action;

(3) reducer:

  • 如何将state和action联系在一起呢? 答案就是通过reducer

    reducer是一个纯函数;

    reducer做的事情就是将传入的state和action结合起来生成一个新的state;

3.redux的三大原则

(1) 单一数据源:

整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中:

Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护;

单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改;

(2) state是只读的:

唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State:

这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state;

这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题;

(3) 使用纯函数来执行修改:

通过reducer将旧state和actions联系在一起,并且返回一个新的State:

随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分;

但是所有的reducer都应该是纯函数,不能产生任何的副作用;

5.redux的基本使用过程

(1) redux测试项目的搭建:

  • 创建一个新的项目文件夹:learn-redux

    • 执行初始化操作 npm init -y 或 yarn init -y
    • 安装redux: npm i redux 或 yarn add redux
  • 创建src目录,在src目录下创建一个store文件夹,并且在该文件夹下创建index.js文件

  • 修改package.json用于执行index.js,也可以不配置,直接使用node命令运行

(2) redux的基本使用步骤:

  • 创建一个对象,作为我们要保存的状态state

  • 创建一个store来存储这个state:

    • 创建store时必须创建reducer;
    • 我们可以通过store.getState()来访问state;
  • 通过action来修改state:

    • 通过dispatch来派发action;
    • 通常action中都会有type属性,
  • 修改reducer中的处理代码,reducer是一个纯函数

  • 可以在派发action之前,监听store的变化

// index.js
const { createStore } = require("redux");

​















// 初始化数据
const initialState = {
  name: "yuan",
  age: 18,
};
​















// 定义reducer函数:纯函数
// 两个参数:
// 参数一:store中目前保存的state
// 参数二:本次需要更新的action
// 返回值:它的返回值会作为之后存储的state
function reducer(state = initialState, action) {
  switch (action.type) {
    case "change_name":
      return { ...state, name: action.name };
    case "change_age":
      return { ...state, age: action.age };
    default:
      return state;
  }
}
​





// 创建store
const store = createStore(reducer);
module.exports = store;
​










// test.js
const store = require("./store/index copy");
​







// 使用state中的数据
console.log(store.getState());
​


// 修改state中的数据
const nameAction = { type: "change_name", name: "xiao" };
store.dispatch(nameAction);
​


const ageAction = { type: "change_age", age: 23 };
store.dispatch(ageAction);
​
console.log(store.getState());

(3) redux结构的划分

  • index.js

    const { createStore } = require("redux");
    const reducer = require("./reducer");
    ​
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    const store = createStore(reducer);
    ​
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    module.exports = store;
    
  • constants.js

    const CHANGE_NAME = "change_name";
    const CHANGE_AGE = "change_age";
    ​
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    module.exports = {
      CHANGE_NAME,
      CHANGE_AGE,
    };
    
  • reducer.js

    const { CHANGE_NAME, CHANGE_AGE } = require("./constants");
    ​
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    const initialState = {
    
      name: "yuan",
      age: 18,
    };
    ​
    
    
    
    
    
    
    
    
    
    
    
    function reducer(state = initialState, action) {
      switch (action.type) {
        case CHANGE_NAME:
          return { ...state, name: action.name };
        case CHANGE_AGE:
          return { ...state, age: action.age };
        default:
          return state;
      }
    
    
    
    
    
    }
    
    ​
    
    
    
    
    
    
    
    
    
    
    module.exports = reducer;
    
  • actionCreator.js

    const { CHANGE_NAME, CHANGE_AGE } = require("./constants");
    ​
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    const changeNameAction = (name) => ({
      type: CHANGE_NAME,
      name,
    });
    ​
    
    
    
    
    
    
    
    
    
    
    
    const changeAgeAction = (age) => ({
      type: CHANGE_AGE,
      age,
    });
    ​
    
    
    
    
    
    
    
    
    
    
    
    
    module.exports = {
      changeNameAction,
      changeAgeAction,
    };
    
6.在React中直接使用redux

开始之前需要强调一下,redux和react没有直接的关系,你完全可以在React, Angular, Ember, jQuery, or vanilla JavaScript中使用Redux。

尽管这样说,redux依然是和React库结合的更好,因为他们是通过state函数来描述界面的状态,Redux可以发射状态的更新, 让他们作出相应; 目前redux在react中使用是最多的,所以我们需要将之前编写的redux代码,融入到react当中去。

这里我创建了两个组件:

Home组件:其中会展示当前的counter值,并且有一个+1和+5的按钮;

Profile组件:其中会展示当前的counter值,并且有一个-1和-5的按钮;

(1) 安装redux,创建store

store/index.js

import reducer from "./reducer";

const { createStore } = require("redux");

​















const store = createStore(reducer);
​
















export default store;

store/constants.js

export const ADD_NUM = "add_num";
export const SUB_NUM = "sub_num";

store/reducer.js

import { ADD_NUM, SUB_NUM } from "./constants";
​









































const initialState = {

  counter: 100,
};
​











function reducer(state = initialState, action) {
  switch (action.type) {
    case ADD_NUM:
      return { ...state, counter: state.counter + action.num };
    case SUB_NUM:
      return { ...state, counter: state.counter - action.num };
    default:
      return state;
  }



}


​













export default reducer;

store/actionCreator.js

import { ADD_NUM, SUB_NUM } from "./constants";
​









































export const addNumAction = (num) => ({
  type: ADD_NUM,
  num,
});
​











export const subNumAction = (num) => ({
  type: SUB_NUM,
  num,
});

(2) Home组件:

import React, { PureComponent } from "react";











import store from "../store";

import { addNumAction } from "../store/actionCreator";

​






















class Home extends PureComponent {
  constructor() {






    super();






    this.state = {


      counter: store.getState().counter,

    };




  }






​












  // 核心一:在componentDidMount中监听store的变化,当数据发生变化时重新设置counter

  componentDidMount() {

    store.subscribe(() => {

      const state = store.getState();

      this.setState({ counter: state.counter });

    });

  }


​









  // 核心二:当发生点击事件时,调用store的dispatch来派发对应的action

  addNum(num) {
    store.dispatch(addNumAction(num));
  }

​





  render() {

    const { counter } = this.state;

    return (

      <>

        <h2>home</h2>
        <h2>{counter}</h2>

        <button onClick={(e) => this.addNum(5)}>+5</button>
      </>

    );



  }



}



​




export default Home;

(3) Profile组件

import React, { PureComponent } from "react";











import store from "../store";

import { subNumAction } from "../store/actionCreator";
​






















class Profile extends PureComponent {
  constructor() {






    super();






    this.state = {


      counter: store.getState().counter,

    };




  }






​












  // 核心一:在componentDidMount中监听store的变化,当数据发生变化时重新设置counter

  componentDidMount() {

    store.subscribe(() => {

      const state = store.getState();

      this.setState({ counter: state.counter });

    });

  }


​









  // 核心二:当发生点击事件时,调用store的dispatch来派发对应的action

  subNum(num) {
    store.dispatch(subNumAction(num));
  }

​





  render() {

    const { counter } = this.state;

    return (

      <>

        <h2>Profile</h2>
        <h2>{counter}</h2>

        <button onClick={(e) => this.subNum(5)}>-5</button>
      </>

    );



  }



}



​




export default Profile;

我们发现Home组件和Profile组件中的代码是大同小异的, 所以这不是我们最终编写的代码, 后面还会对代码进行优化。

7.通过react-redux库连接React和Redux

(1) 安装react-redux库:npm i react-redux

(2) 在index.js中为App根组件提供store:

import React from "react";




import ReactDOM from "react-dom/client";
import App from "./16_React-redux/App";
// 引入Provider和store
import { Provider } from "react-redux";
import store from "./16_React-redux/store";
​











const root = ReactDOM.createRoot(document.getElementById("root"));
​















root.render(
  // 在Provider中声明要提供的store
  <Provider store={store}>
    <App />
  </Provider>
);

(3) 在组件中使用react-redux提供的connect高阶函数将store与组件连接起来,connect接受两个参数,第一个参数的含义是将state映射到props,第二个参数的含义是将dispatch映射到props上。

import React, { PureComponent } from "react";











import { connect } from "react-redux";


import { addNumAction } from "../store/actionCreator";

​






















class About extends PureComponent {

  addNum(num) {
    this.props.changeNum(num);
  }







​















  render() {


    const { counter } = this.props;
    return (


      <>


        <h2>about</h2>
        <h2>{counter}</h2>
        <button onClick={(e) => this.addNum(6)}>+6</button>
      </>
    );
  }


}
​








const mapStateToProps = (state) => ({
  counter: state.counter,
});

​





const mapDispatchToProps = (dispatch) => ({
  changeNum(num) {
    dispatch(addNumAction(num));
  },
});
​





export default connect(mapStateToProps, mapDispatchToProps)(About);
8.redux中进行异步操作(网络请求)的方案

(1) 在之前简单的案例中,redux中保存的counter是一个本地定义的数据。

我们可以直接通过同步的操作来dispatch action,state就会被立即更新。

但是真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux中。

发生网络请求我们有两种方案, 可以直接在组件的钩子函数中发送网络请求, 再将数据存放到store中; 也可以直接在store中发生网络请求

(2) 组件中进行异步操作

网络请求可以在class组件的生命周期函数componentDidMount中发送。

(3) 安装redux-thunk库引入中间件:npm i redux-thunk

import { createStore, applyMiddleware } from "redux";
import reducer from "./reducer";
// 导入中间件
import thunk from "redux-thunk";
​
















// 应用中间件
const store = createStore(reducer, applyMiddleware(thunk))
​















export default store

应用之后,store.dispatch()就可以派发函数了

(4) 定义一个返回函数的action

import { CHANGE_BANNERS, CHANGE_RECOMMENDS } from "./constants";
import axios from "axios";


​















export const changeBannersAction = (banners) => ({
  type: CHANGE_BANNERS,
  banners,
});
​















export const changeRecommendsAction = (recommends) => ({
  type: CHANGE_RECOMMENDS,
  recommends,
});
​








export const fetchHomeDataAction = () => {
  return (dispatch, getState) => {
    axios.get("http://123.207.32.32:8000/home/multidata").then((res) => {
      console.log(res);
      const banners = res.data.data.banner.list;
      const recommends = res.data.data.recommend.list;
      dispatch(changeBannersAction(banners));
      dispatch(changeRecommendsAction(recommends));
    });
  };
};

自动执行action中的返回的函数时, 会传给这个函数一个dispatch函数和getState函数;

dispatch函数: 用于我们之后再次派发action;

getState函数: 考虑到我们之后的一些操作需要依赖原来的状态,调用getState函数可以让我们可以获取之前的一些状态;

我们就可以在返回的该函数中, 编写异步的网络请求相关代码

(5) 派发action

import React, { PureComponent } from "react";











import axios from "axios";


import { connect } from "react-redux";
import {
  changeBannersAction,
  changeRecommendsAction,
  fetchHomeDataAction,
} from "../store/actionCreator";
​















class Category extends PureComponent {
  constructor() {
    super();
    this.state = {};
  }


​







  //   componentDidMount() {
  //     axios.get("http://123.207.32.32:8000/home/multidata").then((res) => {
  //       //   console.log(res);
  //       const banners = res.data.banner.list;
  //       const recommends = res.data.recommend.list;
​








  //       this.props.changeBanners(banners);
  //       this.props.changeRecommends(recommends);
  //     });
  //   }
  componentDidMount() {
    this.props.fetchHomeData();
  }




​










  render() {
    return (
      <>
        <h2>category</h2>
      </>
    );

  }


}

​




const mapDispatchToprops = (dispatch) => ({
  //   changeBanners(banners) {
  //     dispatch(changeBannersAction(banners));
  //   },
  //   changeRecommends(recommends) {
  //     dispatch(changeRecommendsAction(recommends));
  //   },
  fetchHomeData() {
    dispatch(fetchHomeDataAction());
  },
});
​

export default connect(null, mapDispatchToprops)(Category);

(6) 数据的展示

import React, { PureComponent } from "react";











import { connect } from "react-redux";


​















class Show extends PureComponent {
  constructor(props) {

    super(props);

    this.state = {};

  }







​















  render() {


    const { banners, recommends } = this.props;
    return (


      <>


        <h2>show</h2>
        <div>
          {banners.map((item) => {
            return <h2 key={item.title}>{item.title}</h2>;
          })}
        </div>
​









        <div>
          {recommends.map((item) => {
            return <h2 key={item.title}>{item.title}</h2>;
          })}
        </div>

      </>
    );


  }




}

​




const mapStateToProps = (state) => ({
  banners: state.banners,
  recommends: state.recommends,
});
​


export default connect(mapStateToProps)(Show);
9.redux-devtools

利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等。

(1) 在对应的浏览器中安装相关的插件(比如chrome中在扩展商店搜索Redux DevTools);

(2) 在redux中继承devtools的中间件

import reducer from "./reducer";

import thunk from "redux-thunk";
import { createStore, applyMiddleware, compose } from "redux";
​






















const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true }) || compose;
const enhancer = composeEnhancer(applyMiddleware(thunk));
const store = createStore(reducer, enhancer);
​















export default store;
10.reducer的拆分

将不同模块需要共享的数据拆分到不同的reducer里

import { createStore, compose, combineReducers } from "redux"
import { log, thunk, applyMiddleware } from "./middleware"
// import thunk from "redux-thunk"
​






















import counterReducer from "./counter"
import homeReducer from "./home"
import userReducer from "./user"
​















// 正常情况下 store.dispatch(object)
// 想要派发函数 store.dispatch(function)
​















// 将两个reducer合并在一起
const reducer = combineReducers({
  counter: counterReducer,
  home: homeReducer,
  user: userReducer
})
​










// combineReducers实现原理(了解)
// function reducer(state = {}, action) {
//   // 返回一个对象, store的state
//   return {
//     counter: counterReducer(state.counter, action),
//     home: homeReducer(state.home, action),
//     user: userReducer(state.user, action)
//   }
// }
​


// redux-devtools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
const store = createStore(reducer)
​







applyMiddleware(store, log, thunk)
​



export default store

store目录:

image-20230607093143541

十一、redux-toolkit

1.redux toolkit的介绍

Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。

在前面我们学习Redux的时候应该已经发现,redux的编写逻辑过于的繁琐和麻烦。

并且代码通常分拆在多个文件中(虽然也可以放到一个文件管理,但是代码量过多,不利于管理);

Redux Toolkit包旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题;

在很多地方为了称呼方便,也将之称为“RTK”;

2.安装Redux Toolkit:

npm install @reduxjs/toolkit react-redux

3.Redux Toolkit的核心API

configureStore: 包装createStore以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供 的任何 Redux 中间件,redux-thunk默认包含,并启用 Redux DevTools Extension。

createSlice: 接受reducer函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有相应的actions。

createAsyncThunk: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个pending/fulfilled/rejected基于该承诺分派动作类型的 thunk

4.Redux Toolkit基本使用

(1) 我们先对counter的reducer进行重构:通过createSlice创建一个slice

createSlice主要包含如下几个参数:

name:用户标记slice的名词,在之后的redux-devtools中会显示对应的名词;

initialState:初始化值,第一次初始化的值;

reducers:相当于之前的reducer函数:对象类型,可以添加很多的函数;函数类似于原来reducer中的case语句;函数的参数:第一个参数state,第二个参数action,action有两个属性,一个是自动生成的type,还有一个是传递的参数放在payload中

store/features/counter.js

import { createSlice } from "@reduxjs/toolkit";
​









































const counterSlice = createSlice({
  name: "counter",
  initialState: {
    counter: 888,
  },
  reducers: {
    addNumber(state, { payload }) {
      state.counter = state.counter + payload;
    },
    subNumber(state, { payload }) {
      state.counter = state.counter - payload;
    },
  },
});

​













export const { addNumber, subNumber } = counterSlice.actions;
export default counterSlice.reducer;

(2) store的创建

configureStore用于创建store对象,常见参数如下:

reducer:将slice中的reducer组成一个对象传入此处;

middware:可以使用参数,传入其他的中间件;

devTools:是否配置devTools工具,默认为true

store/index.js

import { configureStore } from "@reduxjs/toolkit";
​









































import counterReducer from "./features/counter";
​






















const store = configureStore({
  reducer: {
    counter: counterReducer,
  },


  devTools: false,
});





​















export default store;

(3) index.js

import App from "./17_react-toolkit/App";
import { Provider } from "react-redux";
import store from "./17_react-toolkit/store";
​






















const root = ReactDOM.createRoot(document.getElementById("root"));
​











root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

(4) About.jsx

import React, { PureComponent } from "react";











import { connect } from "react-redux";


import { addNumber, subNumber } from "../store/features/counter";
​






















class About extends PureComponent {

  constructor() {






    super();






    this.state = {};


  }






​









  render() {

    return (


      <>


        <h2>About</h2>
        <h2>About counter: {this.props.counter}</h2>
        <button
          onClick={(e) => {
            this.props.addNumber(1);
          }}
        >
          +1
        </button>
        <button
          onClick={(e) => {
            this.props.subNumber(1);
          }}
        >
          -1
        </button>
      </>
    );
  }


}
​



const mapStateToProps = (state) => ({
  counter: state.counter.counter,
});
​




const mapDispatchToProps = (dispatch) => ({
  addNumber(num) {
    dispatch(addNumber(num));
  },
  subNumber(num) {
    dispatch(subNumber(num));
  },
});
​

export default connect(mapStateToProps, mapDispatchToProps)(About);
5.Redux Toolkit异步操作

在之前的开发中,我们通过redux-thunk中间件让dispatch中可以进行异步操作, 其实Redux Toolkit工具包默认已经给我们集成了Thunk相关的功能, 我们可以通过createAsyncThunk函数创建一个异步的action。

createAsyncThunk函数有参数:

参数一: 传入事件类型type

参数二: 传入一个函数, 该函数可以执行异步操作, 甚至可以直接传入一个异步函数

(1) 方式一:

在home.js中, 通过createAsyncThunk函数创建一个异步的action

再在extraReducers中监听这个异步的action的状态, 当他处于fulfilled状态时, 获取到网络请求的数据, 并修改原来state中的数据

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";


​















export const fetchHomeDataAction = createAsyncThunk("fetch/home", async () => {
  // 1.发送网络请求
  const res = await axios.get("http://123.207.32.32:8000/home/multidata");
​











  // 返回结果,那么action状态会变成fulfilled状态
  return res.data;
});





​















const homeSlice = createSlice({
  name: "home",
  initialState: {
    banners: [],
    recommends: [],
  },

  reducers: {
    changeBanners(state, { payload }) {
      state.banners = payload;
    },
    changeRecommends(state, { payload }) {
      state.recommends = payload;
    },
  },
  // extraReducers中针对异步action, 监听它的状态
  //   extraReducers: {
  //     [fetchHomeDataAction.pending](state, action) {
  //       console.log("pending");
  //     },
  //     [fetchHomeDataAction.fulfilled](state, { payload }) {
  //       state.banners = payload.data.banner.list;
  //     },
  //     [fetchHomeDataAction.rejected](state, action) {
  //       console.log("reject");
  //     },
  //   },
  extraReducers: (builder) => {
    builder
      .addCase(fetchHomeDataAction.pending, (state, action) => {
        console.log("pending");
      })
      .addCase(fetchHomeDataAction.fulfilled, (state, { payload }) => {
        state.banners = payload.data.banner.list;
      });
  },
});
​


export const { changeBanners, changeRecommends } = homeSlice.actions;
export default homeSlice.reducer;
componentDidMount() {

    this.props.fetchHomeData();
}


​






















const mapDispatchToProps = (dispatch) => ({

  fetchHomeData() {

    dispatch(fetchHomeDataAction());
  },


});

方式二:

如果我们不想通过在extraReducers在监听状态, 再修改state这种方法的话, 还有另外的一种做法

我们创建的fetchHomeMultidataAction这个异步action是接受两个参数的

  • 参数一, extraInfo: 在派发这个异步action时, 如果有传递参数, 会放在extraInfo里面

  • 参数二, store: 第二个参数将store传递过来

    这样我们获取到结果后, 通过dispatch修改store中的state, 无需再监听异步action的状态

export const fetchHomeDataAction = createAsyncThunk(
  "fetch/home",
  async (extraInfo, { dispatch, getState }) => {
    console.log(extraInfo, dispatch, getState);
​
















    // 1.发送网络请求
    const res = await axios.get("http://123.207.32.32:8000/home/multidata");
​















    // 2.取出数据, 并且在此处直接dispatch操作(可以不做)
    const banners = res.data.data.banner.list;
    const recommends = res.data.data.recommend.list;
    dispatch(changeBanners(banners));
    dispatch(changeRecommends(recommends));
​








    // 返回结果,那么action状态会变成fulfilled状态
    // return res.data;
  }





);
componentDidMount() {

    this.props.fetchHomeData();
}


​






















const mapDispatchToProps = (dispatch) => ({

  fetchHomeData() {

    dispatch(fetchHomeDataAction({ name: "yuan", age: 19 }));
  },


});

不管是哪种方式, 都需要在页面的componentDidMount生命周期中, 通过派发异步的action发送网络请求

十二、react-router

1.认识react-router

目前前端流行的三大框架, 都有自己的路由实现:

Angular的ngRouter

React的ReactRouter

Vue的vue-router

React Router在最近两年版本更新的较快,并且在最新的React Router6.x版本中发生了较大的变化。

目前React Router6.x已经非常稳定,我们可以放心的使用;

说明一下, Router4.x和Router5.x的区别是不大的, 而Router6.x就有些区别, 所以Router系列的文章主要针对Router6.x进行讲解, 当有与4或5版本不同的地方时会单独强调

安装React Router:

安装时,我们选择安装react-router-dom, npm install react-router-dom;

因为react-router会包含一些react-native的内容,web开发并不需要;

2.Router的组件API

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

(1) BrowserRouter或HashRouter:

Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件;

BrowserRouter使用history模式;

import React from "react"

import ReactDOM from "react-dom/client"

import { BrowserRouter } from "react-router-dom"
import App from "./App"

​
















const root = ReactDOM.createRoot(document.querySelector("#root"))

​











root.render(

  <BrowserRouter>
    <App/>

  </BrowserRouter>
)

HashRouter使用hash模式

import React from "react"

import ReactDOM from "react-dom/client"

import { HashRouter } from "react-router-dom"
import App from "./App"

​
















const root = ReactDOM.createRoot(document.querySelector("#root"))

​











root.render(

  <HashRouter>
    <App/>

  </HashRouter>
)

(2) Router的映射配置:

Routes:包裹所有的Route,在其中匹配一个路由

Router6.x使用的是Routes组件

Router5.x使用的是Switch组件

Route:Route用于路径的匹配

Router6.x不允许Router组件单独存在

path属性: 用于设置匹配到的路径;

element属性: 设置匹配到路径后,渲染的组件

  • Router5.x使用的是component属性

  • exact: 精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;

    • Router6.x不再支持该属性
<div className='app'>

  <div className='header'>header</div>
  

  <div className='counter'>
    <Routes>
      <Route path='/' element={<Home/>}/>
      <Route path='/about' element={<About/>}/>
      <Route path='/profile' element={<Profile/>}/>
    </Routes>
  </div>
  
  <div className='footer'>footer</div>
</div>

(3) Router配置和跳转

Link组件:

通常路径的跳转是使用Link组件,这个组件最终会被渲染成a元素;

NavLink是在Link基础之上增加了一些样式属性(后续会讲解);

to属性: Link中最重要的属性,用于设置跳转到的路径

<div className='app'>

  <div className='header'>
    <Link to="/">首页</Link>
    <Link to="/about">关于</Link>
    <Link to="/profile">我的</Link>
  </div>
​











  <div className='counter'>
    <Routes>
      <Route path='/' element={<Home/>}/>
      <Route path='/about' element={<About/>}/>
      <Route path='/profile' element={<Profile/>}/>
    </Routes>
  </div>
​







  <div className='footer'>footer</div>
</div>

NavLink组件:

需求:路径选中时,对应的a元素的文字变为红色

这个时候,我们要使用NavLink组件来替代Link组件, NavLink组件选中时, 有添加一个类(这个组件了解即可, 不如自己控制更方便):

事实上在默认匹配成功时,NavLink就会动态的添加上一个class: active , 选中的时候就会添加;

所以我们也可以直接编写样式

当然,如果你担心这个class在其他地方被使用了,出现样式的层叠,也可以自定义class和动态添加样式:

style属性: 传入一个函数,函数的参数接收一个对象,该对象包含isActive属性

<NavLink to="/" style={({ isActive }) => ({color: isActive ? "red" : ""})}>首页</NavLink>
<NavLink to="/about" style={({ isActive }) => ({color: isActive ? "red" : ""})}>关于</NavLink>
<NavLink to="/profile" style={({ isActive }) => ({color: isActive ? "red" : ""})}>我的</NavLink>

className:传入一个函数,函数的参数接受一个对象,该对象包含isActive属性

<NavLink to="/" className={({ isActive }) => isActive ? "my-class" : ""}>首页</NavLink>
<NavLink to="/about" className={({ isActive }) => isActive ? "my-class" : ""}>关于</NavLink>
<NavLink to="/profile" className={({ isActive }) => isActive ? "my-class" : ""}>我的</NavLink>

Navigate组件使用:

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

<Routes>


  {/* 当默认路径 / 时, 重定向到home页面 */}

  <Route path='/' element={<Navigate to="/home"/>}></Route>

  <Route path='/home' element={<Home/>}/>

  <Route path='/about' element={<About/>}/>

  <Route path='/profile' element={<Profile/>}/>

</Routes>

Not Found页面配置:

开发一个Not Found页面;

配置对应的Route,并且设置path为*即可;

<Routes>


  {/* 当默认路径 / 时, 重定向到home页面 */}

  <Route path='/' element={<Navigate to="/home"/>}></Route>

  <Route path='/home' element={<Home/>}/>

  <Route path='/about' element={<About/>}/>

  <Route path='/profile' element={<Profile/>}/>

  {/* 当上面路径都没有匹配到时, 显式Notfound组件 */}
  <Route path='*' element={<Notfound/>}/>
</Routes>
3.路由的嵌套
<Routes>


  <Route path='/' element={<Navigate to="/home"/>}></Route>
  

  {/* 配置二级路由 */}
  <Route path='/home' element={<Home/>}>
    <Route path='/home' element={<Navigate to="/home/recommend"/>}/>
    <Route path='/home/recommend' element={<HomeRecommend/>}/>
    <Route path='/home/ranking' element={<HomeRanking/>}/>
  </Route>
  
  <Route path='/about' element={<About/>}/>
  <Route path='/profile' element={<Profile/>}/>
  <Route path='*' element={<Notfound/>}/>
</Routes>

组件用于在父路由元素中作为子路由的占位元素, 也就是子路由的展示位置(必须写)。

// home组件
​









































import { Link, Outlet } from 'react-router-dom'
​






















export class Home extends PureComponent {
  render() {

    return (
      <div>
        <h2>Home</h2>
        <Link to="/home/recommend">推荐</Link>
        <Link to="/home/ranking">排行</Link>
        <Outlet/>
      </div>
    )
  }



}


4.手动实现路由的跳转
// 修改为函数组件, 类组件无法使用hook
export function App() {
  // 使用hook
  const navigate = useNavigate()
​
















  function navigateTo(path) {
    navigate(path)
  }







​















  return (


    <div className='app'>
      <div className='header'>
        <Link to="/home">首页</Link>
        <Link to="/about">关于</Link>
        <Link to="/profile">我的</Link>
​










        {/* 点击时将路径传入到navigate中 */}
        <button onClick={() => navigateTo("/category")}>分类</button>
        <span onClick={() => navigateTo("/order")}>订单</span>
      </div>
​








      <div className='counter'>
        <Routes>
          {/* 当默认路径 / 时, 重定向到home页面 */}
          <Route path='/' element={<Navigate to="/home"/>}></Route>
          {/* 配置二级路由 */}
          <Route path='/home' element={<Home/>}>
            <Route path='/home' element={<Navigate to="/home/recommend"/>}/>
            <Route path='/home/recommend' element={<HomeRecommend/>}/>
            <Route path='/home/ranking' element={<HomeRanking/>}/>
          </Route>
          <Route path='/about' element={<About/>}/>
          <Route path='/profile' element={<Profile/>}/>
          <Route path='/category' element={<Category/>}/>
          <Route path='/order' element={<Order/>}/>
          {/* 当上面路径都没有匹配到时, 显式Notfound组件 */}
          <Route path='*' element={<Notfound/>}/>
        </Routes>
      </div>
​



      <div className='footer'>footer</div>
    </div>
  )
}

那么如果是一个函数式组件,我们可以直接调用它提供的hooks的写法,但是如果是一个类组件呢?

Router6.x确实是没有提供类组件的API, 如果我们确实想要在类组件中使用, 需要再使用高阶组件对类组件进行增强(通过高阶组件增强向类组件中传入navigate);

如果是Router5.x, 是有提供withRouter这样一个高阶组件的, 但是Router6.x中, 我们需要自己实现这样的高阶组件

封装高阶函数方法如下, 由于其他地方也可能使用高阶组件, 所以我是在一个单独的文件中进行封装

import { useState } from "react";
import {
  useLocation,
  useNavigate,
  useParams,
  useSearchParams,
} from "react-router-dom";
​















function withRouter(Cpn) {
  return function (props) {
    // 1.导航
    const navigate = useNavigate();
​








    // 2.动态路由传参数:/detail/:id
    const params = useParams();
​










    // 查询字符串参数:/user?name=yuan&age=18
    const location = useLocation();
    const [searchParams] = useSearchParams();
    const query = Object.fromEntries(searchParams);
​








    const router = { navigate, params, location, query };
​







    return <Cpn {...props} router={router}></Cpn>;
  };
}

​





export default withRouter;

这样我们引入自己封装的高阶组件, 通过高阶组件的增强, 就可以在类组件的props中获取到navigate

export class App extends PureComponent {
  navigateTo(path) {
    // 经过高阶组件增强的组件中, 可以在props中拿到navigate
    const { naviagte } = this.props.router
    // 调用navigate
    naviagte(path)
  }




​















  render() {
    return (

      <div className='app'>
        <div className='header'>
          <Link to="/home">首页</Link>
          <Link to="/about">关于</Link>
          <Link to="/profile">我的</Link>
​










          {/* 发生点击事件时, 将路劲传递过去 */}
          <button onClick={() => this.navigateTo("/category")}>分类</button>
          <span onClick={() => this.navigateTo("/order")}>订单</span>
        </div>
​








        <div className='counter'>
          <Routes>
            {/* 当默认路径 / 时, 重定向到home页面 */}
            <Route path='/' element={<Navigate to="/home"/>}></Route>
            {/* 配置二级路由 */}
            <Route path='/home' element={<Home/>}>
              <Route path='/home' element={<Navigate to="/home/recommend"/>}/>
              <Route path='/home/recommend' element={<HomeRecommend/>}/>
              <Route path='home/ranking' element={<HomeRanking/>}/>
            </Route>
            <Route path='/about' element={<About/>}/>
            <Route path='/profile' element={<Profile/>}/>
            <Route path='/category' element={<Category/>}/>
            <Route path='/order' element={<Order/>}/>
            {/* 当上面路径都没有匹配到时, 显式Notfound组件 */}
            <Route path='*' element={<Notfound/>}/>
          </Routes>
        </div>
​



        <div className='footer'>footer</div>
      </div>
    )
  }

}

​

// 使用高阶组件对App组件进行增强
export default withRouter(App)
5.路由的配置文件

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

但是这样的方式会让路由变得非常混乱,我们希望像vue-router那样, 将所有的路由配置放到一个单独的文件进行集中管理:

在早期的时候,Router并且没有提供相关的API,我们需要借助于react-router-config完成;

在Router6.x中,为我们提供了useRoutes API可以完成相关的配置;

例如我们将下面的映射关系配置到一个单独的文件中:

<div className='counter'>
  <Routes>
    {/* 当默认路径 / 时, 重定向到home页面 */}
    <Route path='/' element={<Navigate to="/home"/>}></Route>
    {/* 配置二级路由 */}
    <Route path='/home' element={<Home/>}>
      <Route path='/home' element={<Navigate to="/home/recommend"/>}/>
      <Route path='/home/recommend' element={<HomeRecommend/>}/>
      <Route path='home/ranking' element={<HomeRanking/>}/>
    </Route>
    <Route path='/about' element={<About/>}/>
    <Route path='/profile' element={<Profile/>}/>
    <Route path='/category' element={<Category/>}/>
    <Route path='/order' element={<Order/>}/>
    <Route path='/detail/:id' element={<Detail/>}/>
    <Route path='/user' element={<User/>} />
    {/* 当上面路径都没有匹配到时, 显式Notfound组件 */}
    <Route path='*' element={<Notfound/>}/>
  </Routes>
</div>

首先, 使用useRoutes这个API替代原来的Routes和Route组件, useRoutes可以当成一个函数直接使用, 但是只能在函数组件中使用

<div className='counter'>
  {useRoutes(routes)}
</div>

再在route/index.js中对映射关系进行配置

import { Navigate } from "react-router-dom"
import Home from '../pages/Home'
import About from '../pages/About'
import Profile from '../pages/Profile'
import Notfound from '../pages/Notfound'
import HomeRecommend from '../pages/HomeRecommend'
import HomeRanking from '../pages/HomeRanking'
import Category from '../pages/Category'
import Order from '../pages/Order'
import Detail from '../pages/Detail'
import User from '../pages/User'
​












const routes = [
  {
    path: "/",
    element: <Navigate to="/home"/>
  },

  {
    path: "/home",
    element: <Home/>,
    children: [
      {
        path: "/home",
        element: <Navigate to="/home/recommend" />
      },
      {
        path: "/home/recommend",
        element: <HomeRecommend/>
      },
      {
        path: "/home/ranking",
        element: <HomeRanking/>
      }
    ]
  },
  {
    path: "/about",
    element: <About/>
  },
  {
    path: "/profile",
    element: <Profile/>
  },
  {
    path: "/category",
    element: <Category/>
  },
  {
    path: "/order",
    element: <Order/>
  },
  {
    path: "detail/:id",
    element: <Detail/>
  },
  {
    path: "/user",
    element: <User/>
  },
  {
    path: "*",
    element: <Notfound/>
  }

]
​
export default routes

如果我们对某些组件进行了异步加载(懒加载, 分包处理),那么需要使用Suspense进行包裹:

例如我们对Detail和User进行懒加载(分包处理)

// import Detail from '../pages/Detail'
// import User from '../pages/User'
​















const Detail = React.lazy(() => import("../pages/Detail"))
const User = React.lazy(() => import("../pages/User"))

并且还需要使用Suspense对组件进行包裹

root.render(
  <HashRouter>
    <Suspense fallback={<h3>loading</h3>}>
      <App/>
    </Suspense>
  </HashRouter>
)

十三、react hooks

1.为什么需要hook

Hook 是 React 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期);

我们先来思考一下class组件相对于函数式组件有什么优势?比较常见的是下面的优势:

  • class组件可以定义自己的state,用来保存组件自己内部的状态;函数式组件不可以,因为函数每次调用都会产生新的临时变量;
  • class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑; 函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
  • class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等;函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;
  • 所以,在Hook出现之前,对于上面这些情况我们通常都会编写class组件。
2.class组件存在的问题

(1) 复杂组件变得难以理解:

  • 我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,我们的class组件会变得越来越复杂;
  • 比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在componentWillUnmount中移除);
  • 而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;

(2) 难以理解的class:

  • 很多人发现学习ES6的class是学习React的一个障碍。
  • 比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this;
  • 虽然我认为前端开发人员必须掌握this,但是依然处理起来非常麻烦;

(3) 组件复用状态很难:

  • 在前面为了一些状态的复用我们需要通过高阶组件;
  • 像我们之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用;
  • 或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套;
  • 这些代码让我们不管是编写和设计上来说,都变得非常困难;

Hook的出现就可以解决上面的问题

3.简单总结一下Hooks

(1) 它可以让我们在不编写class的情况下使用state以及其他的React特性;

(2) 我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;

(3) Hook的使用场景:

  • Hook的出现基本可以代替我们之前所有使用class组件的地方;
  • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
  • Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用;
4.useState
import React, { memo, useState } from "react";


​









































const App = memo((props) => {


  const [counter, setCounter] = useState(100);

​
















  const addNum = (num) => {
    setCounter(counter + num);
  };
​















  return (


    <>
      <h2>counter:{counter}</h2>
      <button onClick={() => addNum(1)}>+1</button>
    </>
  );
});

​













export default App;

useState来自react,需要从react中导入,是一个hook函数, 官方中也将它成为State Hook, 它与class组件里面的 this.state 提供的功能完全相同;

一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。

useState只有一个参数: 接收一个初始化状态的值(设置初始值),在第一次组件被调用时使用来作为初始化值(如果不设置则默认为undefined);

useState的返回值: 返回一个数组,数组包含两个元素:

  • 元素一: 当前状态的值(第一次调用为初始化值);
  • 元素二: 是一个设置状态值变化的函数;
  • 不过我们如果总是使用索引来获取这两个元素总是不方便的, 因此在开发中我们通常是会对数组进行解构(当然要取什么名字是自定义的);
  • 例如上面代码: const [counter, setCounter] = useState(100)

hook的使用规则:

只能在函数组件的顶层调用 Hook。不能在循环语句、条件判断语句或者子函数中调用。

只能在 React 的函数组件自定义hook中调用 Hook。不能在其他 JavaScript 函数中调用。

5.useEffect

我们知道在类组件中是可以有生命周期函数的, 那么如何在函数组件中定义类似于生命周期这些函数呢?

Effect Hook 可以让你来完成一些类似于class中生命周期的功能;

事实上,类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects);

所以对于完成这些功能的Hook被称之为 Effect Hook;

(1) useEffect的基本使用:

假如我们现在有一个需求:页面中的title总是显示counter的数字,分别使用class组件和Hook实现:

class的实现

import React, { PureComponent } from "react";











​









































class App extends PureComponent {



  constructor() {




    super();




    this.state = {




      counter: 100,
    };


  }






​









  componentDidMount() {
    document.title = this.state.counter;
  }



​








  componentDidUpdate() {
    document.title = this.state.counter;
  }





​










  render() {


    const { counter } = this.state;

    return (



      <>
        <div>counter: {counter}</div>
        <button onClick={(e) => this.setState({ counter: counter + 1 })}>
          +1
        </button>
      </>
    );


  }



}


​





export default App;


hook的实现

import React, { memo, useState, useEffect } from "react";
​









































const App = memo((props) => {


  const [counter, setCounter] = useState(100);

​
















  useEffect(() => {
    document.title = counter;
  });
​















  const addNum = (num) => {
    setCounter(counter + num);
  };

  return (
    <>
      <h2>counter: {counter}</h2>
      <button onClick={() => addNum(1)}>+1</button>
    </>
  );
});
​









export default App;

(2) useEffect的解析

  • 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;
  • useEffect要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数;
  • 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数;

(3) 清除副作用(Effect):

在class组件的编写过程中,某些副作用的代码,我们需要在componentWillUnmount中进行清除:

比如我们之前的事件总线或Redux中手动调用subscribe;

都需要在componentWillUnmount有对应的取消订阅;

Effect Hook通过什么方式来模拟componentWillUnmount呢?

useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B:

type EffectCallback = () => (void | (() => void | undefined));

为什么要在 effect 中返回一个函数?

这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数;

如此可以将添加和移除订阅的逻辑放在一起;

它们都属于 effect 的一部分;

React 何时清除 effect?

React 会在组件更新和卸载的时候执行清除操作, 将上一次的监听取消掉, 只留下当前的监听 ;

正如之前学到的,effect 在每次渲染的时候都会执行;

import React, { memo, useEffect } from 'react'

​









































const App = memo(() => {




  useEffect(() => {
    // 监听store数据发生改变
    const unsubscribe = store.subscribe(() => {
​











    })
​















    // 返回值是一个回调函数, 该回调函数在组件重新渲染或者要卸载时执行
    return () => {
      // 取消监听操作
      unsubscribe()
    }

  })
​










  return (

    <div>

      <h2>App</h2>
    </div>

  )

})

​







export default App

(4) 使用多个effect

使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题:

比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中;

一个函数组件中可以使用多个Effect Hook,我们可以将逻辑分离到不同的useEffect中:

import React, { memo, useEffect } from 'react'

​









































const App = memo(() => {




  // 监听的useEffect
  useEffect(() => {
    console.log("监听的代码逻辑")
​











    return () => {
      console.log("取消的监听代码逻辑")
    }
  })
​












  // 发送网络请求的useEffect
  useEffect(() => {
    console.log("网络请求的代码逻辑")
  })
​













  // 操作DOM的useEffect
  useEffect(() => {
    console.log("操作DOM的代码逻辑")
  })
​








  return (
    <div>
      App
    </div>

  )

})

​










export default App

Hook允许我们按照代码的用途分离它们, 而不是像生命周期函数那样, 将很多逻辑放在一起:

React将按照 effect 声明的顺序依次调用组件中的每一个 effect;

(5) useEffect性能优化

默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:

某些代码我们只是希望执行一次即可(比如网络请求, 组件第一次渲染中执行一次即可, 不需要执行多次),类似于类组件中的componentDidMount和componentWillUnmount中完成的事情;

另外,多次执行也会导致一定的性能问题;

我们如何决定useEffect在什么时候应该执行和什么时候不应该执行呢?

useEffect实际上有两个参数:

  • 参数一: 执行的回调函数, 这个参数我们已经使用过了不再多说;
  • 参数二: 是一个数组类型, 表示 该useEffect在哪些state发生变化时,才重新执行;(受谁的影响才会重新执行)
import React, { memo, useEffect, useState } from 'react'

​









































const App = memo(() => {




  const [counter, setCounter] = useState(100)

  


  // 发送网络请求的useEffect, 只有在counter发生改变时才会重新执行
  useEffect(() => {

    console.log("网络请求的代码逻辑")
  }, [counter])
​









  return (

    <div>
      <h2 onClick={() => setCounter(counter+1)}>{counter}</h2>
    </div>

  )

})

​













export default App

但是,如果一个函数我们不希望依赖任何的内容时,也可以传入一个空的数组 []:

那么这里的两个回调函数分别对应的就是componentDidMount和componentWillUnmount生命周期函数了;

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

​









































const App = memo(() => {




  const [counter, setCounter] = useState(100)

  


  // 传入空数组表示不受任何数据依赖
  useEffect(() => {

    // 此时传入的参数一这个回调函数: 相当于componentDidMount
    console.log("监听的代码逻辑")
​









    // 参数一这个回调函数的返回值: 相当于componentWillUnmount
    return () => {
      console.log("取消的监听代码逻辑")
    }

  }, [])
​










  return (

    <div>

      <h2 onClick={() => setCounter(counter+1)}>{counter}</h2>
    </div>

  )

})

​







export default App

6.useCallback和useMemo性能优化

(1) useCallback的使用:

useCallback实际的目的是为了性能优化

useCallback进行什么样的优化呢?

例如下面这个计数器的案例, 我们点击按钮时, counter数据会发生变化, App函数组件就会重新渲染, 意味着increment函数就会被重新定义一次, 每点击一次按钮, increment函数就会重新被定义; 虽然每次定义increment函数, 垃圾回收机制会将上一次定义的increment函数回收, 但是这种不必要的重复定义是会影响性能的

import React, { memo, useState } from 'react'
​









































const App = memo(() => {




  const [counter, setCounter] = useState(10)
  


  function increment() {
    setCounter(counter + 1)
  }







​















  return (


    <div>
      <h2>{counter}</h2>
      <button onClick={() => increment()}>+1</button>
    </div>

  )

})

​













export default App

如何进行性能的优化呢?

调用useCallback会返回一个 memoized(有记忆的) 的回调函数;

在依赖不变的情况下,多次定义的时候,返回的回调函数是相同的;

参数一: 传入一个回调函数, 如果依赖发生改变会定义一个新的该回调函数使用, 如果依赖没有发生改变, 依然使用原来的回调函数

参数二: 用于控制依赖的, 第二个参数要求传入一个数组, 数组中可以传入依赖, 传空数组表示没有依赖

useCallback拿到的结果是函数

const memoizedCallback = useCallback(
  () => {

   doSomething(a, b)
  },
  [a, b]

)

useCallback的作用:

通常使用useCallback的目的是在向子组件传递函数时, 将要传递的函数进行优化在传递给子组件, 避免子组件进行多次渲染;

并不是为了函数不再重新定义, 也不是对函数定义做优化

定义一个子组件Test, 并将increment函数传递到子组件中, 我们在子组件中可以拿到increment方法修改App组件中的counter;

由于counter发生改变, 就会重新定义一个新的increment函数, 因此我们只要修改了counter, 就会传递一个新的increment函数到Test组件中; Test组件中的props就会发生变化, Test组件会被重新渲染

如果此时App组件中再定义一个方法changeMessage用来修改message;

我们会发现当message发生改变时, 子组件Test也会被重新渲染; 这是因为message发生改变, App组件会重新渲染, 那么就会重新定义一个新的increment函数, 将新的increment函数传递到Test组件, Test组件的props发生改变就会重新渲染

import React, { memo, useState } from "react";


​









































const Test = memo((props) => {


  console.log("test被重新渲染");


  return (


    <>


      <button onClick={props.increment}>add</button>


    </>


  );



});





​















const App = memo((props) => {



  const [counter, setCounter] = useState(0);


  const [message, setMessage] = useState("hello react");


​







  const addNum = (num) => {
    setCounter(counter + num);
  };
  return (


    <>


      <div>app</div>

      <h2>counter: {counter}</h2>

      <h2>message: {message}</h2>

      <button onClick={(e) => addNum(1)}>+1</button>
      <button onClick={(e) => setMessage("hello callback")}>修改文本</button>

      <Test increment={(e) => addNum(1)}></Test>
    </>

  );

});

​




export default App;


但是如果我们使用useCallback, 就可以避免App组件中message发生改变时, Test组件重新渲染

因为message组件发生改变, 但是我们下面的useCallback函数是依赖counter的, 在依赖没有发生改变时, 多次定义返回的值是相同的(也就是修改message重新渲染App组件时, increment并没有重新定义, 依然是之前的); 就意味着Test组件中的props没有改变, 因此Test组件不会被重新渲染

如果是counter值发生改变, 因为useCallback函数是依赖counter的, 所以会定义一个新的函数给increment; 当向Test组件传递新的increment时, Test组件的props就会改变, Test依然会重新渲染, 这也是我们想要实现的效果

import React, { memo, useState, useCallback } from "react";
​









































const Test = memo((props) => {


  console.log("test被重新渲染");


  return (


    <>


      <button onClick={props.increment}>add</button>


    </>


  );



});





​















const App = memo((props) => {



  const [counter, setCounter] = useState(0);


  const [message, setMessage] = useState("hello react");


​







  const addNum = useCallback(() => {
    setCounter(counter + 1);
  }, [counter]);
  return (


    <>


      <div>app</div>

      <h2>counter: {counter}</h2>

      <h2>message: {message}</h2>

      <button onClick={addNum}>+1</button>
      <button onClick={(e) => setMessage("hello callback")}>修改文本</button>

      <Test increment={addNum}></Test>
    </>

  );

});

​




export default App;


还可以再进一步的进行优化:

现在我们的代码是counter发生变化时, useCallback会重新定义一个新的函数返回给increment; 但是我们想做到, counter发生变化, 依然使用原来的函数, 不需要重新定义一个新的函数;

可能会有小伙伴想, 直接将依赖改为一个空数组, 但是如果是这样的话就会产生闭包陷阱; 我们修改counter时确实不会重新生成一个新的函数, 但是原来的函数中使用的counter永远是之前的值, 也就是0; 这是因为我们旧的函数在定义的那一刻, counter的值是0; 由于修改counter依然使用旧的函数, 这样无论我们修改多少次counter, 页面展示的数据永远是 0 + 1 的结果

const increment = useCallback(() => {
  setCounter(counter + 1)
}, [])

这个时候我们就需要结合使用另一个hook: useRef

useRef函数在组件多次进行渲染时, 返回的是同一个值; 我们就可以将最新的counter储存到useRef返回的对象的current属性中; 这样做的好处就是, counter发生改变时, 也不会重新定义一个函数, 意味着修改counter也不会导致Test组件重新渲染

import React, { memo, useState, useCallback, useRef } from "react";
​









































const Test = memo((props) => {


  console.log("test被重新渲染");


  return (


    <>


      <button onClick={props.increment}>add</button>


    </>


  );



});





​















const App = memo((props) => {



  const [counter, setCounter] = useState(0);


  const [message, setMessage] = useState("hello react");


  const counterRef = useRef();
  counterRef.current = counter;
​













  const addNum = useCallback(() => {
    setCounter(counterRef.current + 1);
  }, []);
  return (


    <>
      <div>app</div>
      <h2>counter: {counter}</h2>
      <h2>message: {message}</h2>
      <button onClick={addNum}>+1</button>
      <button onClick={(e) => setMessage("hello callback")}>修改文本</button>
      <Test increment={addNum}></Test>
    </>
  );
});
​







export default App;

(2) useMemo的解析

useMemo实际的目的也是为了进行性能的优化, 例如下面这个例子

我们定义一个计算累加的函数calcNumTotal, 在App组件中调用这个函数计算结果

但是counter改变时, App组件就会重新渲染, 那么calcNumTotal函数又会重新计算; 但是counter的改变和calcNumTotal函数并没有关系, 却要重新渲染; 这种类似的场景我们就可以使用useMemo进行性能优化

import React, { memo, useState } from "react";


​









































function calcNumTotal(num) {

  let total = 0;
  for (let i = 0; i <= num; i++) {
    total += num;
  }




​















  return total;
}




​















const App = memo((props) => {



  const [counter, setCounter] = useState(10);
  return (
    <>
      <div>app</div>
      <h2>counter: {counter}</h2>
      <h2>{calcNumTotal(100)}</h2>
      <button onClick={(e) => setCounter(counter + 1)}>+1</button>
    </>
  );
});
​







export default App;

如何使用 useMemo进行性能的优化呢?

useMemo返回的也是一个 memoized(有记忆的) 值; 在依赖不变的情况下,多次定义的时候,返回的值是相同的;

参数一: 传入一个回调函数

参数二: 传入一个数组, 表示依赖, 什么都不依赖传入空数组; 如果不传则该函数什么都不会做, 无意义

const memoizedValue = useMemo(
  () => {

    computeExpensiveValue(a, b)
  }, 
  [a, b]

)

这样我们就可以对上面的代码进行优化了, 实现counter发生变化, 而calcNumTotal函数不需要重新计算结果:

import React, { memo, useState, useMemo } from "react";
​









































function calcNumTotal(num) {

  console.log("函数被调用");
  let total = 0;
  for (let i = 0; i <= num; i++) {
    total += num;
  }







​















  return total;
}


​












const App = memo((props) => {

  const [counter, setCounter] = useState(10);
  const result = useMemo(() => {
    return calcNumTotal(100);
  }, []);
  return (
    <>
      <div>app</div>
      <h2>counter: {counter}</h2>
      <h2>{result}</h2>
      <button onClick={(e) => setCounter(counter + 1)}>+1</button>
    </>
  );

});
​





export default App;

(3) useMemo与useCallback的区别:

useMemo拿到的传入回调函数的返回值, useCallback拿到的传入的回调函数本身;

简单来说useMemo是对函数的返回值做优化, useCallback是对函数做优化;

7.useRef和useImperativeHandle

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

最常用的ref两种用法:

用法一: 用来获取DOM(或者组件,但是需要是class组件)元素, 操作DOM;

import React, { memo, useRef } from "react";
​









































const App = memo((props) => {


  const ref1 = useRef();
  const inputRef = useRef();
​











  function getDom() {
    console.log(ref1.current);
    inputRef.current.focus();
  }

  return (

    <>
      <div ref={ref1}>hello world</div>
      <input type="text" ref={inputRef} />
      <button onClick={getDom}>获取dom</button>
    </>
  );
});
​



export default App;

用法二: 保存一个数据,这个对象在整个生命周期中可以保存不变;

import React, { memo, useRef } from 'react'
import { useCallback } from 'react'
import { useState } from 'react'
​






















let obj = null
​











const App = memo(() => {
  const [count, setCount] = useState(0)
  const nameRef = useRef()
  console.log(obj === nameRef)
  obj = nameRef
​












  // 通过useRef解决闭包陷阱
  const countRef = useRef()
  countRef.current = count
​










  const increment = useCallback(() => {
    setCount(countRef.current + 1)
  }, [])
​









  return (


    <div>

      <h2>Hello World: {count}</h2>
      <button onClick={e => setCount(count+1)}>+1</button>
      <button onClick={increment}>+1</button>
    </div>

  )

})

​










export default App

(2) useImperativeHandle:

通过forwardRef可以将ref转发到子组件;

子组件拿到父组件中创建的ref,绑定到自己的某一个元素中;

父组件中就可以获取到子组件中绑定了ref的元素

import React, { memo, forwardRef, useRef } from "react";
​









































const HelloWorld = memo(

  forwardRef((props, ref) => {

    return (

      <>
        <input type="text" ref={ref} />
      </>
    );
  })
);

​












const App = memo((props) => {

  const inputRef = useRef();
  function getDom() {
    inputRef.current.focus();
    console.log(inputRef.current);
  }




  return (


    <>


      <HelloWorld ref={inputRef}></HelloWorld>
      <button onClick={getDom}>获取子组件焦点</button>
    </>
  );
});
​

export default App;

forwardRef的做法本身没有什么问题,但是我们是将子组件的DOM直接暴露给了父组件:

直接暴露给父组件带来的问题是某些情况的不可控;

父组件可以拿到DOM后进行任意的操作;

但是,事实上在上面的案例中,我们只是希望父组件可以操作的focus,其他并不希望它随意操作

例如修改元素内容的操作inputRef.current.value = “aaa”等等, 我们希望可以限制它的操作;

通过useImperativeHandle可以值暴露固定的操作(对操作进行限制):

useImperativeHandle要求传入两个参数:

  • 参数一: 传入一个ref
  • 参数二: 传入一个回调函数, 要求该回调函数返回一个对象, 该对象会绑定到传入的ref的current属性中

通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起; 所以在父组件中,使用 inputRef.current时,实际上使用的是参数二返回的对象;

import React, { memo, forwardRef, useRef, useImperativeHandle } from "react";
​









































const HelloWorld = memo(

  forwardRef((props, ref) => {

    const inputRef = useRef();
​











    useImperativeHandle(ref, () => {
      return {
        focus() {
          inputRef.current.focus();
        },
      };
    });
​








    return (

      <>
        <input type="text" ref={inputRef} />
      </>
    );

  })
);
​








const App = memo((props) => {
  const inputRef = useRef();
  function getDom() {
    inputRef.current.focus();
    console.log(inputRef.current);
  }




  return (
    <>
      <HelloWorld ref={inputRef}></HelloWorld>
      <button onClick={getDom}>获取子组件焦点</button>
    </>
  );
});
​

export default App;
8.useContext和useReducer

(1)useContext的使用:

新建context/index.js

import { createContext } from "react";
​









































const UserContext = createContext();
const ThemeContext = createContext();
​
















export { UserContext, ThemeContext };

在index.js注入context

root.render(
  <UserContext.Provider value={{ name: "yuan", age: 18 }}>
    <ThemeContext.Provider value={{ color: "red", size: "30px" }}>
      <App></App>
    </ThemeContext.Provider>
  </UserContext.Provider>
);

新建App.jsx

import React, { memo, useContext } from "react";
import { UserContext, ThemeContext } from "./context";
​















const App = memo((props) => {
  const userInfo = useContext(UserContext);
  const themeInfo = useContext(ThemeContext);
  return (
    <>
      <div>
        {userInfo.name} - {userInfo.age}
      </div>
      <div>
        {themeInfo.color} - {themeInfo.size}
      </div>
    </>
  );
});
​










export default App;

(2) useReducer

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

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

在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分;

或者这次修改的state需要依赖之前的state时,也可以使用;

import React, { memo, useReducer } from "react";
​









































function reducer(state, action) {
  switch (action.type) {
    case "add_num":
      return { ...state, counter: state.counter + action.num };
    case "sub_num":
      return { ...state, counter: state.counter - action.num };
    default:
      return state;
  }






}
​








const App = memo((props) => {
  const [state, dispatch] = useReducer(reducer, { counter: 0 });
  return (
    <>
      <div>app</div>
      <h2>counter: {state.counter}</h2>
      <button onClick={() => dispatch({ type: "add_num", num: 1 })}>+1</button>
      <button onClick={() => dispatch({ type: "sub_num", num: 1 })}>-1</button>
    </>
  );
});

​





export default App;

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

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

9.useLayoutEffect

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

  • useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
  • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;
10.自定义hook
import { useState, useEffect } from "react"
​









































function useScrollPosition() {
  const [ scrollX, setScrollX ] = useState(0)
  const [ scrollY, setScrollY ] = useState(0)
​











  useEffect(() => {
    function handleScroll() {
      // console.log(window.scrollX, window.scrollY)
      setScrollX(window.scrollX)
      setScrollY(window.scrollY)
    }
​








    window.addEventListener("scroll", handleScroll)
    return () => {
      window.removeEventListener("scroll", handleScroll)
    }
  }, [])
​



  return [scrollX, scrollY]
}



​








export default useScrollPosition
11.redux hooks
// 1.使用useSelector将redux中store的数据映射到组件内
const { count } = useSelector((state) => ({
    count: state.counter.count
}))
​
















// 2.使用dispatch直接派发action
const dispatch = useDispatch()
function addNumberHandle(num, isAdd = true) {
    if (isAdd) {
      dispatch(addNumberAction(num))
    } else {
      dispatch(subNumberAction(num))
    }
}


© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYG26mp7' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片