React脚手架和组件化学习

157 阅读18分钟

三、React脚手架解析

前端工程的复杂化

如果我们只是开发几个小的demo程序,那么永远不需要考虑一些复杂的问题:

  • 比如目录结构如何组织划分;
  • 比如如何管理文件之间的相互依赖;
  • 比如如何管理第三方模块的依赖;
  • 比如项目发布前如何压缩、打包项目;

现代的前端项目已经越来越复杂了:

  • 不会再是在HTML中引入几个css文件,引入几个编写的js文件或者第三方的js文件这么简单;
  • 比如css可能是使用less、sass等预处理器进行编写,我们需要将它们转成普通的css才能被浏览器解析;
  • 比如JavaScript代码不再只是编写在几个文件中,而是通过模块化的方式,被组成在成百上千个文件中,我们需要通过模块化的技术来管理它们之间的相互依赖;
  • 比如项目需要依赖很多的第三方库,如何更好的管理它们(比如管理它们的依赖、版本升级等);

为了解决上面这些问题,我们需要再去学习一些工具:

  • 比如babel、webpack、gulp,配置它们转换规则、打包依赖、热更新等等一些的内容;
  • 脚手架的出现,就是帮助我们解决这一系列问题的;

脚手架是什么呢?

编程中提到的脚手架(Scaffold),其实是一种工具,帮我们可以快速生成项目的工程化结构;

  • 每个项目作出完成的效果不同,但是它们的基本工程化结构是相似的;
  • 既然相似,就没有必要每次都从零开始搭建,完全可以使用一些工具,帮助我们生产基本的工程化模板;
  • 不同的项目,在这个模板的基础之上进行项目开发或者进行一些配置的简单修改即可;
  • 这样也可以间接保证项目的基本机构一致性,方便后期的维护;

◼ 总结:脚手架让项目从搭建到开发,再到部署,整个流程变得快速和便捷;

创建React项目

  • NPM下載create-react-app包

  • 现在,我们就可以通过脚手架来创建React项目了。

  • 创建React项目的命令如下:

    • 注意:项目名称不能包含大写字母
    • 另外还有更多创建项目的方式,可以参考GitHub的readm
  • 创建完成后,进入对应的目录,就可以将项目跑起来:

  • 创建React项目

    create-react-app 项目名称

    cd 01-test-react

    yarn start

目录分析

image-20221017151647923

了解PWA

整个目录结构都非常好理解,只是有一个PWA相关的概念:

  • PWA全称Progressive Web App,即渐进式WEB应用;
  • 一个 PWA 应用首先是一个网页, 可以通过 Web 技术编写出一个网页应用;
  • 随后添加上 App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能;
  • 这种Web存在的形式,我们也称之为是 Web App;

PWA解决了哪些问题呢?

  • 可以添加至主屏幕,点击主屏幕图标可以实现启动动画以及隐藏地址栏;
  • 实现离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能;
  • 实现了消息推送;
  • 等等一系列类似于Native App相关的功能;

脚手架中的webpack

React脚手架默认是基于Webpack来开发的;

但是,很奇怪:我们并没有在目录结构中看到任何webpack相关的内容?

  • 原因是React脚手架将webpack相关的配置隐藏起来了(其实从Vue CLI3开始,也是进行了隐藏);

如果我们希望看到webpack的配置信息,应该怎么来做呢?

  • 我们可以执行一个package.json文件中的一个脚本:"eject": "react-scripts eject"
  • 这个操作是不可逆的,所以在执行过程中会给与我们提示;

脚手架中的webpack

yarn eject

image-20221017152009384

开始编写代码

在src目录下,创建一个index.js文件,因为这是webpack打包的入口。

在index.js中开始编写React代码:

  • 我们会发现和写的代码是逻辑是一致的;
  • 只是在模块化开发中,我们需要手动的来导入React、ReactDOM,因为它们都是在我们安装的模块中;
import React from "react"
import ReactDOM from "react-dom/client";
// 编写React代码,并且通过React渲染出来对应的内容import App from "./App";
​
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App />)

如果我们不希望直接在 root.render 中编写过多的代码,就可以单独抽取一个组件App.js:

