“React学习之旅:从入门到精通的点滴感悟“

发布时间:2023年12月25日

在探索和学习React的过程中,我逐渐领悟到了前端开发的魅力与挑战。React,作为Facebook推出的开源JavaScript库,以其独特的思维方式和强大的功能,引领着前端开发的潮流。在这篇文章中,我将分享我的React学习心得,总结笔记,以期帮助那些同样在前端领域摸索的开发者们。

1.Hello React 案例

<div id="root"></div>;

// 类组件和函数式组件
class App extends React.Component {
  // 组件数据
  constructor() {
    super();
    this.state = {
      message: "Hello World",
    };
    // 对需要绑定的方法, 提前绑定好this 或者方法写成箭头函数上下文指向React组件
    this.btnClick = this.btnClick.bind(this);
  }

  // 组件方法(实例方法)
  btnClick() {
    // 1.将state中message值修改掉 2.自动重新执行render函数函数
    this.setState({
      message: "Hello React",
    });
  }

  // 渲染内容 render方法
  render() {
    return (
      <div>
        <h2>{this.state.message}</h2>
        <button onClick={this.btnClick}>修改文本</button>
      </div>
    );
  }
}

// 将组件渲染到界面上
const root = ReactDOM.createRoot(document.querySelector("#root"));
// App根组件
root.render(<App />);

如果上下文中没有定义this,那么this默认会指向全局对象,在浏览器中这个全局对象通常是window。在 React 的类组件中,果没有显示绑定方法的this,那么调用方法时this指向会失去组件实例的上下文,进而变成undefined,从而导致运行时错误。

为了避免这种问题,可以使用.bind()或箭头函数显式绑定方法的this,或者在构造函数中使用箭头函数定义方法,从而确保方法的this指向是组件实例。

2. 列表渲染

....
    // 封装App组件
    class App extends React.Component {
      constructor() {
        super()

        this.state = {
          data: ["Vue", "React", "Angular"]
        }
      }


      render() {
        // 1.对data进行for循环
        // const liEls = []
        // for (let i = 0; i < this.state.data.length; i++) {
        //   const item = this.state.movies[i]
        //   const liEl = <li>{item}</li>
        //   liEls.push(liEl)
        // }

        // 2.data数组 => liEls数组
        // const liEls = this.state.data.map(movie => <li>{movie}</li>)

        return (
          <div>
            <h2>电影列表</h2>
            <ul>
              {this.state.movies.map(movie => <li>{movie}</li>)}
            </ul>
          </div>
        )
      }
    }
......

3.JSX 语法规则

3.1 插入内容

  • jsx 中的注释
  • JSX 嵌入变量作为子元素
    • 情况一:当变量是 Number、String、Array 类型时,可以直接显示
    • 情况二:当变量是 null、undefined、Boolean 类型时,内容为空
      • 如果希望可以显示 null、undefined、Boolean,那么需要转成字符串;
      • 转换的方式有很多,比如 toString 方法、和空字符串拼接,String(变量)等方式;
    • 情况三:Object 对象类型不能作为子元素(not valid as a React child)
  • JSX 嵌入表达式
  • 运算表达式
  • 三元运算符
  • 行一个函数**
// jsx语法规则
class App extends React.Component {
  constructor() {
    super();
    this.state = {
      message: "Hello World",
      count: 1000,
      names: ["紫陌", "张三", "王五"],

      aaa: undefined,
      bbb: null,
      ccc: true,

      friend: { name: "zimo" },

      firstName: "zimo",
      lastName: "紫陌",

      age: 20,

      list: ["aaa", "bbb", "ccc"],
    };
  }

  render() {
    // 解构拿到值
    const { message, names, count, aaa, bbb, ccc, friend, firstName, lastName, age } = this.state;

    // 对内容进行运算后显示(插入表示)
    const ageText = age >= 18 ? "成年人" : "未成年人";
    const liEls = this.state.list.map((item) => <li>{item}</li>);

    return (
      <div>
        {/* 1.Number / String / Array直接显示出来*/}
        <h2>{message}</h2>
        <h2>{count}</h2>
        <h2>{names}</h2>
        {/* 2.undefined/null/Boolean  默认不展示,三种方法都可以显示出来*/}
        <h2>{String(aaa)}</h2>
        <h2>{bbb + ""}</h2>
        <h2>{ccc.toString()}</h2>
        {/* 3.Object类型不能作为子元素展示 */}
        <h2>{friend.name}</h2> {/* zimo */}
        <h2>{Object.keys(friend)[0]}</h2> {/* name */}
        {/* 4.可以插入对应表达式 */}
        <h2>{10 + 20}</h2>
        <h2>{firstName + "" + lastName}</h2>
        {/* 5.插入三元表达式 */}
        <h2>{ageText}</h2>
        <h2>{age >= 18 ? "成年人" : "未成年人"}</h2>
        {/* 6.可以调用方法获取结果 */}
        <ul>{liEls}</ul>
        <ul>
          {this.state.list.map((item) => (
            <li>{item}</li>
          ))}
        </ul>
        <ul>{this.getList()}</ul>
      </div>
    );
  }

  getList() {
    const liEls = this.state.list.map((item) => <li>{item}</li>);
    return liEls;
  }
}

3.2 绑定属性

// 1.定义App根组件
class App extends React.Component {
  constructor() {
    super();
    this.state = {
      title: "紫陌",
      imgURL: "https:*****",
      href: "https://*****",

      isActive: true,
      objStyle: { color: "red", fontSize: "30px" },
    };
  }

  render() {
    const { title, imgURL, href, isActive, objStyle } = this.state;

    // 需求: isActive: true -> active
    // 1.class绑定的写法一: 字符串的拼接
    const className = `abc cba ${isActive ? "active" : ""}`;
    // 2.class绑定的写法二: 将所有的class放到数组中
    const classList = ["abc", "cba"];
    if (isActive) classList.push("active");
    // 3.class绑定的写法三: 第三方库classnames -> npm install classnames

    return (
      <div>
        {/* 1.基本属性绑定 */}
        <h2 title={title}>我是h2元素</h2>
        <img src={imgURL} alt="" />
        <a href={href}>百度一下</a>

        {/* 2.绑定class属性: 最好使用className */}
        <h2 className={className}>哈哈哈哈</h2>
        <h2 className={classList.join(" ")}>哈哈哈哈</h2>

        {/* 3.绑定style属性: 绑定对象类型 */}
        <h2 style={{ color: "red", fontSize: "30px" }}>紫陌YYDS</h2>
        <h2 style={objStyle}>紫陌</h2>
      </div>
    );
  }
}

jsx 语法规则:

? 1.定义虚拟 DOM 时,不要写引号。

? 2.标签中混入 JS 表达式时要用{}。

? 3.样式的类名指定不要用 class,要用 className。

?

? 5.只有一个根标签

? 6.标签必须闭合

? 7.标签首字母

? (1).若小写字母开头,则将该标签转为 html 中同名元素,若 html 中无该标签对应的同名元素,则报错。

? (2).若大写字母开头,react 就去渲染对应的组件,若组件没有定义,则报错。

3.3 事件绑定

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写;
  • 我们需要通过{}传入一个事件处理函数,这个函数会在事件发生时被执行;
class App extends React.Component {
  constructor() {
    super();
    this.state = {
      count: 10,
    };
    this.btn1Click = this.btn1Click.bind(this);
  }

  btn1Click() {
    console.log("btn1", this);
    this.setState({ count: this.state.count + 1 });
  }

  btn2Click = () => {
    console.log("btn2", this);
    this.setState({ count: this.state.count + 1 });
  };

  btn2Click = () => {
    console.log("btn2", this);
    this.setState({ count: this.state.count + 1 });
  };

  btn3Click() {
    console.log("btn2", this);
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    const { count } = this.state;

    return (
      <div>
        <h2>当前计数:{count}</h2>

        {/* 1.this绑定方式一:bind绑定 */}
        <button onClick={this.btn1Click}>按钮1</button>

        {/* 2.this.绑定方式二:ES6 class fields */}
        <button onClick={this.btn2Click}>按钮2</button>

        {/* 3.this绑定方式三:直接传入一个箭头函数(重要) */}
        <button onClick={() => console.log("btn3Click")}>按钮3</button>

        <button onClick={() => this.btn3Click()}>按钮4</button>
      </div>
    );
  }
}

3.4 事件参数传递

  • 在执行事件函数时,有可能我们需要获取一些参数信息:比如 event 对象、其他参数
  • 情况一:获取 event 对象
    • 很多时候我们需要拿到 event 对象来做一些事情(比如阻止默认行为)
    • 那么默认情况下,event 对象有被直接传入,函数就可以获取到 event 对象;
  • 情况二:获取更多参数
    • 有更多参数时,我们最好的方式就是传入一个箭头函数,主动执行的事件函数,并且传入相关的其他参数;
class App extends React.Component {
  constructor() {
    super();
    this.state = {
      message: "Hello World",
    };
  }

  btnClick(event, name, age) {
    console.log("btnClick", event, this);
    console.log("name,age", name, age);
  }

  render() {
    return (
      <div>
        {/* 1.event参数传递 */}
        <button onClick={this.btnClick.bind(this)}>按钮1</button>
        <button
          onClick={(event) => {
            this.btnClick(event);
          }}
        >
          按钮2
        </button>

        {/* 额外参数传递 */}
        <button onClick={this.btnClick.bind(this, "zimo", 18)}>按钮3(不推荐)</button>

        <button onClick={(event) => this.btnClick(event, "zimo", 18)}>按钮4</button>
      </div>
    );
  }
}

3.5 条件渲染

// 1.定义App根组件
class App extends React.Component {
  constructor() {
    super();
    this.state = {
      isShow: false,
      flag: undefined,
    };
  }

  render() {
    const { isShow, flag } = this.state;

    // 1.条件判断方式一: 使用if进行条件判断
    let showElement = null;
    if (isShow) {
      showElement = <h2>显示紫陌</h2>;
    } else {
      showElement = <h1>隐藏紫陌</h1>;
    }

    return (
      <div>
        {/* 1.方式一: 根据条件给变量赋值不同的内容 */}
        <div>{showElement}</div>

        {/* 2.方式二: 三元运算符 */}
        <div>{isShow ? <h2>显示</h2> : <h2>隐藏</h2>}</div>

        {/* 3.方式三: &&逻辑与运算 */}
        {/* 场景: 当某一个值, 有可能为undefined时, 使用&&进行条件判断 */}
        <div> {flag && <h2>{flag}</h2>} </div>
      </div>
    );
  }
}

3.6 条件列表渲染

  • 在 React 中,展示列表最多的方式就是使用数组的map 高阶函数
  • 很多时候我们在展示一个数组中的数据之前,需要先对它进行一些处理 比如过滤掉一些内容:filter 函数
  • 比如截取数组中的一部分内容:slice 函数
class App extends React.Component {
  constructor() {
    super();
    this.state = {
      students: [
        { id: 111, name: "zimo", score: 199 },
        { id: 112, name: "aniy", score: 98 },
        { id: 113, name: "james", score: 199 },
        { id: 114, name: "curry", score: 188 },
      ],
    };
  }

  render() {
    const { students } = this.state;

    /* 第一种分别写 */

    //分数大于100学生进行展示
    const filterStudent = students.filter((item) => {
      return item.score > 100;
    });

    //分数大于100前两名信息
    const sliceStudents = filterStudent.slice(0, 2);

    return (
      <div>
        <h2>列表数据</h2>
        <div>
          {/* 第二种方式链式调用 */}
          {students
            .filter((item) => item.score > 100)
            .slice(0, 2)
            .map((item) => {
              return (
                <div key={item.id}>
                  <h2>学号: {item.id}</h2>
                  <h3>姓名: {item.name}</h3>
                  <h4>分数:{item.score}</h4>
                </div>
              );
            })}
        </div>
      </div>
    );
  }
}

3.7 JSX 的本质

  • JSX 仅仅只是 React.createElement(component, props, …children) 函数的语法糖。 所有的 jsx 最终都会被转换成 React.createElement 的函数调用。

  • createElement 需要传递三个参数:

  • 参数一:type

    • 当前 ReactElement 的类型;
    • 如果是标签元素,那么就使用字符串表示 “div”;
    • 如果是组件元素,那么就直接使用组件的名称;
  • 参数二:config
    ./images

  • 所有 jsx 中的属性都在 config 中以对象的属性和值的形式存储

  • 比如传入 className 作为元素的 class;

  • 参数三:children

    • 存放在标签中的内容,以 children 数组的方式进行存储;
  1. 源码分析

在这里插入图片描述

  1. babel 转换
  • JSX 是通过 babel 帮我们进行语法转换的,写的 jsx 代码都需要依赖 babel。
  • 可以在 babel 的官网中快速查看转换的过程:https://babeljs.io/repl/#?presets=react

在这里插入图片描述

babel 转换过来的代码可以直接运行。界面依然是可以正常的渲染

3.组件化开发

3.1 render 函数的返回值

  • render 被调用时,它会检查 this.props this.state 的变化并返回以下类型之一:
  • React 元素
    • 通常通过 JSX 创建。
    • 例如,
      会被 React 渲染为 DOM 节点, <MyComponent /> 会被 React 渲染为自定义组件;
    • 无论是
      还是 <MyComponent /> 均为 React 元素。
  • 数组或 fragments:使得 render 方法可以返回多个元素。
  • Portals:可以渲染子节点到不同的 DOM 子树中。
  • 字符串或数值类型:它们在 DOM 中会被渲染为文本节点
  • 布尔类型或 null:什么都不渲染。

