一. 组件的分类
React的组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件:
- 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component);
- 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component);
- 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component);
二. 组件的定义
1. 类组件
- 类组件的定义有如下要求:
- 组件的名称是大写字符开头(无论类组件还是函数组件)
- 类组件需要继承自 React.Component
- 类组件必须实现render函数
- 使用class定义一个组件:
- constructor是可选的,我们通常在constructor中初始化一些数据;
- this.state中维护的就是我们组件内部的数据;
- render() 方法是 class 组件中唯一必须实现的方法;
// rcc 类组件
import React, { Component } from 'react'
export default class App extends Component {
render() {
return (
<div>
</div>
)
}
}
1. render方法返回值
当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:
- React 元素:
- 通常通过 JSX 创建。
- 例如, 会被 React 渲染为 DOM 节点, 会被 React 渲染为自定义组件; p无论是 还是 均为 React 元素。
- 数组或 fragments:使得 render 方法可以返回多个元素。 n Portals:可以渲染子节点到不同的 DOM 子树中。
- 字符串或数值类型:它们在 DOM 中会被渲染为文本节点
- 布尔类型或 null:什么都不渲染。
2. 函数式组件
- 函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容。
- 函数组件有自己的特点:
- 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
- 没有this(组件实例);
- 没有内部状态(state);
// rfc 函数式组件
import React from 'react'
export default function fun() {
return (
<div>
</div>
)
}
/**
* 函数式组件的特点:
* 1.没有this对象
* 2.没有内部的状态
*/
三. 组件的生命周期
- 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段;
- 比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程;
- 比如更新过程(Update),组件状态发生变化,重新更新渲染的过程;
- 比如卸载过程(Unmount),组件从DOM树中被移除的过程;
- React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:
- 比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调;
- 比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调;
- 比如实现componentWillUnmount函数:组件即将被移除时,就会回调;
1. 生命周期解析
1. 挂载时
当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
2. 更新时
当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
3. 卸载时
当组件从 DOM 中移除时会调用如下方法:
2. 生命周期函数
1. constructor
在 React 组件挂载之前,会调用它的构造函数。通常,在 React 中,构造函数仅用于以下两种情况:
注意:
- 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
- 在为 React.Component 子类实现构造函数时,应在其他语句之前前调用 super(props)。否则,this.props 在构造函数中可能会出现未定义的 bug。
2. render
render() 方法是 class 组件中唯一必须实现的方法。
当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:
- React 元素。通常通过 JSX 创建。例如, 会被 React 渲染为 DOM 节点, 会被 React 渲染为自定义组件,无论是 还是 均为 React 元素。
- 数组或 fragments。 使得 render 方法可以返回多个元素。欲了解更多详细信息,请参阅 fragments 文档。
- Portals。可以渲染子节点到不同的 DOM 子树中。欲了解更多详细信息,请参阅有关 portals 的文档
- 字符串或数值类型。它们在 DOM 中会被渲染为文本节点
- 布尔类型或 null。什么都不渲染。(主要用于支持返回 test && 的模式,其中 test 为布尔类型。)
3. componentDidMount
componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。
componentDidMount中通常进行:
4. componentDidUpdate
// 1. prevProps 更新前的props
// 2. prevState 更新前的state
// 3. snapshot 如果组件实现了 getSnapshotBeforeUpdate() 生命周期(不常用),则它的返回值将作为 componentDidUpdate() 的第三个参数 “snapshot” 参数传递。否则此参数将为 undefined。
componentDidUpdate(prevProps, prevState, snapshot)
componentDidUpdate() 会在更新后会被立即调用。首次渲染不会执行此方法。
- 当组件更新后,可以在此处对 DOM 进行操作。
- 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 未发生变化时,则不会执行网络请求)。
componentDidUpdate(prevProps) {
// 典型用法(不要忘记比较 props):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}
5. componentWillUnmount
componentWillUnmount() 会在组件卸载及销毁之前直接调用。
6. 不常用生命周期函数
1. getDerivedStateFromProps
state 的值在任何时候都依赖于props时使用;该方法返回一个对象来更新state;
2. getSnapshotBeforeUpdate
在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置);
3. shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState)
当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。首次渲染或使用 forceUpdate() 时不会调用该方法。
目前,如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate(),render() 和 componentDidUpdate()。
四. 组件间通信
1. 父传子
1. 类组件
import React, { Component } from 'react';
// 子组件
class ChildCpn extends Component {
componentDidMount() {
console.log(this.props, "componentDidMount");
}
render() {
// 父类的this就是自己的this
// console.log(this.props, "render");
// 可以不使用直接对象结构
const {name, age, height} = this.props;
return (
<h2>子组件展示数据: {name + " " + age + " " + height}</h2>
)
}
}
// 父组件
export default class App extends Component {
constructor() {
super();
this.state = {
user: {
name: 'coder',
age: 18,
height: '2.22',
},
}
}
render() {
const { user } = this.state;
return (
<div>
<ChildCpn name="why" age="18" height="1.88"/>
{/* 可以直接使用对象解构来进行传参 */}
<ChildCpn {...user}/>
</div>
)
}
}
2. 函数式组件
import React, { Component } from 'react';
// 子组件
function ChildCpn(props) {
const { name, age, height } = props;
return (
<h2>{name + age + height}</h2>
)
}
// 父组件
export default class App extends Component {
render() {
return (
<div>
<ChildCpn name="why" age="18" height="1.88" />
<ChildCpn name="kobe" age="40" height="1.98" />
</div>
)
}
}
2. 子传父
通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;
import React, { Component } from 'react'
// 子组件
class AddCounter extends Component {
addCount(count) {
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>
)
}
}
// 子组件
class SubCounter extends Component {
subCount(count) {
// 直接回调即可
this.props.subClick(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 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)}/>
<SubCounter subClick={(count) => this.changeCounter(count)}/>
</div>
)
}
}
export default App
3. React中的插槽(slot)
1. 组件的children子元素(不推荐)
每个组件都可以获取到 props.children:**它包含组件的开始标签和结束标签之间的内容。 **
弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;
import React, { Component } from 'react'
class NavBar extends Component {
render() {
const { children } = this.props
console.log(children)
return (
<div className='nav-bar'>
<div className="left">{children[0]}</div>
<div className="center">{children[1]}</div>
<div className="right">{children[2]}</div>
</div>
)
}
}
export class App extends Component {
render() {
const btn = <button>按钮2</button>
return (
<div>
{/* 1.使用children实现插槽 */}
<NavBar>
<button>按钮</button>
<h2>哈哈哈</h2>
<i>斜体文本</i>
</NavBar>
</div>
)
}
}
export default App
2. props属性传递React元素(推荐)
通过具体的属性名,可以让我们在传入和获取时更加的精准
import React, { Component } from 'react'
NavBarTwo 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 class App extends Component {
render() {
// 属性是JSX元素
const btn = <button>按钮2</button>
return (
<div>
{/* 2.使用props实现插槽 */}
<NavBarTwo
leftSlot={btn}
centerSlot={<h2>呵呵呵</h2>}
rightSlot={<i>斜体2</i>}
/>
</div>
)
}
}
export default App
3. 作用域插槽
import React, { Component } from 'react'
// 子组件
class TabControl extends Component {
constructor() {
super()
this.state = {
currentIndex: 0
}
}
itemClick(index) {
// 1.自己保存最新的index
this.setState({ currentIndex: index })
// 2.让父组件执行对应的函数
this.props.tabClick(index)
}
render() {
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)}
>
{/* <span className='text'>{item}</span> */}
{/* 通过回调函数决定父组件渲染哪些内容 */}
{itemType(item)}
</div>
)
})
}
</div>
)
}
}
// 父组件
export class App extends Component {
constructor() {
super()
this.state = {
titles: ["流行", "新款", "精选"],
tabIndex: 0
}
}
tabClick(tabIndex) {
this.setState({ tabIndex })
}
// 通过回调函数决定父组件渲染哪些内容
getTabItem(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)}
// itemType={item => <button>{item}</button>}
itemType={item => this.getTabItem(item)}
/>
<h1>{titles[tabIndex]}</h1>
</div>
)
}
}
export default App
4. 非父子组件
1. Context
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
- 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 节点
什么时候使用Context.Consumer呢? 1.当使用value的组件是一个函数式组件时; 2.当组件中需要使用多个Context时;
import React, { Component } from 'react';
// 1.创建Context对象
const UserContext = React.createContext({
nickname: "aaaa",
level: -1
})
// 孙组件-类组件
class ProfileHeader extends Component {
render() {
console.log(this.context);
// jsx ->
return (
<div>
<h2>用户昵称: {this.context.nickname}</h2> // 3. 使用context
<h2>用户等级: {this.context.level}</h2>
</div>
)
}
}
// 孙组件-函数组件
function ProfileHeader() {
return (
<UserContext.Consumer>
{// 此处需要为箭头函数,参数就为传过来的值
value => {
return (
<div>
<h2>用户昵称: {value.nickname}</h2>
<h2>用户等级: {value.level}</h2>
</div>
)
}
}
</UserContext.Consumer>
)
}
// 3.在需要取值的组件的contextType属性赋值
ProfileHeader.contextType = UserContext;
ProfileHeader.contextType = ThemeContext;
// 子组件
function Profile(props) {
return (
<div>
<ProfileHeader />
</div>
)
}
// 父组件
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
nickname: "kobe",
level: 99
}
}
render() {
return (
<div>
// 2. 将创建的组件赋值,所有此组件的子组件都可以取到
<UserContext.Provider value={this.state}>
<Profile />
</UserContext.Provider>
</div>
)
}
}
2. EventBus(事件总线)
class HomeBanner extends Component {
prevClick() {
console.log("上一个")
eventBus.emit("bannerPrev", "why", 18, 1.88)
}
nextClick() {
console.log("下一个")
eventBus.emit("bannerNext", {nickname: "kobe", level: 99})
}
render() {
return (
<div>
<h2>HomeBanner</h2>
<button onClick={e => this.prevClick()}>上一个</button>
<button onClick={e => this.nextClick()}>下一个</button>
</div>
)
}
export class App extends Component {
constructor() {
super()
this.state = {
name: "",
age: 0,
height: 0
}
}
componentDidMount() {
// eventBus.on("bannerPrev", (name, age, height) => {
// console.log("app中监听到bannerPrev", name, age, height)
// this.setState({ name, age, height })
// })
eventBus.on("bannerPrev", this.bannerPrevClick, this)
eventBus.on("bannerNext", this.bannerNextClick, this)
}
bannerPrevClick(name, age, height) {
console.log("app中监听到bannerPrev", name, age, height)
this.setState({ name, age, height })
}
bannerNextClick(info) {
console.log("app中监听到bannerNext", info)
}
componentWillUnmount() {
eventBus.off("bannerPrev", this.bannerPrevClick)
}
render() {
const { name, age, height } = this.state
return (
<div>
<h2>App Component: {name}-{age}-{height}</h2>
<HomeBanner/>
</div>
)
}
}
5. 属性验证propTypes
import PropTypes from 'prop-types';
MyComponent.propTypes = {
// 你可以将属性声明为 JS 原生类型,默认情况下
// 这些属性都是可选的。
optionalArray: PropTypes.array,
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func,
optionalNumber: PropTypes.number,
optionalObject: PropTypes.object,
optionalString: PropTypes.string,
optionalSymbol: PropTypes.symbol,
// 任何可被渲染的元素(包括数字、字符串、元素或数组)
// (或 Fragment) 也包含这些类型。
optionalNode: PropTypes.node,
// 一个 React 元素。
optionalElement: PropTypes.element,
// 一个 React 元素类型(即,MyComponent)。
optionalElementType: PropTypes.elementType,
// 你也可以声明 prop 为类的实例,这里使用
// JS 的 instanceof 操作符。
optionalMessage: PropTypes.instanceOf(Message),
// 你可以让你的 prop 只能是特定的值,指定它为
// 枚举类型。
optionalEnum: PropTypes.oneOf(['News', 'Photos']),
// 一个对象可以是几种类型中的任意一个类型
optionalUnion: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Message)
]),
// 可以指定一个数组由某一类型的元素组成
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),
// 可以指定一个对象由某一类型的值组成
optionalObjectOf: PropTypes.objectOf(PropTypes.number),
// 可以指定一个对象由特定的类型值组成
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
}),
// An object with warnings on extra properties
optionalObjectWithStrictShape: PropTypes.exact({
name: PropTypes.string,
quantity: PropTypes.number
}),
// 你可以在任何 PropTypes 属性后面加上 `isRequired` ,确保
// 这个 prop 没有被提供时,会打印警告信息。
requiredFunc: PropTypes.func.isRequired,
// 任意类型的必需数据
requiredAny: PropTypes.any.isRequired,
// 你可以指定一个自定义验证器。它在验证失败时应返回一个 Error 对象。
// 请不要使用 `console.warn` 或抛出异常,因为这在 `oneOfType` 中不会起作用。
customProp: function(props, propName, componentName) {
if (!/matchme/.test(props[propName])) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
},
// 你也可以提供一个自定义的 `arrayOf` 或 `objectOf` 验证器。
// 它应该在验证失败时返回一个 Error 对象。
// 验证器将验证数组或对象中的每个值。验证器的前两个参数
// 第一个是数组或对象本身
// 第二个是他们当前的键。
customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
if (!/matchme/.test(propValue[key])) {
return new Error(
'Invalid prop `' + propFullName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
})
};