import React from "react";
import HelloWorld from "./Component/HelloWorld";
// 编写一个组件
class App extends React.Component {
  constructor() {
    super();
    this.state = {
      message: "Hello React",
    };
  }
​
  render() {
    const { message } = this.state;
​
    return (
      <div>
        <h2>{message}</h2>
        <HelloWorld></HelloWorld>
      </div>
    );
  }
}
export default App;

四、React组件学习

组件化开发概念

  • 组件化是一种分而治之的思想:

    • 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理及扩展。
    • 但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。
  • 我们需要通过组件化的思想来思考整个应用程序:

    • 我们将一个完整的页面分成很多个组件;
    • 每个组件都用于实现页面的一个功能块;
    • 而每一个组件又可以进行细分;
    • 而组件本身又可以在多个地方进行复用;

image-20221023161833715

  • 组件化是React的核心思想,也是我们后续课程的重点,前面我们封装的App本身就是一个组件:

    • 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。
    • 任何的应用都会被抽象成一颗组件树。

React的组件化

  • React的组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件:

    • 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component);
    • 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component);
    • 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component);
  • 这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离:

    • 函数组件、无状态组件、展示型组件主要关注UI的展示;
    • 类组件、有状态组件、容器型组件主要关注数据逻辑;

类组件

类组件的定义有如下要求:

  • 组件的名称是大写字符开头

    • 类组件需要继承React.Component
    • 类组件必须实现render函数
  • 在ES6之前,可以通过create-react-class 模块来定义类组件,但是目前官网建议我们使用ES6的class类定义。

  • 使用class定义一个组件:

    • constructor是可选的,我们通常在constructor中初始化一些数据;
    • this.state中维护的就是我们组件内部的数据;
    • render() 方法是 class 组件中唯一必须实现的方法;

Render 函数的返回值

  • 当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:

  • React 元素:

    • 通常通过 JSX 创建。
    • 例如,<div / > 会被 React 渲染为 DOM 节点,<MyComponent / > 会被 React 渲染为自定义组件;
    • 无论是 <div / > 还是 <MyComponent / > 均为 React 元素。
  • 数组或 fragments:使得 render 方法可以返回多个元素。

  • Portals:可以渲染子节点到不同的 DOM 子树中。

  • 字符串或数值类型:它们在 DOM 中会被渲染为文本节点

  • 布尔类型或 null:什么都不渲染。

import React, { Component } from "react";
​
// 1.类组件
export class App extends Component {
  constructor() {
    super();
    this.state = {
      message: "Hello Component",
    };
  }
  render() {
    const { message } = this.state;
    /* 1.React元素:通过jsx编译的代码就会被编译成React.createElement,所以返回的就是一个React元素 */
    /* return <h2>{message}</h2> */
​
    /* 2.组件或者fragments */
    // return ["abc", "bbb", "vax"];
​
    // 3.返回字符串/数字类型
    // return "Hello World";
​
    // 4.返回null或者布尔值
    return true;
  }
}
​
export default App;

函数组件

  • 函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容。

  • 函数组件有自己的特点(当然,后面我们会讲hooks,就不一样了):

    • 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
    • this关键字不能指向组件实例(因为没有组件实例);
    • 没有内部状态(state);
  • 我们来定义一个函数组件:

// 函数式组件
function App() {
  return <h1>App Function Component</h1>;
}
​
export default App;

组件的生命周期

  • 很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期;

  • React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能;

  • 生命周期和生命周期函数的关系:

  • 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段;

    • 比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程;
    • 比如更新过程(Update),组件状态发生变化,重新更新渲染的过程;
    • 比如卸载过程(Unmount),组件从DOM树中被移除的过程;
  • React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调这些函数就是生命周期函数

    • 比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调;
    • 比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调;
    • 比如实现componentWillUnmount函数:组件即将被移除时,就会回调;
    • 我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能;
  • 我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的

常用生命周期

image-20221023163048175

Constructor
  • 如果不初始化state或者不进行方法绑定,则不需要为React组件实现构造函数

  • constructor中通常只做两件事

    • 通过this.state复制对象来初始化内部的state
    • 为事件绑定实例(this)