3.2 函数组件

  • 函数组件是使用 function 来进行定义的函数,只是这个函数会返回和类组件中 render 函数返回一样的内容。
  • 函数组件有自己的特点(hooks 就不一样了):
    • 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
    • this 关键字不能指向组件实例(因为没有组件实例);
    • 没有内部状态(state);
// 函数式组件
function App(props) {
  // 返回值: 和类组件中render函数返回的是一致
  return <h1>Hello zimo</h1>;
}

export default App;

4.React 生命周期

4.1 常用生命周期函数图

在这里插入图片描述

  1. 全部生命周期函数

在这里插入图片描述

4.2 生命周期函数例子

父组件:APP 组件

import React, { Component } from "react";
import Hello from "./Hello";

class App extends Component {
  constructor() {
    super();
    this.state = {
      isShow: true,
    };
  }
  isShowFun() {
    this.setState({
      isShow: !this.state.isShow,
    });
  }
  render() {
    const { isShow } = this.state;
    return (
      <div>
        我是父组件
        {isShow && <Hello />}
        <button onClick={() => this.isShowFun()}>显示隐藏子组件</button>
      </div>
    );
  }
}

export default App;

子组件:Hello

import React, { Component } from "react";

class Hello extends Component {
  constructor() {
    // 1.构造方法: constructor
    console.log("1. Hello constructor");
    super();

    this.state = {
      message: "你好紫陌",
    };
  }

  updateText() {
    this.setState({
      message: "你好啊,zimo~",
    });
  }

  // 2.执行render函数
  render() {
    console.log("2. Hello render");
    const { message } = this.state;
    return (
      <div>
        <h2>{message}</h2>
        <p>{message}这是子组件</p>
        <button onClick={() => this.updateText()}>修改文本</button>
      </div>
    );
  }

  // 3.组件被渲染到DOM:被挂载到DOM
  componentDidMount() {
    console.log("Hello组件挂载完成");
  }

  // 4. 组件的DOM被更新完成:DOM发生更新
  componentDidUpdate() {
    console.log("Hello组件更新完成");
  }

  // 5.组件从DOM中卸载掉:从DOM移除掉
  componentWillUnmount() {
    console.log("Hello组件卸载完成");
  }

  // 不常用的生命周期函数
  shouldComponentUpdate() {
    return true; //false 不会更新数据
  }
  // 等等
}

export default Hello;

运行效果

在这里插入图片描述

4.3 不常用生命周期函数

  • pgetSnapshotBeforeUpdate:在 React 更新 DOM 之前回调的一个函数,可以获取 DOM 更新前的一些信息(比如说滚动位置);
  • pgetDerivedStateFromProps:state 的值在任何时候都依赖于 props 时使用;该方法返回一个对象来更新 state;
  • 等等

4.4 总结:新旧版生命周期函数

旧版生命周期函数:

  1. 初始化阶段: 由 ReactDOM.render()触发—初次渲染 1. constructor() 2. componentWillMount() 3. render() 4. componentDidMount() =====> 常用
    一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
  2. 更新阶段: 由组件内部 this.setSate()或父组件 render 触发 1. shouldComponentUpdate() 2. componentWillUpdate() 3. render() =====> 必须使用的一个 4. componentDidUpdate()
  3. 卸载组件: 由 ReactDOM.unmountComponentAtNode()触发 1. componentWillUnmount() =====> 常用
    一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

新版生命周期函数:

  1. 初始化阶段: 由 ReactDOM.render()触发—初次渲染 1. constructor() 2. getDerivedStateFromProps 3. render() 4. componentDidMount() =====> 常用
    一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
  2. 更新阶段: 由组件内部 this.setSate()或父组件重新 render 触发 1. getDerivedStateFromProps 2. shouldComponentUpdate() 3. render() 4. getSnapshotBeforeUpdate 5. componentDidUpdate()
  3. 卸载组件: 由 ReactDOM.unmountComponentAtNode()触发 1. componentWillUnmount() =====> 常用
    一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

5.组件通信

5.1 父传子(props)

  • 父组件通过 属性**=值** 的形式来传递给子组件数据;
  • 子组件通过 props 参数获取父组件传递过来的数据;

父组件:Main:

export class Main extends Component {
  constructor() {
    super();

    this.state = {
      banners: [],
      productList: [],
    };
  }

  componentDidMount() {
    axios.get("http://123.207.32.32:8000/home/multidata").then((res) => {
      const banners = res.data.data.banner.list;
      const recommend = res.data.data.recommend.list;
      this.setState({
        banners,
        productList: recommend,
      });
    });
  }

  render() {
    const { banners, productList } = this.state;

    return (
      <div className="main">
        <div>Main</div>
        <MainBanner banners={banners} title="紫陌" />
        <MainBanner />
        <MainProductList productList={productList} />
      </div>
    );
  }
}

子组件:MainBanner

import React, { Component } from "react";
import PropTypes from "prop-types";

export class MainBanner extends Component {
  /* es2022的语法等同于下面 MainBanner.defaultProps */
  // static defaultProps = {
  //   banners:[],
  //   title:"默认标题"
  // }

  render() {
    // console.log(this.props)
    const { title, banners } = this.props;

    return (
      <div className="banner">
        <h2>封装一个轮播图: {title}</h2>
        <ul>
          {banners?.map((item) => {
            return <li key={item.acm}>{item.title}</li>;
          })}
        </ul>
      </div>
    );
  }
}

//MainBanner传入props类型进行验证
MainBanner.propTypes = {
  banners: PropTypes.array,
  title: PropTypes.string,
};

//MainBanner传入的props的默认值
MainBanner.defaultProps = {
  banners: [],
  title: "默认标题",
};

export default MainBanner;

props 类型验证

  • 从 React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types 库
  • 更多的验证方式,可以参考官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html
  • 比如验证数组,并且数组中包含哪些元素;
  • 比如验证对象,并且对象中包含哪些 key 以及 value 是什么类型;
  • 比如某个原生是必须的,使用 requiredFunc: PropTypes.func.isRequired
  • 如果没有传递,我们希望有默认值呢?
    • 我们使用 defaultProps 就可以了

禹神案例:

1. props限制案例:
----------------------------------------------------------------------------------------------------------
	//创建组件
    class Person extends React.Component{
        render(){
            // console.log(this);
            const {name,age,sex} = this.props
            //props是只读的
            //this.props.name = 'jack' //此行代码会报错,因为props是只读的
            return (
                <ul>
                    <li>姓名:{name}</li>
                    <li>性别:{sex}</li>
                    <li>年龄:{age+1}</li>
                </ul>
            )
        }
    }
    //对标签属性进行类型、必要性的限制
    Person.propTypes = {
        name:PropTypes.string.isRequired, //限制name必传,且为字符串
        sex:PropTypes.string,//限制sex为字符串
        age:PropTypes.number,//限制age为数值
        speak:PropTypes.func,//限制speak为函数
    }
    //指定默认标签属性值
    Person.defaultProps = {
        sex:'男',//sex默认值为男
        age:18 //age默认值为18
    }

.............

    function speak(){
        console.log('我说话了');
    }

2.props的简写方式也就是利用es2022语法
---------------------------------------------------------------------------------------------------
    	//创建组件
		class Person extends React.Component{

			constructor(props){
				//构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问props
				// console.log(props);
				super(props)
				console.log('constructor',this.props);
			}

			//对标签属性进行类型、必要性的限制
			static propTypes = {
				name:PropTypes.string.isRequired, //限制name必传,且为字符串
				sex:PropTypes.string,//限制sex为字符串
				age:PropTypes.number,//限制age为数值
			}

			//指定默认标签属性值
			static defaultProps = {
				sex:'男',//sex默认值为男
				age:18 //age默认值为18
			}

			render(){
				// console.log(this);
				const {name,age,sex} = this.props
				//props是只读的
				//this.props.name = 'jack' //此行代码会报错,因为props是只读的
				return (
					<ul>
						<li>姓名:{name}</li>
						<li>性别:{sex}</li>
						<li>年龄:{age+1}</li>
					</ul>
				)
			}
		}


-----------------------------------------------------------------------------------------------------
    3. 函数式组件传递props
    //创建组件
		function Person (props){
			const {name,age,sex} = props
			return (
					<ul>
						<li>姓名:{name}</li>
						<li>性别:{sex}</li>
						<li>年龄:{age}</li>
					</ul>
				)
		}
		Person.propTypes = {
			name:PropTypes.string.isRequired, //限制name必传,且为字符串
			sex:PropTypes.string,//限制sex为字符串
			age:PropTypes.number,//限制age为数值
		}

		//指定默认标签属性值
		Person.defaultProps = {
			sex:'男',//sex默认值为男
			age:18 //age默认值为18
		}
		//渲染组件到页面
		ReactDOM.render(<Person name="jerry"/>,document.getElementById('test1'))

5.2 子传父

  • 在 vue 中是通过自定义事件来完成的;
  • 在 React 中是通过 props 传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;

案例:

父组件:APP

export class App extends Component {
  constructor() {
    super();
    this.state = {
      counter: 100,
    };
  }

  //调用修改变量
  changeCounter(count) {
    this.setState({
      counter: this.state.counter + count,
    });
  }

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

    return (
      <div>
        <h2>当前计数为:{counter}</h2>
        <AddCounter addClick={(count) => this.changeCounter(count);}/>
      </div>
    );
  }
}

子组件:AddCounter

export class AddCounter extends Component {
  addCount(count) {
    //拿到父组件定义的函数传变量
    this.props.addClick(count);
  }

  render() {
    return (
      <div>
        <button
          onClick={(e) => {
            this.addCount(10);
          }}
        >
          点我加10
        </button>
      </div>
    );
  }
}

图解:

在这里插入图片描述

6.React 中的插槽

  • Vue 中有一个固定的做法是通过 slot 来完成插槽效果
  • React 对于这种需要插槽的情况非常灵活,有两种方案可以实现:
    • 组件的 children 子元素;
    • props 属性传递 React 元素;
  1. 第一种方式:每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容。

    children 实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;

父组件:APP

export class App extends Component {
  render() {
    return (
      <div>
        {/* 1.使用children实现插槽 */}
        <NavBar>
          <button>按钮</button>
          <h2>zimo</h2>
          <span>紫陌</span>
        </NavBar>
      </div>
    );
  }
}

子组件:NavBar

export class NavBar extends Component {
  render() {
    const { children } = this.props;
    return (
      <div className="nav-bar">
        <div className="left">{children[0]}</div>
        <div className="center">{children[1]}</div>
        <div className="right">{children[2]}</div>
      </div>
    );
  }
}

2.第二种方式:另外一个种方案就是使用 props 实现:通过具体的属性名,可以让我们在传入和获取时更加的精准;

父组件:APP

export class App extends Component {
  render() {
    const spanEl = <span>紫陌</span>;

    return (
      <div>
        {/* 2.使用props实现插槽 */}
        <NavBar leftSlot={<button>按钮2</button>} centerSlot={<h2>zimo</h2>} rightSlot={spanEl} />
      </div>
    );
  }
}

子组件:NavBar

export class NavBar extends Component {
  render() {
    const { leftSlot, centerSlot, rightSlot } = this.props;

    return (
      <div className="nav-bar">
        <div className="left">{leftSlot}</div>
        <div className="center">{centerSlot}</div>
        <div className="right">{rightSlot}</div>
      </div>
    );
  }
}

react 作用域插槽效果实现

tabs 案例演示:

在这里插入图片描述

7. 非父子通信(context)

  • Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props;

7.1context 相关 API

  1. React.createContext

    • 创建一个需要共享的 Context 对象:

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

    • defaultValue 是组件在顶层查找过程中没有找到对应的 Provider,那么就使用默认值

    • // 1.创建一个Context
      const userContext = React.createContext(defaultValue);
      
  2. Context.Provider

    • 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:

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

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

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

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

    • <MyContext.Provider value={/* 某个值*/}
      
  3. Class.contextType

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

    • 这能让你使用 this.context 来消费最近 Context 上的那个值;

    • 你可以在任何生命周期中访问到它,包括 render 函数中;

    • //设置组件的contextType为某一个Context
      //class.contextType = ThemeContext
      HomeInfo.contextType = ThemeContext;
      
    1. Context.Consumer
    • 这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。

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

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

    • <UserContext.Consumer>
        {(value) => {
          return <h2>Info User: {value.nickname}</h2>;
        }}
      </UserContext.Consumer>
      

什么时候使用Context.Consumer 呢?

  1. 当使用 value 的组件是一个函数式组件时;

  2. 当组件中需要使用多个 Context 时;

案例一:

APP 父组件:

theme-context.jsx
--------------------------------------------------------------------------------------------------------
import React from "react"

// 1.创建一个Context
const ThemeContext = React.createContext({ color: "blue", size: 10 })

export default ThemeContext

user-context.jsx
---------------------------------------------------------------------------------------------------------
import React from "react"

// 1.创建一个Context
const UserContext = React.createContext()

export default UserContext


APP.jsx
----------------------------------------------------------------------------------------------------------

import React, { Component } from 'react'
import Home from './Home'

import ThemeContext from "./context/theme-context"
import UserContext from './context/user-context'
import Profile from './Profile'

