目录
Context.consumer 在组件在组件中需要使用多个 Context 时
一、什么是组件化开发
组件化是一种分而治之的思想:
- 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。
- 但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。
我们需要通过组件化的思想来思考整个应用程序:
- 我们将一个完整的页面分成很多个组件;
- 每个组件都用于实现页面的一个功能块;
- 而每一个组件又可以进行细分;
- 而组件本身又可以在多个地方进行复用;
- 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。
- 任何的应用都会被抽象成一颗组件树。
- 有了组件化的思想,我们在之后的开发中就要充分的利用它。
- 尽可能的将页面拆分成一个个小的、可复用的组件。
- 这样让我们的代码更加方便组织和管理,并且扩展性也更强。
- 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component);
- 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component);
- 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component);
- 函数组件、无状态组件、展示型组件主要关注UI的展示;
- 类组件、有状态组件、容器型组件主要关注数据逻辑;
二、类组件
- 组件的名称是大写字符开头(无论类组件还是函数组件)
- 类组件需要继承自 React.Component
- 类组件必须实现render函数
- constructor是可选的,我们通常在constructor中初始化一些数据;
- this.state中维护的就是我们组件内部的数据;
- render() 方法是 class 组件中唯一必须实现的方法;
三、render 函数的返回值
- 通常通过 JSX 创建。
- 例如,<div /> 会被 React 渲染为 DOM 节点,<MyComponent /> 会被 React 渲染为自定义组件;
- 无论是 <div /> 还是 <MyComponent /> 均为 React 元素。
2.数组或 fragments:使得 render 方法可以返回多个元素。
3.Portals:可以渲染子节点到不同的 DOM 子树中。
4.字符串或数值类型:它们在 DOM 中会被渲染为文本节点
5.布尔类型或 null:什么都不渲染。
import React from "react";
// 1.类组件 首字母要大写
class App extends React.Component {
// constructor 维护组件状态
constructor() {
super()
this.state = {
message: "App component"
}
}
render() {
// const { message } = this.state
// 1.react 元素:通过 jsx 编写的代码就会被编译成 React.createElement,所以返回的就是一个React元素
// return <h2>{message}</h2>
// 2.组件或者 fragments(后续了解)和数组
// return ["abc", "cba", "cab"]
// return [
// <h1>h1元素</h1>,
// <h2>h2元素</h2>,
// <div>div元素</div>
// ]
// 3.字符串或数值类型
// return "你好"
// 4.布尔类型或 null 不会显示
return null
}
}
export default App;
四、函数组件
- 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
- this关键字不能指向组件实例(因为没有组件实例);
- 没有内部状态(state);
五、生命周期
5.1 认知生命周期
- 比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程;
- 比如更新过程(Update),组件状态发生变化,重新更新渲染的过程;
- 比如卸载过程(Unmount),组件从DOM树中被移除的过程;
- 比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调;
- 比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调;
- 比如实现componentWillUnmount函数:组件即将被移除时,就会回调;
- 我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能;
5.2 生命周期函数
- Constructor
- 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
- constructor中通常只做两件事情:
- 通过给 this.state 赋值对象来初始化内部的state;
- 为事件绑定实例(this);
- componentDidMount
- componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。
- componentDidMount中通常进行哪里操作呢?
- 依赖于DOM的操作可以在这里进行;
- 在此处发送网络请求就最好的地方;(官方建议)
- 可以在此处添加一些订阅(会在componentWillUnmount取消订阅);
- componentDidUpdate
- componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。
- 当组件更新后,可以在此处对 DOM 进行操作;
- 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。
- componentWillUnmount
- componentWillUnmount() 会在组件卸载及销毁之前直接调用。
- 在此方法中执行必要的清理操作;
- 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅
App.jsx
import React from "react";
import HelloWorld from "./HelloWorld";
class App extends React.Component {
constructor() {
super()
this.state = {
isShowHW: "true"
}
}
changeShow() {
// 对 isShowHW 取反
this.setState({ isShowHW: !this.state.isShowHW })
}
render() {
const { isShowHW } = this.state
// console.log(isShowHW);
return (
<div>
<h1>App React</h1>
<button onClick={e => this.changeShow()}>切换</button>
{isShowHW && <HelloWorld />}
</div>
)
}
}
export default App
HelloWorld.jsx
import React from "react";
class HelloWorld extends React.Component {
constructor() {
console.log("HelloWorld Constructor");
super()
this.state = {
message: '程序员的第一个代码 Hello World'
}
}
changeText() {
this.setState({ message: "message 被修改咯" })
}
render() {
console.log("HelloWorld render");
const { message } = this.state
return (
<div>
<h1>{message}</h1>
<button onClick={e => this.changeText()}>修改文本</button>
</div>
)
}
// 3.组件被渲染到 DOM/ 被挂载到 DOM
componentDidMount() {
console.log("componentDidMount");
}
// 4.组件被更新完成
componentDidUpdate() {
console.log("HelloWorld componentDidUpdate");
}
// 5.监听组件被卸载,从 DOM 中移除掉
componentWillUnmount() {
console.log("HelloWorld componentWillUnmount");
}
}
export default HelloWorld
5.3 不常用生命周期函数
- getDerivedStateFromProps:state 的值在任何时候都依赖于 props时使用;该方法返回一个对象来更新state;
- getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置);
- shouldComponentUpdate:该生命周期函数很常用,但是我们等待讲性能优化时再来详细讲解;
// 在界面更新前保存一些数据, 在组件被更新完成 componentDidMount 的第三个参数可以获得返回的数据
getSnapshotBeforeUpdate() {
console.log("getSnapshotBeforeUpdate");
return {
scrollPosition: 1000
}
}
六、认识组件的嵌套
组件当中存在嵌套关系
- 在之前的案例中,我们只是创建了一个组件App;
- 如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护;
- 所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件;
- 再将这些组件组合嵌套在一起,最终形成我们的应用程序;
上面的嵌套逻辑如下,它们存在如下关系:
- App组件是Header、Main、Footer组件的父组件;
- Main组件是Banner、ProductList组件的父组件;
6.1 认识组件间的通信
- 比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示;
- 又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给他们来进行展示;
- 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;
6.1.1 父传子
父组件在展示子组件,可能会传递一些数据给子组件:
- 父组件通过 属性=值 的形式来传递给子组件数据; <MainBanner banners={banners}>
- 子组件通过 props 参数获取父组件传递过来的数据;
class Main extends Component { constructor(props){ super(props) this.state={} } render() { this.state const {banners} = this.props } }
可以换成真实数据进行展示:
代码如下:
componentDidMount() {
axios.get("http://123.207.32.32:8000/home/multidata").then(res => {
// console.log(res);
const banners = res.data.data.banner.list
const recommend = res.data.data.recommend.list
this.setState({
banners,
productList: recommend
})
})
}
6.1.2 类型验证propTypes
对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说:
- 当然,如果你项目中默认继承了Flow或者TypeScript,那么直接就可以进行类型验证;
- 但是,即使我们没有使用Flow或者TypeScript,也可以通过 prop-types 库来进行参数验证;
- 比如验证数组,并且数组中包含哪些元素;
- 比如验证对象,并且对象中包含哪些key以及value是什么类型;
- 比如某个原生是必须的,使用 requiredFunc: PropTypes.func.isRequired
在上面的案例中添加propTypes的参数如下:
- banners: PropTypes.array.isRequired:表示 banners 的类型必须是数组且是必须的
-
title: PropTypes.string:表示 title 的类型必须是 字符串类型的
默认值:defaultProps,当数据中没有东西时,使用默认值
6.1.3 子传父
- 在vue中是通过自定义事件来完成的;
- 在React中同样是通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;
- 将计数器案例进行拆解;
- 将按钮封装到子组件中:CounterButton;
- CounterButton发生点击事件,将内容传递到父组件中,修改counter的值;
父组件 App.jsx
import React, { Component } from 'react'
import AddCounter from './AddCounter'
import SubCounter from './SubCounter'
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>
<h1>Counter: {counter}</h1>
<AddCounter addCount={(count) => this.changeCounter(count)} />
<SubCounter subCount={(count) => this.changeCounter(count)} />
</div>
)
}
}
export default App
子组件 AddCounter.jsx
import React, { Component } from 'react'
export class AddCounter extends Component {
addCount(count) {
this.props.addCount(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>
)
}
}
export default AddCounter
子组件 SubCounter.jsx
import React, { Component } from 'react'
export class SubCounter extends Component {
subCount(count) {
this.props.subCount(count)
console.log(count);
}
render() {
return (
<div>
<button onClick={e => this.subCount(-1)}>-1</button>
<button onClick={e => this.subCount(-5)}>-5</button>
<button onClick={e => this.subCount(-10)}>-10</button>
</div>
)
}
}
export default SubCounter
6.2 组件通信案例练习
1. 实现鼠标点击哪个标题,标题就添加 active 属性变为红色选中状态:
- 默认第一个标题是选中状态,所以先自定义一个变量 currentIndex: 0;
- 在遍历的时候传入 index,在标题的标签中写入一个判断,className={`item ${index === currentIndex? 'active': ' '}`},判断当前的索引值;
- 接下来判断鼠标点击的索引值,完成鼠标点击谁,谁就添加一个 active 属性:在map遍历的地方传入 index,判断当前鼠标点击的标题的索引,然后传入一个 onClick 点击事件,onClick={e=>this.itemClick(index)},这样就能获得鼠标点击的对象的索引值了;
- 最后将index的值传给 currentIndex ,这样就完成的点击谁谁就添加 active 属性。
import React, { Component } from 'react'
import './style.css'
export class index extends Component {
constructor() {
super()
this.state = {
currentIndex: 0
}
}
itemClick(index) {
this.setState({
currentIndex: 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 index
2. 实现点击哪个标题,下面展示相对应的内容
1、在父组件中创建一个变量 tabIndex,来获取标题的索引
2、父组件通过创建事件获取子组件的索引值
<TabControl titles={titles} tabClick={(i) => this.tabClick(i)} />
tabClick(index) {
this.setState({
tabIndex: index
})
}
3、在组件的事件中通过:this.props.tabClick(index),将索引传给父组件
父组件:
import React, { Component } from 'react'
import TabControl from './TabControl'
export class App extends Component {
constructor() {
super()
this.state = {
titles: ['流行', '新歌', '精选'],
tabIndex: 0
}
}
tabClick(index) {
this.setState({
tabIndex: index
})
}
render() {
const { titles, tabIndex } = this.state
return (
<div>
<TabControl titles={titles} tabClick={(i) => this.tabClick(i)} />
<h1>{titles[tabIndex]}</h1>
</div>
)
}
}
export default App
子组件,将值传递给父组件:this.props.tabClick(index)
itemClick(index) {
this.setState({
currentIndex: index
})
this.props.tabClick(index)
}
七、组件的插槽实现
- 组件的children子元素;
- props属性传递React元素;
7.1 children 实现插槽
每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容。
但是通过children实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;
可以通过 propsType 限制children传入的为元素(即只传入一个元素,使children为元素)
7.2 props 实现插槽
7.3 作用域插槽
八、Context 应用场景
- 在开发中,比较常见的数据传递方式是通过props属性自上而下(由父到子)进行传递。
- 但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)。
- 如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。
- 我这边顺便补充一个小的知识点:Spread Attributes
- React提供了一个API:Context;
- Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props;
- Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言;
使用 Context 步骤
1. 创建 Context
2.通过 ThemeContext 中的 Provider 中的value属性为后代提供数据
3.设置组件中的 contextType 的类型为某一个 context
4.获取到数据,并且使用数据
8.1 Context相关API
- 创建一个需要共享的Context对象:
- 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值;
- defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值
// 1. 创建一个 Context
const ThemeContext = React.createContext()
- 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:
- Provider 接收一个 value 属性,传递给消费组件;
- 一个 Provider 可以和多个消费组件有对应关系;
- 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
- 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;
{/* 第二步:通过 ThemeContext 中的 Provider 中的value属性为后代提供数据 */}
<ThemeContext.Provider value={{ color: 'red', size: '30' }}>
<Home {...info} />
</ThemeContext.Provider>
- 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:
- 这能让你使用 this.context 来消费最近 Context 上的那个值;
- 你可以在任何生命周期中访问到它,包括 render 函数中;
// 第三步.设置组件中的 contextType 的类型为某一个 context
HomeInfo.contextType = ThemeContext
- 这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。
- 这里需要 函数作为子元素(function as child)这种做法;
- 这个函数接收当前的 context 值,返回一个 React 节点;
import ThemeContext from "./context/theme-context";
function HomeBanner() {
return <div>
<span>HomeBanner</span>
{/* 函数式组件中使用 Context 共享的数据 */}
<ThemeContext.Consumer>
{
value => {
return <h2>{value.color}</h2>
}
}
</ThemeContext.Consumer>
</div>
}
export default HomeBanner
- 如果使用Context,该组件不是包裹在 Conntext.Provider中
- 那么使用的是 defaultValue 里面设置的值
// 1. 创建一个 Context,里面是defaultValue
const ThemeContext = React.createContext({ color: 'red', level: '100' })
- 1.当使用value的组件是一个函数式组件时;
- 2.当组件中需要使用多个Context时;
Context.consumer 在组件在组件中需要使用多个 Context 时