componentDidMount
  • componentDidMount()会在组件挂载后(插入dom树种)立即调用

  • componentDidMount中通常进行哪些操作呢?

    • 依赖于DOM的操作可以在这里进行
    • 在此处就是发送网络请求的最好地方(官方推荐)
    • 可以在此处添加一些订阅(会在componentWillUnmount取消订阅)
componentDidUpdate
  • componentDidUpdate() 会在更新后被立即调用,首次渲染不会执行此方法

    • 当组件更新后,可以在此处对DOM进行操作
    • 如果你对更新前后的props进行了比较,也可以选择在此处进行网络请求;(例如:放props未发生变化时,则不会执行网络请求)
componentWillUnmount
  • componentWillUnmount() 会在组件卸载及销毁之前直接调用

    • 在此方法中执行必要的清理操作
    • 例如:清除timer,取消网络请求或清除在componentDidMount()中创建的订阅等
import React, { Component } from "react";
​
export class HelloWorld extends Component {
  // 1.构造方法:constructor
  constructor() {
    console.log("constructor");
    super();
    this.state = {
      message: "Hello 生命周期",
    };
  }
  changeText() {
    this.setState({
      message: "你好啊,洛依尘",
    });
  }
  // 2.执行render函数
  render() {
    console.log("render");
    const { message } = this.state;
    return (
      <div>
        <h2>{message}</h2>
        <button onClick={(e) => this.changeText()}>修改文本</button>
      </div>
    );
  }
  // 3.组件被渲染到DOM:被挂载到DOM
  componentDidMount() {
    console.log("组件被渲染到DOM componentDidMount");
  }
  // 4.组件的DOM被更新完成:DOM发生更新
  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log(
      "组件的DOM被更新完成 componentDidUpdate",
      prevProps,
      prevState,
      snapshot
    );
  }
  // 5.组件从DOM中卸载掉:从DOM移除掉
  componentWillUnmount() {
    console.log("组件从DOM中卸载掉 componentWillUnmount ");
  }
​
  // 不常用生命周期
  shouldComponentUpdate() {
    return true;
  }
​
  getSnapshotBeforeUpdate() {
    console.log("DOM更新前 getSnapshotBeforeUpdate");
    return {
      scrollPosition: 1000,
    };
  }
}
​
export default HelloWorld;

不常用生命周期函数

完整生命周期image-20221023164758372

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

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

组件间的通信

  • 在开发过程中,我们会经常遇到需要组件之间相互进行通信:

    • 比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示;
    • 又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给他们来进行展示;
    • 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;
  • 总之,在一个React项目中,组件之间的通信是非常重要的环节;

  • 父组件在展示子组件,可能会传递一些数据给子组件:

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

参数propTypes

  • 对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说:

    • 当然,如果你项目中默认继承了Flow或者TypeScript,那么直接就可以进行类型验证;
    • 但是,即使我们没有使用Flow或者TypeScript,也可以通过 prop-types 库来进行参数验证;
  • 从 React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types 库

  • 更多的验证方式,可以参考官网:zh-hans.reactjs.org/docs/typech…

    • 比如验证数组,并且数组中包含哪些元素;
    • 比如验证对象,并且对象中包含哪些key以及value是什么类型;
    • 比如某个原生是必须的,使用 requiredFunc: PropTypes.func.isRequired
  • 如果没有传递,我们希望有默认值呢?

    • 我们使用defaultProps就可以了

父传子方式

父组件

import React, { Component } from "react";
import axios from "axios";
​
import MainBanner from "./MainBanner";
import MainProductList from "./MainProductList";
​
export class Main extends Component {
  constructor() {
    super();
    this.state = {
      banners: ["新歌曲", "新MV", "新歌单"],
      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">
        <MainBanner banners={banners} title="轮播图" />
        <MainBanner  />
        <MainProductList productList={productList} />
      </div>
    );
  }
}
​
export default Main;

子组件