export class App extends Component {
  render() {

    return (
      <div>
        <h2>App</h2>

        {/*  通过ThemeContext中Provider中value属性为后代提供数据 */}
        <UserContext.Provider value={{nickname: "kobe", age: 30}}>
          <ThemeContext.Provider value={{color: "red", size: "30"}}>
            <A/>
          </ThemeContext.Provider>
        </UserContext.Provider>
        <Profile/>
      </div>
    )
  }
}

A 组件:

export class Home extends Component {
  render() {
    return (
      <div>
        <B />
        <C />
      </div>
    );
  }
}

B 组件:类组件

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

export class B extends Component {
  render() {
    // 4.第四步操作: 获取数据, 并且使用数据
    console.log(this.context);

    return (
      <div>
        {/*获取最近的context  */}
        <h2>HomeInfo: {this.context.color}</h2>
        {/*获取指定的context  */}
        <UserContext.Consumer>
          {(value) => {
            return <h2>Info User: {value.nickname}</h2>;
          }}
        </UserContext.Consumer>
      </div>
    );
  }
}

// 3.第三步操作: 设置组件的contextType为某一个Context
HomeInfo.contextType = ThemeContext;

C 组件:函数组件

import ThemeContext from "./context/theme-context";

function C() {
  return (
    <div>
      {/* 函数式组件中使用Context共享的数据 */}
      <ThemeContext.Consumer>
        {(value) => {
          return <h2> Banner theme:{value.color}</h2>;
        }}
      </ThemeContext.Consumer>
    </div>
  );
}

export default HomeBanner;

案例二:

import React, { Component } from "react";

//创建Context对象
const MyContext = React.createContext();
const { Provider, Consumer } = MyContext;
export default class A extends Component {
  state = { username: "tom", age: 18 };

  render() {
    const { username, age } = this.state;
    return (
      <div className="parent">
        <h3>我是A组件</h3>
        <h4>我的用户名是:{username}</h4>
        <Provider value={{ username, age }}>
          <B />
        </Provider>
      </div>
    );
  }
}

class B extends Component {
  render() {
    return (
      <div className="child">
        <h3>我是B组件</h3>
        <C />
      </div>
    );
  }
}

class C extends Component {
  //声明接收context
  static contextType = MyContext;
  render() {
    const { username, age } = this.context;
    return (
      <div className="grand">
        <h3>我是C组件</h3>
        <h4>
          我从A组件接收到的用户名:{username},年龄是{age}
        </h4>
      </div>
    );
  }
}

function C() {
  return (
    <div className="grand">
      <h3>我是C组件</h3>
      <h4>
        我从A组件接收到的用户名:
        <Consumer>{(value) => `${value.username},年龄是${value.age}`}</Consumer>
      </h4>
    </div>
  );
}
在应用开发中一般不用context, 一般都它的封装react插件;

8.setState 地使用

  • 开发中我们并不能直接通过修改 state 的值来让界面发生更新:
  • 因为我们修改了 state 之后,希望 React 根据最新的 State 来重新渲染界面,但是这种方式的修改 React 并不知道数据发生了变化;
  • React 并没有实现类似于 Vue2 中的 Object.defineProperty 或者 Vue3 中的 Proxy 的方式来监听数据的变化;
  • 我们必须通过 setState 来告知 React 数据已经发生了变化;

8.1 setState 三种用法

案例一:

export class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      message: "Hello Zimo",
    };
  }

  changeText() {
    // 1.基本使用
    this.setState({
      message: "你好啊, 紫陌",
    });

    // 2.setState可以传入一个回调函数
    // 好处一: 可以在回调函数中编写新的state的逻辑
    // 好处二: 当前的回调函数会将之前的state和props传递进来
    this.setState((state, props) => {
      // 1.编写一些对新的state处理逻辑
      // 2.可以获取之前的state和props值
      console.log(this.state.message, this.props);

      return {
        message: "你好啊, 紫陌",
      };
    });

    // 3.setState在React的事件处理中是一个异步调用
    // 如果希望在数据更新之后(数据合并), 获取到对应的结果执行一些逻辑代码
    // 那么可以在setState中传入第二个参数: callback
    this.setState({ message: "你好啊, 紫陌" }, () => {
      console.log("++++++:", this.state.message); //后打印,setState异步
    });
    console.log("------:", this.state.message); //先打印
  }

  render() {
    const { message } = this.state;

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

案例二:宇哥

import React, { Component } from "react";

export default class Demo extends Component {
  state = { count: 0 };

  add = () => {
    //对象式的setState
    //1.获取原来的count值
    const { count } = this.state;
    //2.更新状态
    this.setState({ count: count + 1 }, () => {
      console.log(this.state.count);
    });
    //console.log('12行的输出',this.state.count); //0

    //函数式的setState
    this.setState((state) => ({ count: state.count + 1 }));
  };

  render() {
    return (
      <div>
        <h1>当前求和为:{this.state.count}</h1>
        <button onClick={this.add}>点我+1</button>
      </div>
    );
  }
}

8.2 为什么 setState 设计为什么异步?

  • setState 设计为异步,可以显著的提升性能
    • 如果每次调用 setState 都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新渲染,这样效率是很低的;
    • 最好的办法应该是获取到多个更新,之后进行批量更新;
  • 如果同步更新了 state,但是还没有执行 render 函数,那么 state 和 props 不能保持同步;
    • state 和 props 不能保持一致性,会在开发中产生很多的问题;

案例:

import React, { Component } from "react";

function Hello(props) {
  return <h2>{props.message}</h2>;
}

export class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      message: "Hello Zimo",
      counter: 0,
    };
  }

  changeText() {
    this.setState({ message: "你好啊,紫陌" });
    console.log(this.state.message);
  }

  increment() {
    console.log("------");
    //以下三个最终是counter:1
    // this.setState({
    //   counter: this.state.counter + 1
    // })
    // this.setState({
    //   counter: this.state.counter + 1
    // })
    // this.setState({
    //   counter: this.state.counter + 1
    // })

    //以下结果是counter:3
    // this.setState((state) => {
    //   return {
    //     counter: state.counter + 1
    //   }
    // })
    // this.setState((state) => {
    //   return {
    //     counter: state.counter + 1
    //   }
    // })
    // this.setState((state) => {
    //   return {
    //     counter: state.counter + 1
    //   }
    // })
  }

  render() {
    const { message, counter } = this.state;
    console.log("render被执行");

    return (
      <div>
        <h2>message: {message}</h2>
        <button onClick={(e) => this.changeText()}>修改文本</button>
        <h2>当前计数: {counter}</h2>
        <button onClick={(e) => this.increment()}>counter+1</button>

        <Hello message={message} />
      </div>
    );
  }
}

8.3setState 一定是异步吗

  1. React18 之前
    • 分成两种情况:
    • 在组件生命周期或 React 合成事件中,setState 是异步;
    • 在 setTimeout 或者原生 dom 事件中,setState 是同步;
    • 在这里插入图片描述

?

  1. React18 之后

setState 默认是异步的

在*React18 之后,默认所有的操作都被放到了批处理中(异步处理)

如果希望代码可以同步会拿到,则需要执行特殊的 flushSync 操作

import { flushSync } from 'react-dom'
changeText() {
    setTimeout(() => {
        // 在react18之前, setTimeout中setState操作, 是同步操作
        // 在react18之后, setTimeout中setState异步操作(批处理)
        flushSync(() => {
            this.setState({ message: "你好啊, 李银河" })
        })
        console.log(this.state.message)
    }, 0);
}

9. render 函数优化(PureComponent,memo)

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

9.1shouldComponentUpdate

  • React 给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为 SCU),这个方法接受参数,并且需要有 返回值:

  • 该方法有两个参数:

  • 参数一:nextProps 修改之后,最新的 props 属性

  • 参数二:nextState 修改之后,最新的 state 属性

  • 该方法返回值是一个 boolean 类型:

  • 返回值为 true,那么就需要调用 render 方法;

  • 返回值为 false,那么久不需要调用 render 方法

  • 默认返回的是 true,也就是只要 state 发生改变,就会调用 render 方法;

9.2PureComponent

  • 如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。
  • 我们来设想一下 shouldComponentUpdate 中的各种判断的目的是什么?
  • props 或者 state 中的数据是否发生了改变,来决定 shouldComponentUpdate 返回 true 或者 false;
  • 事实上 React 已经考虑到了这一点,所以 React 已经默认帮我们实现好了
  • ? 将 class 继承自 PureComponent。

9.3 高阶组件 memo

  • 函数式组件我们在 props 没有改变时,也是不希望其重新渲染其 DOM 树结构的
  • 我们需要使用一个高阶组件 memo:

案例

APP组件------------------------------------------------------------
import React, { PureComponent } from 'react'
import Home from './Home'
import Recommend from './Recommend'
import Profile from './Profile'


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

    this.state = {
      message: "Hello World",
      counter: 0
    }
  }

  // shouldComponentUpdate(nextProps, newState) {
  //   // App进行性能优化的点
  //   if (this.state.message !== newState.message || this.state.counter !== newState.counter) {
  //     return true
  //   }
  //   return false
  // }

  changeText() {
    this.setState({ message: "你好啊,紫陌!" })
    // this.setState({ message: "Hello World" })
  }

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

  render() {
    console.log("App render")
    const { message, counter } = this.state

    return (
      <div>
        <h2>App-{message}-{counter}</h2>
        <button onClick={e => this.changeText()}>修改文本</button>
        <button onClick={e => this.increment()}>counter+1</button>
        <Home message={message}/>
        <Recommend counter={counter}/>
        <Profile message={message}/>
      </div>
    )
  }
}

export default App



Home组件----------------------------------------------------------------------------------------
import React, { PureComponent } from 'react'

export class Home extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      friends: []
    }
  }

  // shouldComponentUpdate(newProps, nextState) {
  //   // 自己对比state是否发生改变: this.state和nextState
  //   if (this.props.message !== newProps.message) {
  //     return true
  //   }
  //   return false
  // }

  render() {
    console.log("Home render")
    return (
      <div>
        <h2>Home Page: {this.props.message}</h2>
      </div>
    )
  }
}

export default Home

Profile组件----------------------------------------------------------------------------------------------------
import { memo } from "react"

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

export default Profile

10.数据不可变(state)

修改 state 数据必须把 state 数据拷贝出来替换掉原来的数据,PureComponent 才可以调用 render 函数。

性能优化 – React (reactjs.org) 数据不可变

import React, { PureComponent } from "react";

export class App extends PureComponent {
  constructor() {
    super();

    this.state = {
      books: [
        { name: "你不知道JS", price: 99, count: 1 },
        { name: "JS高级程序设计", price: 88, count: 1 },
        { name: "React高级设计", price: 78, count: 2 },
        { name: "Vue高级设计", price: 95, count: 3 },
      ],
      friend: {
        name: "kobe",
      },
      message: "Hello World",
    };
  }

  addNewBook() {
    const newBook = { name: "Angular高级设计", price: 88, count: 1 };

    // 1.直接修改原有的state, 重新设置一遍
    // 在PureComponent是不能重新渲染(render)
    /*    this.state.books.push(newBook)
    this.setState({ books: this.state.books }) */

    // 2.赋值一份books, 在新的books中修改, 设置新的books保证不是同一份books才会刷新render
    const books = [...this.state.books];
    books.push(newBook);

    this.setState({ books: books });
  }

  addBookCount(index) {
    // this.state.books[index].count++   这个也不能重新渲染render

    const books = [...this.state.books];
    books[index].count++;
    this.setState({ books: books });
  }

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

    return (
      <div>
        <h2>数据列表</h2>
        <ul>
          {books.map((item, index) => {
            return (
              <li key={index}>
                <span>
                  name:{item.name}-price:{item.price}-count:{item.count}
                </span>
                <button onClick={(e) => this.addBookCount(index)}>+1</button>
              </li>
            );
          })}
        </ul>
        <button onClick={(e) => this.addNewBook()}>添加新书籍</button>
      </div>
    );
  }
}

export default App;

11.ref 获取 DOM

  • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性;
  • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性;
  • 你不能在函数组件上使用 ref 属性,因为他们没有实例;可以通过 React.forwardRef

11.1ref 获取 DOM

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

export class App extends PureComponent {
  constructor() {
    this.titleRef = createRef();
    this.titleEl = null;
  }

  getNativeDOM() {
    // 1.方式一: 在React元素上绑定一个ref字符串
    // console.log(this.refs.why)

    // 2.方式二: 提前创建好ref对象, createRef(), 将创建出来的对象绑定到元素
    // console.log(this.titleRef.current)

    // 3.方式三: 传入一个回调函数, 在对应的元素被渲染之后, 回调函数被执行, 并且将元素传入
    console.log(this.titleEl);
  }

  render() {
    return (
      <div>
        <h2 ref="why">Hello 紫陌</h2>
        <h2 ref={this.titleRef}>你好zimo</h2>
        <h2 ref={(el) => (this.titleEl = el)}>你好啊紫陌</h2>
        <button onClick={(e) => this.getNativeDOM()}>获取DOM</button>
      </div>
    );
  }
}

export default App;

11.2 ref 获取类组件实例

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

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

  render() {
    return <h1>Hello World</h1>;
  }
}

export class App extends PureComponent {
  constructor() {
    super();

    this.hwRef = createRef();
  }

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

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

export default App;

11.3 ref 获取函数组实例

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

const HelloWorld = forwardRef(function (props, ref) {
  return (
    <div>
      <h1 ref={ref}>Hello World</h1>
      <p>哈哈哈</p>
    </div>
  );
});

export class App extends PureComponent {
  constructor() {
    super();

    this.hwRef = createRef();
  }

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

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

export default App;

12.受控和非受控组件

  • 表单元素通常自己维护 state,并根据用户输入进 行更新。

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

  • 我们将两者结合起来,使 React 的 state 成为“唯一数据源”; ? 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作;

  • 被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”;

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

  • 类似 vue 的双向绑定

    12.1 受控组件

class App extends PureComponent {
  constructor(props) {
    super();
    this.state = {
      name: "zimo",
    };
  }

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

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

        {/* 非受控组件 */}
        <input type="text" />
      </div>
    );
  }
}

各种表单收集数据例子

class App extends PureComponent {
  constructor(props) {
    super();
    this.state = {
      username: "",
      password: "",

      isAGree: false,

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

      fruit: ["orange"],
    };
  }

  handleSubmitClick(event) {
    // 1.阻止默认行为
    event.preventDefault();

    // 2.获取到所有的表单数据, 对数据进行组织
    console.log("获取所有的输入内容");
    console.log(this.state.username, this.state.password);
    const hobbies = this.state.hobbies.filter((item) => item.isChecked).map((item) => item.value);
    console.log("获取爱好: ", hobbies);

    // 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
  }

  handleInputChage(event) {
    console.log(event);
    this.setState({
      [event.target.name]: event.target.value,
    });
  }

  handleAgreeChange(event) {
    this.setState({
      isAGree: event.target.checked,
    });
  }

  handleHobbiesChange(evnet, index) {
    const hobbies = [...this.state.hobbies];
    hobbies[index].isChecked = evnet.target.checked;
    this.setState({ hobbies });
  }

  handleFruitChange(event) {
    const options = Array.from(event.target.selectedOptions);
    const values = options.map((item) => item.value);
    this.setState({ fruit: values });

    // 效果同上
    const values2 = Array.from(event.target.selectedOptions, (item) => item.value);
    console.log(values2);
  }

  render() {
    const { username, password, isAGree, hobbies, fruit } = this.state;
    return (
      <div>
        {/* 表单 */}
        <form onSubmit={(e) => this.handleSubmitClick(e)}>
          {/* 1.用户名密码 */}
          <div>
            <label htmlFor="username">
              用户名:
              <input
                type="text"
                id="username"
                value={username}
                name="username"
                onChange={(e) => this.handleInputChage(e)}
              />
            </label>
            <label htmlFor="password">
              密码:
              <input
                type="password"
                id="password"
                value={password}
                name="password"
                onChange={(e) => this.handleInputChage(e)}
              />
            </label>
          </div>

          {/* 2.checkbox 单选*/}
          <label htmlFor="agree">
            <input type="checkbox" id="agree" checked={isAGree} onChange={(e) => this.handleAgreeChange(e)} />
            同意协议
          </label>

          {/* 3.checkbox */}
          <div>
            你的爱好:
            {hobbies.map((item, index) => {
              return (
                <label htmlFor={item.value} key={item.value}>
                  <input
                    type="checkbox"
                    value={item.value}
                    checked={item.isChecked}
                    id={item.value}
                    onChange={(e) => this.handleHobbiesChange(e, index)}
                  />
                  <span>{item.text}</span>
                </label>
              );
            })}
          </div>

          {/* 4.select  多选*/}
          <select value={fruit} onChange={(e) => this.handleFruitChange(e)} multiple>
            <option value="apple">苹果</option>
            <option value="orange">橘子</option>
            <option value="banana">香蕉</option>
          </select>
          {/* 提交按钮 */}
          <button type="submit">注册</button>
        </form>
      </div>
    );
  }
}

export default App;

12.2 非受控组件

  • React 推荐大多数情况下使用 受控组件 来处理表单数据: 一个受控组件中,表单数据是由 React 组件来管理的;
  • 另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;
  • 如果要使用非受控组件中的数据,那么我们需要使用 ref 来从 DOM 节点中获取表单数据。
  • 使用 ref 来获取 input 元素, 在非受控组件中通常使用来设置默认
  • 开发非受控组件用的比较少
import React, { PureComponent, createRef } from "react";

class App extends PureComponent {
  constructor(props) {
    super();
    this.state = {
      intro: "紫陌yyds",
    };

    this.introRef = createRef();
  }

  handleSubmitClick(event) {
    // 1.阻止默认行为
    event.preventDefault();

    // 2.获取到所有的表单数据, 对数据进行组织
    console.log("ref", this.introRef.current.value);

    // 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)
  }

  render() {
    const { intro } = this.state;
    return (
      <div>
        {/* 非受控组件 */}
        <input type="text" defaultValue={intro} ref={this.introRef} />

        {/* 提交按钮 */}
        <button type="submit">注册</button>
      </div>
    );
  }
}

13.高阶组件

  • 高阶函数的定义至少满足以下条件之一:
    • 接受一个或多个函数作为输入;
    • 输出一个函数;
  • JavaScript 中比较常见的 filter、map、reduce 都是高阶函数。
  • 高阶组件的英文是 Higher-Order Components,简称为 HOC;
  • 官方的定义:高阶组件是参数为组件,返回值为新组件的函数;

**高阶组件 本身不是一个组件,而是一个函数,这个函数的参数是一个组件,返回值也是一个组件 **

13.1 高阶组件的定义和作用

import React, { PureComponent } from "react";

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

class Hello extends PureComponent {
  render() {
    return <div>你好啊,紫陌</div>;
  }
}

const HelloHOC = hoc(Hello);

export class App extends PureComponent {
  render() {
    return (
      <div>
        <HelloHOC />
      </div>
    );
  }
}

export default App;

13.2 高阶组件应用-props 增强

APP 组件

import React, { PureComponent } from "react";
import enhancedUserInfo from "./hoc/enhanced_props";
import About from "./pages/About";

const Home = enhancedUserInfo(function (props) {
  return (
    <h1>
      Home: {props.name}-{props.level}-{props.banners}
    </h1>
  );
});

const Profile = enhancedUserInfo(function (props) {
  return (
    <h1>
      Profile: {props.name}-{props.level}
    </h1>
  );
});

const HelloFriend = enhancedUserInfo(function (props) {
  return (
    <h1>
      HelloFriend: {props.name}-{props.level}
    </h1>
  );
});

export class App extends PureComponent {
  render() {
    return (
      <div>
        <Home banners={["轮播图1", "轮播图2"]} />
        <Profile />
        <HelloFriend />

        <About />
      </div>
    );
  }
}

export default App;

enhancedUserInfo 高阶组件:

import { PureComponent } from "react";

// 定义组件: 给一些需要特殊数据的组件, 注入props
function enhancedUserInfo(OriginComponent) {
  class NewComponent extends PureComponent {
    constructor(props) {
      super(props);

      this.state = {
        userInfo: {
          name: "zimo",
          level: 99,
        },
      };
    }

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

  return NewComponent;
}

export default enhancedUserInfo;

About 组件

import React, { PureComponent } from "react";
import enhancedUserInfo from "../hoc/enhanced_props";

export class About extends PureComponent {
  render() {
    return <div>About: {this.props.name}</div>;
  }
}

export default enhancedUserInfo(About);

13.3 高阶组件应用-Context 共享

定义 context theme_context.js

import { createContext } from "react";

const ThemeContext = createContext();

export default ThemeContext;

定义 HOC 函数 with_theme.js

import ThemeContext from "../context/theme_context";

function withTheme(OriginComponent) {
  return (props) => {
    return (
      <ThemeContext.Consumer>
        {(value) => {
          return <OriginComponent {...value} {...props} />;
        }}
      </ThemeContext.Consumer>
    );
  };
}
export default withTheme;

Product 组件:

import React, { PureComponent } from "react";
import withTheme from "../hoc/with_theme";

export class Product extends PureComponent {
  render() {
    const { color, size } = this.props;
    return (
      <div>
        <h2>
          Product:{color} - {size}
        </h2>
      </div>
    );
  }
}

export default withTheme(Product);

APP 组件:

import React, { PureComponent } from "react";
import ThemeContext from "./context/theme_context";
import Product from "./pages/Product";

export class App extends PureComponent {
  render() {
    return (
      <div>
        <ThemeContext.Provider value={{ color: "red", size: 50 }}>
          <Product />
        </ThemeContext.Provider>
      </div>
    );
  }
}

export default App;

13.4 渲染判断鉴权

  • 在开发中,我们可能遇到这样的场景:
  • 某些页面是必须用户登录成功才能进行进入;
  • 如果用户没有登录成功,那么直接跳转到登录页面;
  • 就可以使用高阶组件来完成鉴权操作:
function loginAuth(OriginComponent) {
  return (props) => {
    // 从localStorage中获取token
    const token = localStorage.getItem("token");

    if (token) {
      return <OriginComponent {...props} />;
    } else {
      return <h2>请先登录, 再进行跳转到对应的页面中</h2>;
    }
  };
}

export default loginAuth;

13.5 生命周期劫持

可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:

下面是高阶组件是计算渲染时间:

import { PureComponent } from "react";

function logRenderTime(OriginComponent) {
  return class extends PureComponent {
    UNSAFE_componentWillMount() {
      this.beginTime = new Date().getTime();
    }

    componentDidMount() {
      this.endTime = new Date().getTime();
      const interval = this.endTime - this.beginTime;
      console.log(`当前${OriginComponent.name}页面花费了${interval}ms渲染完成!`);
    }

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

export default logRenderTime;

13.6 高阶组件的意义

  • 高阶组件可以针对某些 React 代码进行更加优雅的处理。
  • 早期的 React 有提供组件之间的一种复用方式是 mixin,目前已经不再建议使用:
  • Mixin 可能会相互依赖,相互耦合,不利于代码维护;
  • 不同的 Mixin 中的方法可能会相互冲突;
  • Mixin 非常多时,组件处理起来会比较麻烦,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性;
  • HOC 也有缺陷:
  • HOC 需要在原组件上进行包裹或者嵌套,如果大量使用 HOC,将会产生非常多的嵌套,这让调试变得非常困难;
  • HOC 可以劫持 props,在不遵守约定的情况下也可能造成冲突;
  • Hooks 的出现,是开创性的,它解决了很多 React 之前的存在的问题
  • 比如 this 指向问题、比如 hoc 的嵌套复杂度问题等等;

14. Portals 的使用

  • 某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的 DOM 元素中(默认都是挂载到 id 为 root 的 DOM 元素上的)。
  • Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:
  • 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;
  • 第二个参数(container)是一个 DOM 元素;

createPortal(

App H2

, document.querySelector(“#zimo”))

index.html-------------------------------------------------------------------------------------------------
<div id="root"></div>
<div id="zimo"></div>

App组件:---------------------------------------------------------------------------------------------------

import React, { PureComponent } from 'react'
import { createPortal } from "react-dom"

export class App extends PureComponent {
  render() {
    return (
      <div className='app'>
        <h1>App H1</h1>
         {/*元素放到zimo容器 */}
        {
          createPortal(<h2>App H2</h2>, document.querySelector("#zimo"))
        }
      </div>
    )
  }
}

export default App

15.Fragment

  • 开发中,我们总是在一个组件中返回内容时包裹一个 div 元素:
  • 我们又希望可以不渲染这样一个 div,使用 Fragment (类似于 Vue 中的 template)
  • Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点
  • React 还提供了 Fragment 的短语法:
  • 它看起来像空标签 <></>
  • 如果我们需要在 Fragment 中添加 key,那么就不能使用短语法
import React, { PureComponent, Fragment } from 'react'

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

    this.state = {
      sections: [
        { title: "哈哈哈", content: "我是内容, 哈哈哈" },
        { title: "呵呵呵", content: "我是内容, 呵呵呵" },
        { title: "嘿嘿嘿", content: "我是内容, 嘿嘿嘿" },
        { title: "嘻嘻嘻", content: "我是内容, 嘻嘻嘻" },
      ]
    }
  }

  render() {
    const { sections } = this.state

    return (
      <>
        <h2>我是App的标题</h2>
        <p>我是App的内容, 哈哈哈哈</p>
        <hr />

        {
          sections.map(item => {
            return (
        	{/*这里不能使用简写 因为key*/}
              <Fragment key={item.title}>
                <h2>{item.title}</h2>
                <p>{item.content}</p>
              </Fragment>
            )
          })
        }
      </>
    )
  }
}

export default App

16. StrictMode

  • StrictMode 是一个用来突出显示应用程序中潜在问题的工具:
  • 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI;
  • 它为其后代元素触发额外的检查和警告
  • 严格模式检查仅在开发模式下运行;它们不会影响生产构建;

例子:

  • 可以为应用程序的任何部分启用严格模式
  • 不会对 Profile 组件运行严格模式检查;
  • 但是,Home 以及它们的所有后代元素都将进行检查;
import React, { PureComponent, StrictMode } from "react";
import Home from "./pages/Home";
import Profile from "./pages/Profile";

export class App extends PureComponent {
  render() {
    return (
      <div>
        <StrictMode>
          <Home />
        </StrictMode>
        <Profile />
      </div>
    );
  }
}

export default App;

16.1 严格模式检查的是什么

  1. 识别不安全的生命周期:
  2. 使用过时的 ref API
  3. .检查意外的副作用
    1. 这个组件的 constructor 会被调用两次;
    2. 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用;
    3. 在生产环境中,是不会被调用两次的;
  4. 使用废弃的 findDOMNode 方法
    1. 在之前的 React API 中,可以通过 findDOMNode 来获取 DOM,
  5. 检测过时的 context API
    1. 早期的 Context 是通过 static 属性声明 Context 对象属性,通过 getChildContext 返回 Context 对象等方式来使用 Context 的; 目前这种方式已经不推荐使用,

17. React 过渡动画实现

  • React 曾为开发者提供过动画插件 react-addons-css-transition-group,后由社区维护,形成了现在的 react-transition-group。
  • 这个库可以帮助我们方便的实现组件的 入场 和 离场 动画,使用时需要进行额外的安装
  • react-transition-group 本身非常小,不会为我们应用程序增加过多的负担。

npm

npm install react-transition-group --save

yarn

yarn add react-transition-group

17.1 react-transition-group 主要组件

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

17.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 名称:比如 zimoenter、zimo-enter-active、zimo-enter-done;
    • timeout:
    • 过渡动画的时间
    • appear:
    • 是否在初次进入添加动画(需要和 in 同时为 true)
    • unmountOnExit:退出后卸载组件
    • 其他属性可以参考官网来学习: ? https://reactcommunity.org/react-transition-group/transition
    • CSSTransition 对应的钩子函数:主要为了检测动画的执行过程,来完成一些 JavaScript 的操作
    • onEnter:在进入动画之前被触发;
    • onEntering:在应用进入动画时被触发;
    • onEntered:在应用进入动画结束后被触发;

APP 组件:

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

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

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

        {/* 无动画 */}
        {/* {isShow && <h2>紫陌YYDS</h2>} */}

        {/* 有动画 */}
        <CSSTransition
          // 以下为属性
          in={isShow}
          unmountOnExit={true}
          timeout={2000}
          classNames={"zimo"}
          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">
            <h2>紫陌YYDS</h2>
            <p>zimo学前端</p>
          </div>
        </CSSTransition>
      </div>
    );
  }
}