import React, { Component } from "react";
import PropTypes from "prop-types";
​
export class MainBanner extends Component {
  // 新增defaultProps的方式
  static defaultProps = {
    banners: [],
    title: "默认标题",
  };
  render() {
    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.protTypes = {
  banners: PropTypes.array.isRequired,
  title: PropTypes.string,
};
// MainBanner传入的props的默认值
MainBanner.defaultProps = {
  banners: [],
  title: "默认标题",
};
​
export default MainBanner;

子传父方式

父组件

import React, { Component } from "react";
import AddCounter from "./AddCounter";
import SubCounter from "./SubCounter";
​
export class App extends Component {
  constructor() {
    super();
    this.state = {
      counter: 100,
    };
  }
​
  // 定义一个回调函数改变父组件的count
  changeCount(count) {
    this.setState({
      counter: this.state.counter + count,
    });
  }
​
  render() {
    const { counter } = this.state;
    return (
      <div>
        <h2>当前计数:{counter}</h2>
        <AddCounter
          addClick={(count) => {
            this.changeCount(count);
          }}
        />
      </div>
    );
  }
}
​
export default App;

子组件

import React, { Component } from "react";
import PropTypes from "prop-types";
​
export class AddCounter extends Component {
  addCount(count) {
    console.log("count", count);
    // 子组件调用通过父组件props传来的addClick方法
    this.props.addClick(count);
  }
​
  render() {
    return (
      <div>
        <button onClick={(e) => this.addCount(1)}>+1</button>
        <button onClick={(e) => this.addCount(5)}>+5</button>
        <button onClick={(e) => this.addCount(10)}>+10</button>
      </div>
    );
  }
}
​
AddCounter.protoType = {
  addClick: PropTypes.function,
};
​
export default AddCounter;

组件通信案例练习

image-20221023170011376

父组件

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

export class App extends Component {
  constructor() {
    super();
    this.state = {
      titles: ["流行", "新款", "精选"],
      tabIndex: 0,
    };
  }

  // 改变父组件的回调函数
  tabClick(tabIndex) {
    this.setState({
      tabIndex,
    });
  }

  render() {
    const { titles, tabIndex } = this.state;
    return (
      <div className="app">
        <TabControl titles={titles} tabClick={(i) => this.tabClick(i)} />
        <h1>{titles[tabIndex]}</h1>
      </div>
    );
  }
}

export default App;

子组件

import React, { Component } from "react";
import "./style.css";
​
export class TabControl extends Component {
  constructor() {
    super();
​
    this.state = {
      currentIndex: 0,
    };
  }
​
  itemClick(index) {
    this.setState({
      currentIndex: index,
    });
    this.props.tabClick(index);
  }
​
  render() {
    const { titles } = this.props;
    const { currentIndex } = this.state;
    return (
      <div className="tab-control">
        {titles.map((item, index) => {
          return (
            <div
              className={`item ${index === currentIndex ? "active" : ""}`}
              key={item}
              onClick={(e) => this.itemClick(index)}
            >
              <span className="text"> {item}</span>
            </div>
          );
        })}
      </div>
    );
  }
}
​
export default TabControl;

React中的插槽(Slot)

  • 在开发中,我们抽取了一个组件,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素。

  • 我们应该让使用者可以决定某一块区域到底存放什么内容。 image-20221023170251374

  • 这种需求在Vue当中有一个固定的做法是通过slot来完成的,React呢?

  • React对于这种需要插槽的情况非常灵活,有两种方案可以实现:

    • 组件的children子元素;
    • props属性传递React元素;

父组件

import React, { Component } from "react";
import NavBar from "./nav-bar";
import NavBar2 from "./nav-bar2";
​
export class App extends Component {
  render() {
    const btn = <button>按钮</button>;
​
    return (
      <div>
        {/* 1.使用children实现插槽 */}
        <NavBar>
          <button>按钮</button>
          <h2>我是标题</h2>
          <i>斜体文字</i>
        </NavBar>
​
        {/* 2.使用props实现插槽 */}
        <NavBar2 leftSlot={btn} centerSlot={321} rightSlot={111} />
      </div>
    );
  }
}
​
export default App;

组件的children子元素

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

image-20221022175841519

import React, { Component } from "react";
import protoTypes from "prop-types";
import "./style.css";
​
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>
    );
  }
}
​
NavBar.protoTypes = {
  children: protoTypes.element,
};
​
export default NavBar;
​
  • 通过children实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;

props属性传递React元素

  • 另外一个种方案就是使用 props 实现:

    • 通过具体的属性名,可以让我们在传入和获取时更加的精准;