export default App;

CSS:

.zimo-appear,
.zimo-enter {
  opacity: 0;
}

.zimo-appear-active,
.zimo-enter-active {
  opacity: 1;
  transition: opacity 2s ease;
}

/* 离开动画 */
.zimo-exit {
  opacity: 1;
}

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

17.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 属性;

  • 当 key 值改变时,CSSTransition 组件会重新渲染,也就会触发动画

APP 组件:

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

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

    this.state = {
      isLogin: true,
    };
  }
  render() {
    const { isLogin } = this.state;

    return (
      <div>
        <SwitchTransition mode="out-in">
          <CSSTransition
            // 当key值改变时,CSSTransition组件会重新渲染,也就会触发动画
            key={isLogin ? "login" : "exit"}
            classNames={"login"}
            timeout={1000}
          >
            <button onClick={(e) => this.setState({ isLogin: !isLogin })}>{isLogin ? "退出" : "登录"}</button>
          </CSSTransition>
        </SwitchTransition>
      </div>
    );
  }
}

export default App;

CSS :

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

.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;
  transition: all 1s ease;
}

17.4 TransitionGroup 动画

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

APP 组件:

import React, { PureComponent } from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group";
import "./style.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}</span>
                  <button onClick={(e) => this.removeBook(index)}>删除</button>
                </li>
              </CSSTransition>
            );
          })}
        </TransitionGroup>
        <button onClick={(e) => this.addNewBook()}>添加新书籍</button>
      </div>
    );
  }
}

export default App;

CSS:

.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;
}

18. React 中的 CSS

  • css 一直是 React 的痛点,也是被很多开发者吐槽、诟病的一个点。
  • 在这一点上,Vue 做的要好于 React
  • React 官方并没有给出在 React 中统一的样式风格: 普通的 css,到 css modules,再到 css in js,有几十种不同的解决方案,上百个不同的库; 大家一致在寻找最好的或者说最适合自己的 CSS 方案,但是到目前为止也没有统一的方案;

18.1 内联样式

  • 内联样式是官方推荐的一种 css 样式的写法:
  • style 接受一个采用小驼峰命名属性的 JavaScript 对象,,而不是 CSS 字符串;
  • 并且可以引用 state 中的状态来设置相关的样式;
  • 内联样式的优点:
  • 内联样式, 样式之间不会有冲突
  • 可以动态获取当前 state 中的状态
  • 内联样式的缺点:
  • 写法上都需要使用驼峰标识
  • 某些样式没有提示
  • 大量的样式, 代码混乱
  • 某些样式无法编写(比如伪类/伪元素)
export class App extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      size: 12,
    };
  }

  addTitleSize() {
    this.setState({
      size: this.state.size + 2,
    });
  }

  render() {
    const { size } = this.state;
    return (
      <div>
        <button onClick={(e) => this.addTitleSize()}>字体变大</button>
        <h2 style={{ fontSize: `${size}px` }}>我是紫陌</h2>
        <h2 style={{ color: "yellowGreen" }}>我是紫陌</h2>
      </div>
    );
  }
}

18.2 普通的 css

  • 普通的 css 我们通常会编写到一个单独的文件,之后再进行引入。
  • 这样的编写方式和普通的网页开发中编写方式是一致的:
  • 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响;
  • 但是普通的 css都属于全局的 css,样式之间会相互影响
  • 这种编写方式最大的问题是样式之间会相互层叠掉;

18.3 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 中很受欢迎的一种方式。

Home.module.css

.section {
  border: 1px solid skyblue;
}

.title {
  color: purple;
}

Home.jsx

import React, { PureComponent } from "react";
import homeStyle from "./Home.module.css";

export class Home extends PureComponent {
  render() {
    return (
      <div className={homeStyle.section}>
        <div className={homeStyle.title}>Home的标题</div>
      </div>
    );
  }
}

export default Home;

18.4 CSS in JS

  • 官方文档也有提到过 CSS in JS 这种方案:

    • CSS-in-JS” 是指一种模式其中 CSS 由 JavaScript 生成而不是在外部文件中定义
    • 注意此功能并不是 React 的一部分,而是由第三方库提供
    • React 对样式如何定义并没有明确态度;
  • 在传统的前端开发中,我们通常会将结构(HTML)、样式(CSS)、逻辑(JavaScript)进行分离。

    • React 的思想中认为逻辑本身和 UI 是无法分离的,所以才会有了 JSX 的语法
    • CSS-in-JS 的模式就是一种将样式(CSS)也写入到 JavaScript 中的方式,并且可以方便的使用 JavaScript 的状态;
    • 所以 React 有被人称之为 All in JS;
  1. 认识 styled-components

    • CSS-in-JS 通过 JavaScript 来为 CSS 赋予一些能力,包括类似于CSS 预处理器一样的样式嵌套、函数定义、逻辑复用、动态修 改状态等等; 虽然 CSS 预处理器也具备某些能力,但是获取动态状态依然是一个不好处理的点
    • 所以,目前可以说 CSS-in-JS 是 React 编写 CSS 最为受欢迎的一种解决方案;
  2. 目前比较流行的 CSS-in-JS 的库

    • styled-components
    • emotion
    • glamorous

目前可以说 styled-components 依然是社区最流行的 CSS-in-JS 库

  1. props、attrs 属性

    • props 可以被传递给 styled 组件 ? 获取 props 需要通过${}传入一个插值函数,props 会作为该函数的参数;

    • 这种方式可以有效的解决动态样式的问题;

    • 添加 attrs 属性

    • export const SectionWrapper = styled.div.attrs((props) => ({
        // 可以通过attrs给标签模板字符串中提供属性
        tColor: props.color || "blue",
      }))` xxx  xxx `;
      
  2. 案例

APP 组件:

import React, { PureComponent } from "react";
import { AppWrapepr, SectionWrapper } from "./style";

export class App extends PureComponent {
  constructor() {
    super();
    this.state = {
      size: 30,
      color: "yellow",
    };
  }
  render() {
    const { size, color } = this.state;
    return (
      <AppWrapepr>
        <SectionWrapper size={size} color={color}>
          <h2 className="title">我是紫陌</h2>
          <p className="content">我是zimo</p>
          <button onClick={(e) => this.setState({ color: "skyblue" })}>修改颜色</button>
        </SectionWrapper>

        <div className="footer">
          <h2>这是底部</h2>
          <p>zimo@yyds</p>
        </div>
      </AppWrapepr>
    );
  }
}

export default App;

?

style.js 文件

import styled from "styled-components";
import { primaryColor, largeSize } from "./style/variables";

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

// 子元素单独抽取到一个样式组件
export const SectionWrapper = styled.div.attrs((props) => ({
  // 可以通过attrs给标签模板字符串中提供属性
  tColor: props.color || "blue",
}))`
  border: 1px solid red;

  .title {
    /* 可以接受外部传入的props */
    font-size: ${(props) => props.size}px;
    color: ${(props) => props.tColor};

    &:hover {
      background-color: purple;
    }
  }
  /* 从一个单独文件中引入变量 */
  .content {
    font-size: ${largeSize}px;
    color: ${primaryColor};
  }
`;

variables.js 文件 (变量文件)

export const primaryColor = "#ff8822";
export const secondColor = "#ff7788";

export const smallSize = "12px";
export const middleSize = "14px";
export const largeSize = "18px";
  1. styled 高级特性

styled 设置主题,支持样式的继承

index.jsx(根组件传主题变量)

<ThemeProvider theme={{ color: "purple", size: "50px" }}>
  <App />
</ThemeProvider>

APP.jsx

import React, { PureComponent } from "react";
import { HomeWrapper, ButtonWrapper } from "./style";

export class Home extends PureComponent {
  render() {
    return (
      <HomeWrapper>
        <h2 className="header">商品列表</h2>
        <ButtonWrapper>哈哈哈</ButtonWrapper>
      </HomeWrapper>
    );
  }
}

export default Home;

style.js

import styled from "styled-components";

const Button = styled.button`
  border: 1px solid red;
  border-radius: 5px;
`;

// 继承
export const ButtonWrapper = styled(Button)`
  background-color: #0f0;
  color: #fff;
`;

// 主题
export const HomeWrapper = styled.div`
  .header {
    color: ${(props) => props.theme.color};
    font-size: ${(props) => props.theme.size};
  }
`;

18.5 React 中添加 class(classnames 库)

React 在 JSX 给了我们开发者足够多的灵活性,你可以像编写 JavaScript 代码一样,通过一些逻辑来决定是否添加某些 class

用于动态添加 classnames 的一个库

yarn add classnames

import React, { PureComponent } from "react";
import classNames from "classnames";

export class App extends PureComponent {
  constructor() {
    super();
    this.state = {
      isbbb: true,
      isccc: true,
    };
  }

  render() {
    const { isbbb, isccc } = this.state;

    const classList = ["aaa"];
    if (isbbb) classList.push("bbb");
    if (isccc) classList.push("ccc");
    const classname = classList.join(" ");
    return (
      <div>
        {/* 不推荐  */}
        <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>
      </div>
    );
  }
}

语法:

classNames(' foo','bar'); //=>'foo  bar'
classNames('foo'{bar: true });//=>·'foo bar'
classNames({'foo-bar': false });//=>''
classNames({ foo: true },{ bar: true });//=>'foo bar'
classNames({ foo: true, bar: true }); //=>.'foo bar'
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); //=>'foo bar baz quux'
classNames(null,false,'bar',undefined,0,1,{baz:null}, '') // 'bar 1'

19.纯函数理解

  • 函数式编程中有一个非常重要的概念叫纯函数,JavaScript 符合函数式编程的范式,所以也有纯函数的概念;

  • 在 react 开发中纯函数是被多次提及的,比如 react 中组件就被要求像是一个纯函数(为什么是像,因为还有 class 组件),redux 中有一个 reducer 的概念,也是要求 必须是一个纯函数;

  • 简单概要纯函数:

    • 确定的输入,一定会产生确定的输出;
    • 函数在执行过程中,不能产生副作用;
  • 纯函数副作用概念:

    • 表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响, 比如修改了全局变量,修改参数或者改变外部的存储;
  • 纯函数的作用和优势

    • 可以安心的编写和安心的使用; 保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的 外部变量是否已经发生了修改; 用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
    • React 中就要求我们无论是函数还是 class 声明一个组件,这个组件都必须像纯函数一样,保护它们的 props 不被修改,reducer也被要求是一个纯函数

20. Redux

20.1 Redux 的三大原则

  • 单一数据源
    • 整个应用程序的state 被存储在一颗 object tree 中,并且这个 object tree只存储在一个 store 中
    • Redux 并没有强制让我们不能创建多个 Store,但是那样做并不利于数据的维护;
    • 单一的数据源可以让整个应用程序的 state 变得方便维护、追踪、修改
  • State 是只读的
  • 唯一修改 State 的方法一定是触发 action,不要试图在其他地方通过任何的方式来修改 State:
  • 这样就确保了 View 或网络请求都不能直接修改 state,它们只能通过 action 来描述自己想要如何修改 state;
  • 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心 race condition(竟态)的问题;
  • 使用纯函数来执行修改
    • 通过 reducer 将 旧 state 和 actions 联系在一起,并且返回一个新的 State:
    • 随着应用程序的复杂度增加,我们可以将 reducer 拆分成多个小的 reducers,分别操作不同 state tree 的一部分;
    • 但是所有的 reducer 都应该是纯函数,不能产生任何的副作用;

20.2 Redux 的使用过程

  • 创建 Store 来存储这个 state
    • 创建 store 时必须创建 reducer; -
    • 通过 store.getState 来获取当前的 state;
  • 通过 action 来修改 state
    • 通过 dispatch 来派发 action;
    • 通常 action 中都会有 type 属性,也可以携带其他的数据;
  • 修改 reducer 中的处理代码
    • reducer 是一个纯函数,不需要直接修改 state
  • 可以在派发 action 之前,监听 store 的变化

20.3Redux 结构划分

  • 创建 store/index.js 文件:
  • 创建 store/reducer.js 文件:
  • 创建 store/actionCreators.js 文件:
  • 创建 store/constants.js 文件:

在这里插入图片描述
在这里插入图片描述

20.4 脱离 React 的 redux 代码

redux 和 react 没有直接的关系,完全可以在 React, Angular, Ember, jQuery, or vanilla JavaScript 中使用 Redux

store/index.js

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

// 创建的store
const store = createStore(reducer);

module.exports = store;

reducer.js

const { ADD_NUMBER, CHANGE_NAME } = require("./constants");

// 初始化数据
const initialState = {
  name: "张三",
  counter: 100,
};

function reducer(state = initialState, action) {
  switch (action.type) {
    case CHANGE_NAME:
      return { ...state, name: action.name };

    case ADD_NUMBER:
      return { ...state, counter: state.counter + action.num };
  }
}

module.exports = reducer;

constants.js

const ADD_NUMBER = "add_number";
const CHANGE_NAME = "change_name";

module.exports = {
  ADD_NUMBER,
  CHANGE_NAME,
};

actionCreators.js

const { CHANGE_NAME, ADD_NUMBER } = require("./constants.js");

const changeNameAction = (name) => ({
  type: CHANGE_NAME,
  name,
});

const addNumberAction = (num) => ({
  type: ADD_NUMBER,
  num,
});

module.exports = {
  changeNameAction,
  addNumberAction,
};

上面四模块就是 store 的结构

组件派发 Action

const store = require("./store1/index.js");
const { addNumberAction, changeNameAction } = require("./actionCreator.js");

const unsubscribe = store.subscribe(() => {
  console.log("订阅数据的变化:", store.getSatte());
});

store.dispatch(changeNameAction("zimo"));
store.dispatch(changeNameAction("紫陌"));
store.dispatch(changeNameAction("yy"));

store.dispatch(addNumberAction(13));
store.dispatch(addNumberAction(167));
store.dispatch(addNumberAction(137));

20.5React 中的 Redux

Store 目录结构代码略过。。。。同上。直接看组件

APP 组件:

import React, { Component } from "react";
import Home from "./pages/home";
import Profile from "./pages/profile";
import "./style.css";
import store from "./store";

class App extends Component {
  constructor() {
    super();
    this.state = {
      counter: store.getState().counter,
    };
  }
  componentDidMount() {
    store.subscribe(() => {
      const state = store.getState();
      this.setState({ counter: state.counter });
    });
  }
  render() {
    const { counter } = this.state;
    return (
      <div>
        <h2>App counter: {counter}</h2>
        <div className="pages">
          <Home />
          <Profile />
        </div>
      </div>
    );
  }
}

export default App;

Home 组件:

import React, { PureComponent } from "react";
import store from "../store";
import { addNumberAction } from "../store/actionCreators";

export class Home extends PureComponent {
  constructor() {
    super();

    this.state = {
      counter: store.getState().counter,
    };
  }

  componentDidMount() {
    store.subscribe(() => {
      const state = store.getState();
      this.setState({ counter: state.counter });
    });
  }
  addNumber(num) {
    store.dispatch(addNumberAction(num));
  }
  render() {
    const { counter } = this.state;
    return (
      <div>
        <h2>Home counterer: {counter}</h2>
        <div>
          <button onClick={(e) => this.addNumber(1)}>+1</button>
          <button onClick={(e) => this.addNumber(5)}>+5</button>
          <button onClick={(e) => this.addNumber(8)}>+8</button>
        </div>
      </div>
    );
  }
}

export default Home;

Profile 组件

import React, { PureComponent } from "react";
import store from "../store";
import { addNumberAction } from "../store/actionCreators";

export class profile extends PureComponent {
  constructor() {
    super();

    this.state = {
      counter: store.getState().counter,
    };
  }

  componentDidMount() {
    store.subscribe(() => {
      const state = store.getState();
      this.setState({ counter: state.counter });
    });
  }

  addNumber(num) {
    store.dispatch(addNumberAction(num));
  }
  render() {
    const { counter } = this.state;
    return (
      <div>
        <h2>Profile Counter: {counter}</h2>
        <div>
          <button onClick={(e) => this.addNumber(1)}>+1</button>
          <button onClick={(e) => this.addNumber(5)}>+5</button>
          <button onClick={(e) => this.addNumber(8)}>+8</button>
        </div>
      </div>
    );
  }
}

export default profile;

上面就是基本使用。但是有个问题。每次写一个组件就要写一个一套相似的逻辑如图:

在这里插入图片描述

20.6 Redux–connect 高阶函数

  • connect 高阶函数 实际上 redux 官方帮助我们提供了 react-redux 的库,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效。

    安装 react-redux:yarn add react-redux

  1. 在 index.js 文件中:context 的全局注入一个 store

    import App from "./App";
    import { Provider } from "react-redux";
    import store from "./store";
    
    const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(
      <Provider store={store}>
        <App />
      </Provider>
    );
    
  2. About 组件:有加减逻辑

import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { addNumberAction, subNumberAction } from "../store/actionCreators";

export class About extends PureComponent {
  calcNumber(num, isAdd) {
    if (isAdd) {
      console.log("加", num);
      this.props.addNumber(num);
    } else {
      console.log("减", num);
      this.props.subNumber(num);
    }
  }
  render() {
    const { counter } = this.props;
    return (
      <div>
        <h2>About Page: {counter}</h2>
        <div>
          <button onClick={(e) => this.calcNumber(6, true)}>+6</button>
          <button onClick={(e) => this.calcNumber(88, true)}>+88</button>
          <button onClick={(e) => this.calcNumber(6, false)}>-6</button>
          <button onClick={(e) => this.calcNumber(88, false)}>-88</button>
        </div>
      </div>
    );
  }
}

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

const mapDispatchToProps = (dispatch) => {
  return {
    addNumber: (num) => {
      dispatch(addNumberAction(num));
    },
    subNumber(num) {
      dispatch(subNumberAction(num));
    },
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(About);
  1. connect()函数是一个高阶函数接收两个参数,connect 函数返回一个高阶组件。最后一个括号是放组件的
  2. 使用了 connect 函数。mapStateToProps 把 store 上的数据映射到了 props。组件通过 props 拿到。action 也是如此。类似 vue 中的 mapState 辅助函数,mapAction 辅助函数

20.7 派发异步的 action

  • redux-thunk 是可以发送异步的请求**,默认情况下的 dispatch(action),action 需要是一个 JavaScript 的对象**;

  • redux-thunk 可以让 dispatch(action 函数),action 可以是一个函数;

  • 该函数会被调用,并且会传给这个函数一个 dispatch 函数和 getState 函数

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

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

  • 使用 redux-thunk

    yarn add redux-thunk

    import { createStore, applyMiddleware } from "redux";
    import thunk from "redux-thunk";
    import reducer from "./reducer.js";
    
    const store = createStore(reducer, applyMiddleware(thunk));
    
    export default store;
    

**案例代码:异步 action:**组件中通过 props 调用 action 发情网络请求

import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { fetchHomeMultidataAction } from "../store/actionCreators";

export class Category extends PureComponent {
  componentDidMount() {
    this.props.fetchHomeMultidata();
  }
  render() {
    return <h2>Category Page: {this.props.counter}</h2>;
  }
}

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

const mapDispatchToProps = (dispatch) => {
  return {
    fetchHomeMultidata: () => {
      dispatch(fetchHomeMultidataAction());
    },
  };
};

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

actionCreators.js 中:

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

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

export const fetchHomeMultidataAction = () => {
  // 普通action
  /* 
    如果是一个普通action,那么我们这里需要返回一个action对象
    问题:对象是不能直接拿到服务器请求的异步数据的
    return {}
  */

  // 异步action处理返回函数
  /* 
    如果返回一个函数那么redux是不支持的。需要借助插件redux-thunk
  */
  return (dispatch, getState) => {
    //异步操作
    axios.get("http://xxxxxx/home/multidata").then((res) => {
      const banners = res.data.data.banner.list;
      const recommends = res.data.data.recommend.list;

      // 派发action
      dispatch(changeBannersAction(banners));
      dispatch(changeRecommendsAction(recommends));
    });
  };
};

组件中 mapStateToProps 函数直接获取即可。

20.8 redux-devtools

  • 第一步:在对应的浏览器中安装相关的插件(比如 Chrome 浏览器扩展商店中搜索 Redux DevTools 即可)
  • 第二步:在 redux 中继承 devtools 的中间件;
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import reducer from "./reducer.js";

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));

export default store;

20.9 Redux 模块化

**combineReducers****函数实现模块化**

  • combineReducers 是如何实现?
  • 事实上,它也是将我们传入的 reducers 合并到一个对象中,最终返回一个 combination 的函数(相当于我们之前的 reducer 函 数了);
  • 执行 combination 函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的 state 还是新的 state
  • 新的 state 会触发订阅者发生对应的刷新,而旧的 state 可以有效的组织订阅者发生刷新

store/index.js

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

import counterReducer from "./counter";
import homeReducer from "./home";

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

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));

export default store;

在这里插入图片描述
在这里插入图片描述

combineReducers实现原理

import counterReducer from "./counter";
import homeReducer from "./home";

// combineReducers实现原理(了解)
function reducer(state = {}, action) {
  // 返回一个对象, store的state
  return {
    //state.counter 第一次undefined 就拿到默认值
    counter: counterReducer(state.counter, action),
    home: homeReducer(state.home, action),
  };
}

21.ReduxToolkit

  • Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。
  • 在前面我们学习 Redux 的时候应该已经发现,redux 的编写逻辑过于的繁琐和麻烦。
  • 并且代码通常分拆在多个文件中(虽然也可以放到一个文件管理,但是代码量过多,不利于管理);
  • Redux Toolkit 包旨在成为编写 Redux 逻辑的标准方式,从而解决上面提到的问题;
  • 在很多地方为了称呼方便,也将之称为“RTK”;

安装 Redux Toolkit:

npm install @reduxjs/toolkit react-redux

  • Redux Toolkit 的核心 API 主要是如下几个:
    • configureStore:包装 createStore 以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供 的任何 Redux 中间件,redux-thunk 默认包含,并启用 Redux DevTools Extension。
    • createSlice:接受 reducer 函数的对象、切片名称和初始状态值,并自动生成切片 reducer,并带有相应的 actions。
    • createAsyncThunk: 接受一个动作类型字符串和一个返回承诺的函数,并生成一个 pending/fulfilled/rejected 基于该承诺分 派动作类型的 thunk

21.Redux Toolkit 使用流程

  1. store 的创建

    • configureStore 用于创建 store 对象,常见参数如下:
    • reducer,将 slice 中的 reducer 可以组成一个对象传入此处;
    • middleware:可以使用参数,传入其他的中间件(自行了解);
    • devTools:是否配置 devTools 工具,默认为 true;
    import { configureStore } from "@reduxjs/toolkit";
    
    import counterReducer from "./features/counter";
    import homeReducer from "./features/home";
    
    const store = configureStore({
      reducer: {
        counter: counterReducer,
        home: homeReducer,
      },
    });
    
    export default store;
    
  2. 通过 createSlice 创建一个 slice。

  • createSlice 主要包含如下几个参数:
    • name:用户标记 slice 的名词, 在之后的 redux-devtool 中会显示对应的名词;
    • initialState:初始化值 ,第一次初始化时的值;
    • reducers:相当于之前的 reducer 函数
    • 对象类型,并且可以添加很多的函数;
    • 函数类似于 redux 原来 reducer 中的一个 case 语句;
    • 函数的参数:
    • 参数一:state
    • 参数二:调用这个 action 时,传递的 action 参数;
    • createSlice 返回值是一个对象,包含所有的 actions;

store/features/counter.js(counter 的 reducer)

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

const counterSlice = createSlice({
  name: "counter",
  initialState: {
    counter: 999,
  },
  reducers: {
    addNumber(state, { payload }) {
      console.log(payload);
      state.counter = state.counter + payload;
    },

    subNumber(state, { payload }) {
      state.counter = state.counter - payload;
    },
  },
});

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

export default counterSlice.reducer;

组件中展示数据:(这部分热 redux 一样)

import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { addNumber } from "../store/features/counter";

export class About extends PureComponent {
  render() {
    const { counter } = this.props;
    return (
      <div>
        <h2>About Page: {counter}</h2>
      </div>
    );
  }
}

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

const mapDispatchToProps = (dispatch) => {
  return {
    addNumber: (num) => {
      dispatch(addNumber(num));
    },
  };
};

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

21.2 Toolkit 的异步操作

  • 在之前 redux-thunk 中间件让 dispatch 中可以进行异步操作
  • Redux Toolkit 默认已经给我们继承了 Thunk 相关的功能:createAsyncThunk