import React, { Component } from "react";
​
export class NavBar2 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>
    );
  }
}
​
export default NavBar2;

组件的作用域插槽

  • 如果我们需要自定义插槽类型,但是插槽的数据需要子组件提供就需要传入一个回调函数

父组件

import React, { Component } from "react";
import TabControl from "./TabControl";
​
export class App extends Component {
  constructor() {
    super();
    this.state = {
      titles: ["流行", "新款", "精选"],
      tabIndex: 0,
    };
  }
​
  // 切换active
  tabClick(tabIndex) {
    this.setState({
      tabIndex,
    });
  }
​
  //  作用域插槽
  tabItemType(item) {
    if (item === "流行") {
      return <span>{item}</span>;
    } else if (item === "新款") {
      return <button>{item}</button>;
    } else {
      return <i>{item} </i>;
    }
  }
​
  render() {
    const { titles, tabIndex } = this.state;
    return (
      <div className="app">
        <TabControl
          titles={titles}
          tabClick={(i) => this.tabClick(i)}
          // 父组件传回一个回调函数,回调函数获取到item传回后再进行处理
          itemType={(item) => this.tabItemType(item)}
        />
        <h1>{titles[tabIndex]}</h1>
      </div>
    );
  }
}
​
export default App;

子组件

import React, { Component } from "react";
import "./style.css";
​
export class TabControl extends Component {
  constructor() {
    super();
​
    this.state = {
      currentIndex: 0,
    };
  }
​
  itemClick(index) {
    this.setState({
      currentIndex: index,
    });
    this.props.tabClick(index);
  }
​
  render() {
    // 子组件获取props里的itemType,并且传回item参数
    const { titles, itemType } = this.props;
    const { currentIndex } = this.state;
    return (
      <div className="tab-control">
        {titles.map((item, index) => {
          return (
            <div
              className={`item ${index === currentIndex ? "active" : ""}`}
              key={item}
              onClick={(e) => this.itemClick(index)}
            >
              {itemType(item)}
            </div>
          );
        })}
      </div>
    );
  }
}
​
export default TabControl;

Context共享数据

  • 非父子组件数据的共享:

    • 在开发中,比较常见的数据传递方式是通过props属性自上而下(由父到子)进行传递。
    • 但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)。
    • 如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。
  • 我们实现一个一层层传递的案例:

    • 我这边顺便补充一个小的知识点:Spread Attributes
  • 但是,如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:

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

Spread Attributes

有时候你需要给组件设置多个属性,你不想一个个写下这些属性,或者有时候你甚至不知道这些属性的名称,这时候 spread attributes 的功能就很有用了。

比如:

var props = {};
props.foo = x;
props.bar = y;
var component = <Component {...props} />;

props 对象的属性会被设置成 Component 的属性。

属性也可以被覆盖:

var props = { foo: 'default' };
var component = <Component {...props} foo={'override'} />;
console.log(component.props.foo); // 'override'

写在后面的属性值会覆盖前面的属性。

Context相关API

  • React.createContext

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

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

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

    • 这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。
    • 这里需要 函数作为子元素(function as child)这种做法;
    • 这个函数接收当前的 context 值,返回一个 React 节点;

什么时候使用默认值defaultValue呢?

import React from "react";
// 一、创建一个Context ()里面是默认数据,当ThemeContext.Provider外的组件获取共享数据时可以使用共享数据
const ThemeContext = React.createContext({ color: "blue", size: "10 " });
​
export default ThemeContext;
​
  • 当ThemeContext.Provider外的组件获取共享数据时可以使用共享数据

什么时候使用Context.Consumer呢?

  1. 当使用value的组件是一个函数式组件时;
  2. 当组件中需要使用多个Context时;

实际案例

context 实例

import React from "react";
// 一、创建一个Context ()里面是默认数据,当ThemeContext.Provider外的组件获取共享数据时可以使用共享数据
const ThemeContext = React.createContext({ color: "blue", size: "10 " });
​
export default ThemeContext;