当 createAsyncThunk 创建出来的 action 被 dispatch 时,会存在三种状态:

  1. pending:action 被发出,但是还没有最终的结果;
  2. fulfilled:获取到最终的结果(有返回值的结果);
  3. rejected:执行过程中有错误或者抛出了异常;
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
// 1.
export const fetchHomeMultidataAction = createAsyncThunk("fetch/homemultidata", async () => {
  const res = await axios.get("http://123.207.32.32:8000/home/multidata");
  return res.data;
});

const homeSlice = createSlice({
  name: "home",
  initialState: {
    banners: [],
    recommends: [],
  },
  reducers: {
    changeBanners(state, { payload }) {
      state.banners = payload;
    },
    changeRecommends(state, { payload }) {
      state.recommends = payload;
    },
  },
  //2.
  extraReducers: {
    [fetchHomeMultidataAction.pending](state, action) {
      console.log("fetchHomeMultidataAction pending");
    },
    [fetchHomeMultidataAction.fulfilled](state, { payload }) {
      state.banners = payload.data.banner.list;
      state.recommends = payload.data.recommend.list;
    },
    [fetchHomeMultidataAction.rejected](state, action) {
      console.log("fetchHomeMultidataAction rejected");
    },
  },
});

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

extraReducers第二种写法(函数式写法)

可以向 builder 中添加 case 来监听异步操作的结果

extraReducers: (builder) => {
  builder
    .addCase(fetchHomeMultidataAction.pending, (state, action) => {
      console.log("fetchHomeMultidataAction-pending");
    })
    .addCase(fetchHomeMultidataAction.fulfilled, (state, { payload }) => {
      state.banners = payload.data.banner.list;
      state.recommends = payload.data.recommend.list;
    });
};

函数式写法还有一个用法

在这里插入图片描述

22.React Router

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

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

    npm install react-router-dom

22.1 BrowserRouter 或 HashRouter

  • BrowserRouter 使用 history 模式;
  • HashRouter 使用 hash 模式;
root.render(
  <HashRouter>
    <App />
  </HashRouter>
);

22.2 路由映射配置

  • Routes:包裹所有的 Route,在其中匹配一个路由
    • Router5.x 使用的是 Switch 组件
  • Route:Route 用于路径的匹配;
  • path 属性:用于设置匹配到的路径;
  • element 属性:设置匹配到路径后,渲染的组件;
    • Router5.x 使用的是 component 属性
  • exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;
  • Router6.x 不再支持该属性
<Routes>
  <Route path="/home" element={<Home />}>
    <Route path="/home/recommend" element={<HomeRecommend />} />
  </Route>
</Routes>

22.3 路由跳转

  1. Link

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

<Link to="/home">首页</Link>
<Link to="/about">关于</Link>
  1. NavLink

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

  • 我们要使用 NavLink 组件来替代 Link 组件:
  • style:传入函数,函数接受一个对象,包含 isActive 属性
  • className:传入函数,函数接受一个对象,包含 isActive 属性
<NavLink to="/home" style={({isActive}) => ({color: isActive ? "red": ""})}>首页</NavLink>
<NavLink to="/about" style={({isActive}) => ({color:  isActive ? "red": ""})}>关于</NavLink>

默认的 activeClassName: 事实上在默认匹配成功时,NavLink 就会添加上一个动态的 active class; 所以我们也可以直接编写样式

如果你担心这个 class 在其他地方被使用了,出现样式的层叠,也可以自定义 class

 <NavLink to="/home" className={({isActive}) => isActive?"link-active":""}>首页</NavLink>
 <NavLink to="/about" className={({isActive}) => isActive?"link-active":""}>关于</NavLink>
  1. Navigate

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

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

23.4Not Found 页面配置

  • 开发一个 Not Found 页面;
  • 配置对应的 Route,并且设置 path 为*即可;
<Route path="*" element={<NotFound />} />

23.5 路由的嵌套

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

23.6 手动路由的跳转

  • 跳转主要是通过 Link 或者 NavLink 进行跳转的,实际上我们也可以通过 JavaScript 代码进行跳转。
  • Navigate 组件是可以进行路由的跳转的,但是依然是组件的方式。
  • 如果我们希望通过 JavaScript 代码逻辑进行跳转(比如点击了一个 button),那么就需要获取到 navigate 对象。
  1. 函数式使用 hooks 跳转

    export function App(props) {
      const navigate = useNavigate();
    
      function navigateTo(path) {
        navigate(path);
      }
    
      return (
        <div className="app">
          <div className="nav">
            <button onClick={(e) => navigateTo("/category")}>分类</button>
            <span onClick={(e) => navigateTo("/order")}>订单</span>
          </div>
          <div className="content">
            <Routes>
              <Route path="/category" element={<Category />} />
              <Route path="/order" element={<Order />} />
            </Routes>
          </div>
        </div>
      );
    }
    
  2. 类路由跳转(必须使用高阶组件封装才行,第一点实现)

    import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
    
    // 高阶组件: 函数
    function withRouter(WrapperComponent) {
      return function (props) {
        // 1.导航
        const navigate = useNavigate();
    
        // 2.动态路由的参数: /detail/:id
        const params = useParams();
    
        // 3.查询字符串的参数: /user?name=why&age=18
        const location = useLocation();
        const [searchParams] = useSearchParams();
        const query = Object.fromEntries(searchParams);
    
        const router = { navigate, params, location, query };
    
        return <WrapperComponent {...props} router={router} />;
      };
    }
    
    export default withRouter;
    

    使用时候:

    import { withRouter } from "../hoc";
    
    export class Home extends PureComponent {
      navigateTo(path) {
        const { navigate } = this.props.router;
        navigate(path);
      }
    
      render() {
        return (
          <div>
            <button onClick={(e) => this.navigateTo("/home/songmenu")}>歌单</button>
    
            {/* 占位的组件 */}
            <Outlet />
          </div>
        );
      }
    }
    
    export default withRouter(Home);
    

23.7 路由参数传递

传递参数有二种方式:

  1. 动态路由的方式;
  2. search 传递参数;
  • 比如/detail 的 path 对应一个组件 Detail;
  • 如果我们将 path 在 Route 匹配时写成/detail/:id,那么 /detail/abc、/detail/123 都可以匹配到该 Route,并且进行显示;
  • 这个匹配规则,我们就称之为动态路由;
  • 使用动态路由可以为路由传递参数
  1. search 传递参数

在这里插入图片描述

高阶组件:

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

// 高阶组件: 函数
function withRouter(WrapperComponent) {
  return function (props) {
    // 1.导航
    const navigate = useNavigate();

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

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

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

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

export default withRouter;
  1. search 传递参数

在这里插入图片描述

23.8 路由的配置文件(包含路由懒加载)

  • 目前路由定义都是直接使用 Route 组件,并且添加属性来完成的。
  • 但是这样的方式会让路由变得非常混乱,我们希望将所有的路由配置放到一个地方进行集中管理:
  • 在早期的时候,Router 并且没有提供相关的 API,我们需要借助于 react-router-config 完成;
  • 在 Router6.x 中,为我们提供了 useRoutes API 可以完成相关的配置;
<div className="content">{useRoutes(routes)}</div>

类似 vue

路由表:

import Home from "../pages/Home";
import HomeRecommend from "../pages/HomeRecommend";
import HomeRanking from "../pages/HomeRanking";
import HomeSongMenu from "../pages/HomeSongMenu";
// import About from "../pages/About"
// import Login from "../pages/Login"
import Category from "../pages/Category";
import Order from "../pages/Order";
import NotFound from "../pages/NotFound";
import Detail from "../pages/Detail";
import User from "../pages/User";
import { Navigate } from "react-router-dom";
import React from "react";

const About = React.lazy(() => import("../pages/About"));
const Login = React.lazy(() => import("../pages/Login"));

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: "/home/songmenu",
        element: <HomeSongMenu />,
      },
    ],
  },
  {
    path: "/about",
    element: <About />,
  },
  {
    path: "/login",
    element: <Login />,
  },
  {
    path: "/category",
    element: <Category />,
  },
  {
    path: "/order",
    element: <Order />,
  },
  {
    path: "/detail/:id",
    element: <Detail />,
  },
  {
    path: "/user",
    element: <User />,
  },
  {
    path: "*",
    element: <NotFound />,
  },
];

export default routes;

路由懒加载:

在这里插入图片描述

23.Hooks

  • 总结一下 hooks:
    • 它可以让我们在不编写 class 的情况下使用 state 以及其他的 React 特性;
    • 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决
  • Hook 的使用场景:
  • Hook 的出现基本可以代替我们之前所有使用 class 组件的地方;
  • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为 Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
  • Hook 只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用
  • 请记住 Hook 是:
    • 完全可选的:你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook。
    • 100% 向后兼容的:Hook 不包含任何破坏性改动。
    • 现在可用:Hook 已发布于 v16.8.0。

23.1 useState Hook

  • 参数:初始化值,如果不设置为 undefined;

  • 返回值:数组,包含两个元素;

    • 元素一:当前状态的值(第一调用为初始化值);
    • 元素二:设置状态值的函数;
  • useState 的返回值是一个数组,我们可以通过数组的解构,来完成赋值会非常方便

  • 也可以在一个组件中定义多个变量和复杂变量(数组、对象)

  • 使用它们会有两个额外的规则:

    • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
    • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。
import React, { memo, useState } from "react";

const App = memo(() => {
  const [message, setMessage] = useState("zimo");

  function changeMessage() {
    setMessage("紫陌");
  }
  return (
    <div>
      <h2>App:{message}</h2>
      <button onClick={changeMessage}>修改文本</button>
    </div>
  );
});

export default App;

23.1 useEffect Hook

  • useEffect Hook 可以让你来完成一些类似于 class 中生命周期的功能;
  • 类似于网络请求、手动更新 DOM、一些事件的监听,都是 React 更新 DOM 的一些副作用(Side Effects);
  • 所以对于完成这些功能的 Hook 被称之为 Effect Hook;
  1. useEffect 的解析
  • 通过 useEffect 的 Hook,可以告诉 React 需要在渲染后执行某些操作;
  • useEffect 要求我们传入一个回调函数,在React 执行完更新 DOM 操作之后,就会回调这个函数;
  • 默认情况下,**无论是第一次渲染之后,**还是每次更新之后,都会执行这个 回调函数
  1. 需要清除 Effect
  • 在 class 组件的编写过程中,某些副作用的代码,我们需要在 componentWillUnmount 中进行清除:
  • 比如我们之前的事件总线或 Redux 中手动调用 subscribe;
  • 都需要在 componentWillUnmount 有对应的取消订阅;

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

  • 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数;
  • 如此可以将添加和移除订阅的逻辑放在一起;
  • 它们都属于 effect 的一部分;

React 何时清除 effect?

  • React 会在组件更新和卸载的时候执行清除操作;
  • Effect 在每次渲染的时候都会执行;
import React, { memo, useEffect, useState } from "react";

const App = memo(() => {
  const [count, setCount] = useState(0);

  // 负责告知React,在执行完当前组件渲染之后要执行的副作用代码
  useEffect(() => {
    console.log("监听redux中数据变化, 监听eventBus中的zimo事件");

    // 返回值:回调函数 => 组件被重新渲染或者组件卸载的时候执行
    return () => {
      console.log("取消监听redux中数据变化, 取消监听eventBus中的zimo事件");
    };
  });

  return (
    <div>
      <button onClick={(e) => setCount(count + 1)}>+1({count})</button>
    </div>
  );
});

export default App;
  1. 多个 Effect
  • 使用 Effect Hook,我们可以将它们分离到不同的 useEffect 中:
  • React 将按照 effect 声明的顺序依次调用组件中的每一个 effect
import React, { memo, useEffect } from "react";
import { useState } from "react";

const App = memo(() => {
  const [count, setCount] = useState(0);

  // 负责告知react, 在执行完当前组件渲染之后要执行的副作用代码
  useEffect(() => {
    // 1.修改document的title(1行)
    console.log("修改title");
  });

  // 一个函数式组件中, 可以存在多个useEffect
  useEffect(() => {
    // 2.对redux中数据变化监听(10行)
    console.log("监听redux中的数据");
    return () => {
      // 取消redux中数据的监听
    };
  });

  useEffect(() => {
    // 3.监听eventBus中的why事件(15行)
    console.log("监听eventBus的zimo事件");
    return () => {
      // 取消eventBus中的zimo事件监听
    };
  });

  return (
    <div>
      <button onClick={(e) => setCount(count + 1)}>+1({count})</button>
    </div>
  );
});

export default App;
  1. Effect 性能优化
  • 默认情况下,useEffect 的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:
  • 某些代码我们只是希望执行一次即可,类似于 componentDidMount 和 componentWillUnmount 中完成的事情;(比如网 络请求、订阅和取消订阅);
  • 多次执行也会导致一定的性能问题;

useEffect实际上有两个参数:

  • 参数一:执行的回调函数
  • 参数二:该 useEffect 在哪些 state 发生变化时,才重新执行;(受谁的影响)

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

  • 这里的两个回调函数分别对应的就是 componentDidMount 和 componentWillUnmount 生命周期函数了;
import React, { memo, useEffect } from "react";
import { useState } from "react";

const App = memo(() => {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState("Hello World");

  useEffect(() => {
    console.log("修改title:", count);
  }, [count]);

  useEffect(() => {
    console.log("监听redux中的数据");
    return () => {};
  }, []);

  useEffect(() => {
    console.log("监听eventBus的why事件");
    return () => {};
  }, []);

  useEffect(() => {
    console.log("发送网络请求, 从服务器获取数据");

    return () => {
      console.log("会在组件被卸载时, 才会执行一次");
    };
  }, []);

  return (
    <div>
      <button onClick={(e) => setCount(count + 1)}>+1({count})</button>
      <button onClick={(e) => setMessage("你好啊")}>修改message({message})</button>
    </div>
  );
});

export default App;

23.3 useContext

在组件中使用共享的 Context 有两种方式

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

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

  • Context Hook 允许我们通过 Hook 来直接获取某个 Context 的值;
index.js-------------------------------------------------------------------------------

root.render(
  <UserContext.Provider value={{name: "zimo", level: 99}}>
    <ThemeContext.Provider value={{color:"red",size:18}}>
        <App />
    </ThemeContext.Provider>
  </UserContext.Provider>
)

函数组件中---------------------------------------------------------------------------

import React, { memo, useContext } from 'react'
import { UserContext, ThemeContext } from "./context"

const App = memo(() => {
  // 使用Context
  const user = useContext(UserContext)
  const theme = useContext(ThemeContext)

  return (
    <div>
      <h2>User: {user.name}-{user.level}</h2>
      <h2 style={{color: theme.color, fontSize: theme.size}}>Theme</h2>
    </div>
  )
})

export default App

当组件上层最近的 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 context value 值。

23.4 useReducer

  • 很多人看到 useReducer 的第一反应应该是 redux 的某个替代品,其实并不是。
  • useReducer 仅仅是 useState 的一种替代方案:
  • 在某些场景下,如果 state 的处理逻辑比较复杂,我们可以通过 useReducer 来对其进行拆分;
  • 或者这次修改的 state 需要依赖之前的 state 时,也可以使用;

注释是 useState 写法

import React, { memo, useReducer } from "react";
// import { useState } from 'react'

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { ...state, counter: state.counter + 1 };
    case "decrement":
      return { ...state, counter: state.counter - 1 };
    case "add_number":
      return { ...state, counter: state.counter + action.num };
    case "sub_number":
      return { ...state, counter: state.counter - action.num };
    default:
      return state;
  }
}

// useReducer+Context => redux

const App = memo(() => {
  // const [count, setCount] = useState(0)
  const [state, dispatch] = useReducer(reducer, { counter: 0, friends: [], user: {} });

  // const [counter, setCounter] = useState()
  // const [friends, setFriends] = useState()
  // const [user, setUser] = useState()

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

      <h2>当前计数: {state.counter}</h2>
      <button onClick={(e) => dispatch({ type: "increment" })}>+1</button>
      <button onClick={(e) => dispatch({ type: "decrement" })}>-1</button>
      <button onClick={(e) => dispatch({ type: "add_number", num: 5 })}>+5</button>
      <button onClick={(e) => dispatch({ type: "sub_number", num: 5 })}>-5</button>
      <button onClick={(e) => dispatch({ type: "add_number", num: 100 })}>+100</button>
    </div>
  );
});

export default App;

23.5 useCallback

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

  • useCallback 会返回一个函数的 memoized(记忆的) 值;
  • 在依赖不变的情况下,多次定义的时候,返回的值是相同的
  • 通常使用 useCallback 的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存;
import React, { memo, useState, useCallback, useRef } from "react";

// useCallback性能优化的点:
// 1.当需要将一个函数传递给子组件时, 最好使用useCallback进行优化, 将优化之后的函数, 传递给子组件

// props中的属性发生改变时, 组件本身就会被重新渲染
const Home = memo(function (props) {
  const { increment } = props;
  console.log("Home被渲染");
  return (
    <div>
      <button onClick={increment}>increment+1</button>

      {/* 100个子组件 */}
    </div>
  );
});

const App = memo(function () {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState("hello");

  // 闭包陷阱: useCallback
  // const increment = useCallback(function foo() {
  //   console.log("increment")
  //   setCount(count+1)
  // }, [count])

  // 进一步的优化: 当count发生改变时, 也使用同一个函数(了解)
  // 做法一: 将count依赖移除掉, 缺点: 闭包陷阱
  // 做法二: useRef, 在组件多次渲染时, 返回的是同一个值
  const countRef = useRef();
  countRef.current = count;
  const increment = useCallback(function foo() {
    console.log("increment");
    setCount(countRef.current + 1);
  }, []);

  // 普通的函数
  // const increment = () => {
  //   setCount(count+1)
  // }

  return (
    <div>
      <h2>计数: {count}</h2>
      <button onClick={increment}>+1</button>

      <Home increment={increment} />

      <h2>message:{message}</h2>
      <button onClick={(e) => setMessage(Math.random())}>修改message</button>
    </div>
  );
});

export default App;

当需要将一个函数传递给子组件时, 最好使用 useCallback 进行优化, 将优化之后的函数, 传递给子组件!

优化体现在传递子组件时。 父组件重新渲染都会重新定义函数体现不到优化。所以体现在子组件。函数传给子组件,子组件重新渲染不会重新定义父组件的函数。

23.6 useMemo

  • useMemo 实际的目的也是为了进行性能的优化。
    • useMemo 返回的也是一个 memoized(记忆的) 值;
    • 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
import React, { memo } from "react";
import { useMemo, useState } from "react";

const HelloWorld = memo(function (props) {
  console.log("HelloWorld被渲染~");
  return <h2>Hello World</h2>;
});

function calcNumTotal(num) {
  // console.log("calcNumTotal的计算过程被调用~")
  let total = 0;
  for (let i = 1; i <= num; i++) {
    total += i;
  }
  return total;
}

const App = memo(() => {
  const [count, setCount] = useState(0);

  // const result = calcNumTotal(50)

  // 1.不依赖任何的值, 进行计算
  const result = useMemo(() => {
    return calcNumTotal(50);
  }, []);

  // 2.依赖count
  // const result = useMemo(() => {
  //   return calcNumTotal(count*2)
  // }, [count])

  // 3.使用useMemo对子组件渲染进行优化
  const info = useMemo(() => ({ name: "why", age: 18 }), []);

  return (
    <div>
      <h2>计算结果: {result}</h2>
      <h2>计数器: {count}</h2>
      <button onClick={(e) => setCount(count + 1)}>+1</button>

      <HelloWorld result={result} info={info} />
    </div>
  );
});

export default App;

useMemo 是给子组件传一个值的。传引用型才有优化 {} []。普通的值没有优化。因为对象和数组会重新声明。

23.7 useRef

  1. 用法一:引入 DOM(或者组件,但是需要是 class 组件)元素
import React, { memo, useRef } from "react";

const App = memo(() => {
  const titleRef = useRef();
  const inputRef = useRef();

  function showTitleDom() {
    console.log(titleRef.current);
    inputRef.current.focus();
  }

  return (
    <div>
      <h2 ref={titleRef}>Hello,zimo</h2>
      <input type="text" ref={inputRef} />
      <button onClick={showTitleDom}>查看title的DOM</button>
    </div>
  );
});

export default App;
  1. 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变
import React, { memo, useRef, useState, useCallback } from "react";

let obj = null;

const App = memo(() => {
  const [count, setCount] = useState(0);
  const nameRef = useRef();
  console.log(obj === nameRef); //true
  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;

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

28.8 useImperativeHandle

  • 回顾一下 ref 和 forwardRef 结合使用:
    • 通过 forwardRef 可以将 ref 转发到子组件;
    • 子组件拿到父组件中创建的 ref,绑定到自己的某一个元素中;
  • forwardRef 的做法本身没有什么问题,但是我们是将子组件的 DOM 直接暴露给了父组件:
  • 直接暴露给父组件带来的问题是某些情况的不可控;
  • 父组件可以拿到 DOM 后进行任意的操作;
import React, { memo, useRef, forwardRef, useImperativeHandle } from "react";

const HelloWorld = memo(
  forwardRef((props, ref) => {
    const inputRef = useRef();

    // 子组件对父组件传入的ref进行处理
    useImperativeHandle(ref, () => {
      return {
        focus() {
          console.log("focus");
          inputRef.current.focus();
        },
        setValue(value) {
          inputRef.current.value = value;
        },
      };
    });

    return <input type="text" ref={inputRef} />;
  })
);

const App = memo(() => {
  const titleRef = useRef();
  const inputRef = useRef();

  function handleDOM() {
    // console.log(inputRef.current)
    inputRef.current.focus();
    // inputRef.current.value = ""
    inputRef.current.setValue("哈哈哈");
  }

  return (
    <div>
      <h2 ref={titleRef}>哈哈哈</h2>
      <HelloWorld ref={inputRef} />
      <button onClick={handleDOM}>DOM操作</button>
    </div>
  );
});

export default App;
  • 上面的案例中,我们只是希望父组件可以操作的 focus,其他并不希望它随意操作;
    • 通过 useImperativeHandle 可以值暴露固定的操作:
    • 通过 useImperativeHandle 的 Hook,将传入的 ref 和 useImperativeHandle 第二个参数返回的对象绑定到了一起;
    • 所以在父组件中,使用 inputRef.current 时,实际上使用的是返回的对象;
    • 比如我调用了 focus 函数,甚至可以调用 printHello 函数;

23.9 useLayoutEffect

  • useLayoutEffect 看起来和 useEffect 非常的相似,事实上他们也只有一点区别而已:
    • useEffect 会在渲染的内容更新到 DOM 上后执行,不会阻塞 DOM 的更新;
    • useLayoutEffect 会在渲染的内容更新到 DOM 上之前执行,会阻塞 DOM 的更新;
  • 如果我们希望在某些操作发生之后再更新 DOM,那么应该将这个操作放到 useLayoutEffect。
import React, { memo, useEffect, useLayoutEffect, useState } from "react";

const App = memo(() => {
  const [count, setCount] = useState(100);

  useLayoutEffect(() => {
    console.log("useLayoutEffect");
    if (count === 0) {
      setCount(Math.random() + 99);
    }
  });

  console.log("App render");

  return (
    <div>
      <h2>count: {count}</h2>
      <button onClick={(e) => setCount(0)}>设置为0</button>
    </div>
  );
});

export default App;

官方更推荐使用 useEffect 而不是 useLayoutEffect。

23.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;

localStorage 数据存储

import { useEffect, useState } from "react";

function useLocalStorage(key) {
  // 1.从localStorage中获取数据, 并且数据数据创建组件的state
  const [data, setData] = useState(() => {
    const item = localStorage.getItem(key);
    if (!item) return "";
    return JSON.parse(item);
  });

  // 2.监听data改变, 一旦发生改变就存储data最新值
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(data));
  }, [data]);

  // 3.将data/setData的操作返回给组件, 让组件可以使用和修改值
  return [data, setData];
}

export default useLocalStorage;

23.11 redux hooks(useDispatch useSelector)

  • 在之前的 redux 开发中,为了让组件和 redux 结合起来,我们使用了 react-redux 中的 connect:

    • 但是这种方式必须使用高阶函数结合返回的高阶组件;
    • 并且必须编写:mapStateToProps 和 mapDispatchToProps 映射的函数;
  • 在 Redux7.1 开始,提供了 Hook 的方式,我们再也不需要编写 connect 以及对应的映射函数了

  • useSelector 的作用是将 state 映射到组件中:

  • 参数一:将 state 映射到需要的数据中;

  • 参数二:可以进行比较来决定是否组件重新渲染;

  • useDispatch 非常简单,就是直接获取 dispatch 函数,之后在组件中直接使用即可;

  • 我们还可以通过 useStore 来获取当前的 store 对象;

import React, { memo } from "react";
import { useSelector, useDispatch, shallowEqual } from "react-redux";
import { addNumberAction, changeMessageAction, subNumberAction } from "./store/modules/counter";

// memo高阶组件包裹起来的组件有对应的特点: 只有props发生改变时, 才会重新渲染
const Home = memo((props) => {
  // 1.使用useSelector将redux中store的数据映射到组件内
  const { message } = useSelector(
    (state) => ({
      message: state.counter.message,
    }),
    shallowEqual
  );

  const dispatch = useDispatch();
  function changeMessageHandle() {
    // 2.使用dispatch直接派发action
    dispatch(changeMessageAction("你好啊, zimo!"));
  }

  console.log("Home render");

  return (
    <div>
      <h2>Home: {message}</h2>
      <button onClick={(e) => changeMessageHandle()}>修改message</button>
    </div>
  );
});

const App = memo((props) => {
  // 1.使用useSelector将redux中store的数据映射到组件内
  const { count } = useSelector(
    (state) => ({
      count: state.counter.count,
    }),
    shallowEqual
  );

  // 2.使用dispatch直接派发action
  const dispatch = useDispatch();
  function addNumberHandle(num, isAdd = true) {
    if (isAdd) {
      dispatch(addNumberAction(num));
    } else {
      dispatch(subNumberAction(num));
    }
  }

  console.log("App render");

  return (
    <div>
      <h2>当前计数: {count}</h2>
      <button onClick={(e) => addNumberHandle(1)}>+1</button>
      <button onClick={(e) => addNumberHandle(6)}>+6</button>
      <button onClick={(e) => addNumberHandle(6, false)}>-6</button>

      <Home />
    </div>
  );
});

export default App;
文章来源:https://blog.csdn.net/weixin_57677300/article/details/135209835
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。