主组件

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 {
  constructor() {
    super();
​
    this.state = {
      info: { name: "kobe", age: 30 },
    };
  }
  render() {
    const { info } = this.state;
    return (
      <div>
        <h2>App</h2>
        {/* 二、通过ThemeContext里的Provider为后代提供数据,可以多层嵌套 */}
        <UserContext.Provider value={{ nickname: "kobe", age: "30" }}>
          <ThemeContext.Provider value={{ color: "red", size: "30" }}>
            <Home {...info} />
          </ThemeContext.Provider>
        </UserContext.Provider>
        <Profile />
      </div>
    );
  }
}
​
export default App;

子组件(正常调用)

import React, { Component } from "react";
import HomeBanner from "./HomeBanner";
import HomeInfo from "./HomeInfo";
​
export class Home extends Component {
  render() {
    const { name, age } = this.props;
    return (
      <div>
        <h2>
          Home:{name}-{age}
        </h2>
        <HomeInfo />
        <HomeBanner/>
      </div>
    );
  }
}
​
export default Home;

孙组件

import React, { Component } from "react";
import ThemeContext from "./context/theme-context";
import UserContext from "./context/user-context";
​
export class HomeInfo extends Component {
  render() {
    // 四、获取数据,并且使用数据
    console.log(this.context);
    return (
      <div>
        {/* 方法一:通过context获取 共享数据 */}
        HomeInfo:{this.context.color}
        {/* 方法二:通过Consumer获取共享数据 */}
        <UserContext.Consumer>
          {(value) => {
            return <h2>Info User:{value.nickname}</h2>;
          }}
        </UserContext.Consumer>
      </div>
    );
  }
}
​
// 三、设置组件的contextType为某一个Context
HomeInfo.contextType = ThemeContext;
​
export default HomeInfo;

函数组件调用

  • 函数组件就只能通过Consumer获取共享数据
import ThemeContext from "./context/theme-context";
​
function HomeBanner() {
  return (
    <div>
      <span>HomeBanner</span>
      {/* 函数式组件中使用Context共享的数据 */}
      <ThemeContext.Consumer>
        {/* 传入一个children */}
        {(value) => {
          return <h2>HomeBanner theme: {value.color}</h2>;
        }}
      </ThemeContext.Consumer>
    </div>
  );
}
​
export default HomeBanner;

事件总线eventBus

  • 如果一个组件想要发送数据或者事件到其他组件,那就要用到事件总线eventBus
  • pnpm install hy-event-store
  • 调用后,用eventBus.emit()发送数据
  • 用eventBus.on()监听数据

event-bus.js

import { HYEventBus } from "hy-event-store"const eventBus = new HYEventBus()
​
export default eventBus

HomeBanner.jsx

import React, { Component } from "react";
import eventBus from "./utils/event-bus";
​
export class HomeBanner extends Component {
  preClick() {
    console.log("preClick");
    // 事件名称,数据
    eventBus.emit("bannerPrev", "why", 18, 1.88);
  }
  nextClick() {
    console.log("nextClick");
    eventBus.emit("bannerNext", { name: "kobe", age: 30 });
  }
  render() {
    const { nextClick, preClick } = this;
    return (
      <div>
        <h2>HomeBanner</h2>
        <button onClick={(e) => preClick()}>上一个</button>
        <button onClick={(e) => nextClick()}>下一个</button>
      </div>
    );
  }
}
​
export default HomeBanner;

App.jsx

import React, { Component } from "react";
import Home from "./Home";
import eventBus from "./utils/event-bus";
​
export class App extends Component {
  constructor() {
    super();
    this.state = {
      name: "",
      age: 0,
      height: 0,
    };
  }
  componentDidMount() {
    // 事件名称,绑定方法,绑定this
    eventBus.on("bannerPrev", this.bannerPrevClick, this);
    eventBus.on("bannerNext", this.bannerNextClick, this);
  }
​
  bannerPrevClick = (name, age, height) => {
    console.log("App中监听到bannerPrev");
    this.setState({
      name,
      age,
      height,
    });
  };
  bannerNextClick(info) {
    console.log("bannerNextClick:", info);
  }
​
  componentWillUnmount() {
    eventBus.off("bannerPrev", this.bannerPrevClick);
  }
​
  render() {
    const { name, age, height } = this.state;
    return (
      <div>
        <h2>
          App Component:{name}-{age}-{height}
        </h2>
        <Home />
      </div>
    );
  }
}
​
export default App;