小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
任何一个人处理信息的逻辑能力都是有限的,所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。
如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解
而前端目前的模块化和组件化都是基于分而治之的思想
什么是组件化开发
如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂, 而且不利于后续的管理以及扩展
但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部 分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。
组件化应用
- 我们将一个完整的页面分成很多个组件
- 每个组件都用于实现页面的一个功能块
- 每一个组件又可以进行细分
- 组件本身又可以在多个地方进行复用
组件化是React的核心思想, App本身就是一个组件
组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
任何的应用都会被抽象成一颗组件树
React组件分类
-
根据组件的定义方式,可以分为: 函数组件(Functional Component )和类组件(Class Component)
-
根据组件内部是否有状态需要维护,可以分成: 无状态组件(Stateless Component )和有状态组件(Stateful Component)
-
据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component)
函数组件、无状态组件、展示型组件主要关注UI的展示
类组件、有状态组件、容器型组件主要关注数据逻辑
当然还有很多组件的其他划分方式: 如异步组件和高阶组件等
类组件
- 组件的名称是大写字符开头(无论类组件还是函数组件)
- 类组件需要继承自 React.Component 或 React.PureComponent
- 类组件必须实现render函数,render() 方法是 class 组件中唯一必须实现的方法
- constructor是可选的,我们通常在constructor中初始化一些数据
- this.state中维护的就是我们组件内部的数据,且当这些数据改变的时候,UI也需要发生相应的改变
import { Component } from 'react'
export default class App extends Component{
render() {
return <h2>最简单的类组件</h2>
}
}
函数组件
函数组件是使用function来进行定义的函数,只是这个函数的返回值是JSX对象
可以认为函数组件是是类组件中的render函数的一种特殊表现形式
所以函数组件(这里指的是不使用React Hook的函数组件)
- 没有生命周期,也会被更新并挂载,但是没有生命周期函数
- 没有this(组件实例)
- 没有内部状态(state)
export default function App() {
return (
<h2>我是最简单的函数组件</h2>
)
}
render的返回值
当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:
- React 元素: 通常通过 JSX 创建
- 数组或 fragments: 使得 render 方法可以返回多个元素
- Portals: 可以渲染子节点到不同的 DOM 子树中
- 字符串或数值类型: 它们在 DOM 中会被渲染为文本节点
- 布尔类型或 null: 什么都不渲染
生命周期函数
很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期
React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数
我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能
生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段:
- 装载阶段(Mount),组件第一次在DOM树中被渲染的过程
- 更新过程(Update),组件状态发生变化,重新更新渲染的过程
- 卸载过程(Unmount),组件从DOM树中被移除的过程
Constructor
如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数
constructor中通常只做两件事情:
- 给 this.state 赋值对象来初始化内部的state
- 为事件绑定实例(this), 即修正事件中的this指向
componentDidMount
componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用
componentDidMount中通常进行哪里操作呢?
- 依赖于DOM的操作可以在这里进行
- 发生网络请求
- 在此处添加一些订阅(需要在componentWillUnmount取消订阅[事件监听])
componentDidUpdate
componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法
- 当组件更新后,可以在此处对 DOM 进行操作
- 比较新旧props的变化,执行对应的操作
componentWillUnmount
componentWillUnmount() 会在组件卸载及销毁之前直接调用
- 此方法中执行必要的清理操作, 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等
除了上述几个比较常用的生命周期函数外, 还有一些不常用的生命周期函数,具体可以点击这里进行查看
组件间通信
如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护
所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件
再将这些组件组合嵌套在一起,最终形成我们的应用程序
所以组件之间的通信就变得尤为的重要
父传子
类组件
import { Component } from 'react'
class Cpn extends Component {
// 所有传入的props会存放于当前组件的props对象中
// 以下是派送类默认的构造函数,所以就算不写构造方法
// 我们依旧可以使用props对象来获取父类传递给子类的数据
/* constructor(props) {
super(props)
} */
render() {
return (<div>
<h2>{ this.props.name }</h2>
<h2>{ this.props.age }</h2>
</div>)
}
}
export default class App extends Component {
render() {
return (
<div>
<Cpn name="Klaus" age="23" />
</div>
)
}
}
函数组件
import { Component } from 'react'
// 父组件传入的props会直接作为函数组件的参数被传入
function Cpn(props) {
return (<div>
<h2>{ props.name }</h2>
<h2>{ props.age }</h2>
<h2>{ props.friends.join(',') }</h2>
</div>)
}
export default class App extends Component {
render() {
return (
<div>
<Cpn name="Klaus" age={23} friends={['Alex', 'Jhon']} />
</div>
)
}
}
属性校验
对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说
React对应props的类型校验,使用的是prop-types这个库
prop-types这个库原本是React中的一部分,
但是在React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types
具体可以验证的方式和使用方式,可以点击这里进行查看
类组件
import { Component } from 'react'
// 这是React脚手架下载的时候自带的包,不需要单独安装
// 引入库的时候,对象名的首字母是大写的
import PropTypes from 'prop-types'
class Cpn extends Component {
// 类组件中类型校验的方式有两种设置方式
// 方式一: Cpn.propTypes = {} ---> 不推荐
// 方式二: 静态属性
// 注意: props类型校验的首字母是小写的
static propTypes = {
name: PropTypes.string.isRequired, // name属性的值是字符串类型,且是必传的
age: PropTypes.number,
friends: PropTypes.array
}
// 设置props的默认值
static defaultProps = {
// 如果默认值和isRequired同时存在,那么会先设置默认值,再将对应的值传入子组件
// 也就是说默认值和isRequired同时存在的时候,isRequired会失去存在的意义
name: 'Steven',
age: 23,
friends: ['Alice']
}
render() {
return (<div>
<h2>{ this.props.name }</h2>
<h2>{ this.props.age }</h2>
<h2>{ this.props.friends.join(',') }</h2>
</div>)
}
}
export default class App extends Component {
render() {
return (
<div>
<Cpn name="Klaus" age={23} friends={['Alex', 'Jhon']} />
{/* 使用默认值 */}
<Cpn />
</div>
)
}
}
函数组件
import { Component } from 'react'
import PropTypes from 'prop-types'
function Cpn(props) {
return (<div>
<h2>{ props.name }</h2>
<h2>{ props.age }</h2>
<h2>{ props.friends.join(',') }</h2>
</div>)
}
// 函数组件没有对应的静态方法和静态属性
// 所以需要手动向函数组件上挂载对应的校验规则和默认值
Cpn.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
friends: PropTypes.array
}
Cpn.defaultProps = {
age: 23,
friends: ['Alice']
}
export default class App extends Component {
render() {
return (
<div>
<Cpn name="Klaus" age={23} friends={['Alex', 'Jhon']} />
{/* 使用默认值 */}
<Cpn />
</div>
)
}
}
子传父
某些情况,我们也需要子组件向父组件传递消息:
- 在vue中是通过自定义事件来完成的
- 在React中同样是通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可
import { Component } from 'react'
class Btn extends Component {
render() {
return (
<>
{/*
和Vue触发自定义事件不一样的是
React直接使用引用指向,让子组件直接触发父组件传递过来的事件函数
*/}
<button onClick={ this.props.increment }>+1</button>
</>
)
}
}
export default class App extends Component {
constructor(props) {
super(props)
this.state = {
counter: 0
}
}
render() {
return (
<div>
<h2>{ this.state.counter }</h2>
{/*
需要在父组件修正this指向
因为这个方法虽然是在子组件中被调用
但是实际的逻辑实在父组件中进行的
所以函数调用的this必须是状态所在的父组件
*/}
<Btn increment={ () => this.increment() }/>
</div>
)
}
increment() {
this.setState({
counter: this.state.counter + 1
})
}
}
阶段案例
需求: 实现一个简单的Tab组件
Cpn/index.css
.tabs {
width: 100%;
display: flex;
justify-content: space-around;
align-items: center;
}
.tab.active {
color: red;
}
.tab span {
padding: 5px 8px;
}
.tab.active span {
border-bottom: 3px solid #ff0000;
}
Cpn/index.js
import { Component } from 'react'
import PropTypes from 'prop-types';
import './index.css'
export default class Tabs extends Component {
static propTypes = {
tabs: PropTypes.array.isRequired,
active: PropTypes.string,
changeActive: PropTypes.func
}
static defaultProps = {
active: 'one'
}
render() {
const { tabs, active, changeActive } = this.props
return (
<div className="tabs">
{
tabs.map(item =>
<div
key={item}
className={'tab ' + (item === active ? 'active' : '')}
onClick= {() => changeActive(item)}
>
<span>{ item }</span>
</div>
)
}
</div>
)
}
}
App.js
import { Component } from 'react'
import Tabs from './Tabs'
export default class App extends Component {
constructor() {
super()
// 只有需要在UI中使用,且数据发生改变的时候,需要重新渲染UI的数据才需要被放置到state对象中
// 其余的数据直接使用class fields 定义成类的实例属性即可
this.tabs = ['one', 'two', 'three']
this.state = {
active: 'one'
}
}
render() {
return (
<div>
<Tabs
active={this.state.active}
tabs={this.tabs}
changeActive={active => this.changeActive(active)}
/>
<h2>{ this.state.active }</h2>
</div>
)
}
changeActive(active) {
this.setState({
active
})
}
}
模拟slot
React没有提供Vue中的slot这样的功能,但是JSX本质上就是一个对象,所以我们可以模拟插槽来进行使用
默认插槽
App.js
import { Component } from 'react'
import Cpn from './Cpn'
export default class App extends Component {
render() {
return (
<Cpn>
{/*
这里的内容会作为Cpn的children
被放置到Cpn的props.children属性中
*/}
<span>default slot</span>
</Cpn>
)
}
}
Cpn.js
import { Component } from 'react'
export default class Cpn extends Component {
render() {
return (
<div>
{/*
使用props.children 这种方式来实现插槽具有一定的限制
如果有多个值会作为数组被传入(此时取值的时候,需要通过索引)
如果只有一个值的时候,值就是对应的值(注意:是对应的值,不是数组)
所以如果props.children的值是数组的时候,
对于传入的slot的顺序会具有一定的要求
所以使用props.children来模拟slot的时候,一般适用于只有一个slot的情况
也就是只要缺省插槽的情况
*/}
{ this.props.children }
</div>
)
}
}
具名插槽
App.js
import { Component } from 'react'
import Cpn from './Cpn'
export default class App extends Component {
constructor() {
super()
this.leftSlot = <h2>left item</h2>
this.centerSlot = <h2>center item</h2>
this.rightSlot = <h2>right item</h2>
}
render() {
return (
<Cpn
leftSlot={ this.leftSlot }
centerSlot={ this.centerSlot }
rightSlot={ this.rightSlot }
/>
)
}
}
Cpn.js
import { Component } from 'react'
export default class Cpn extends Component {
render() {
return (
<div>
<div className="left">{ this.props.leftSlot }</div>
<div className="center">{ this.props.centerSlot }</div>
<div className="right">{ this.props.rightSlot }</div>
</div>
)
}
}
跨父子组件通信
在实际使用的场景中,我们可能会遇到父组件和孙子组件之间需要进行通信
此时我们有多种实现方式
props逐层进行传递
import { Component } from 'react'
function ProfileHeader(props) {
return (
<div>
<h2>nickname: { props.nickname }</h2>
<h2>level: { props.level }</h2>
</div>
)
}
function Profile(props) {
return (
<div>
<ProfileHeader {...props} />
<h2>content</h2>
</div>
)
}
export default class App extends Component {
constructor() {
super()
this.state = {
nickname: 'Klaus',
level: 6
}
}
render() {
return (
<div>
{/* spread attributes是jsx提供的语法糖,可以将解构出的属性直接作为组件的props进行传递 */}
<Profile {...this.state}/>
</div>
)
}
}
但是此时,中间组件Profile明显是没有使用到nickname和level这两个props的
但是因为props逐层传递的需要,Profiles依旧需要进行对应的属性传递
因为React提供了context,(类似于Vue中的provide和inject)
context基本使用
Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props
函数组件
import React,{ Component } from 'react'
// 1. 创建context
// React.createContext()中的参数是默认值
// React.createContext参数的返回值本身是一个React组件
const userContext = React.createContext({
nickname: 'default name',
level: -1
})
function ProfileHeader() {
return (
<userContext.Consumer>
{/*
3. 在消费者中使用context中的数据
*/}
{
info => (
<div>
<h2>nickname: { info.nickname }</h2>
<h2>level: { info.level }</h2>
</div>
)
}
</userContext.Consumer>
)
}
function Profile(props) {
return <ProfileHeader {...this.props} />
}
export default class App extends Component {
constructor() {
super()
this.state = {
nickname: 'Klaus',
level: 6
}
}
render() {
return (
<div>
{/*
2. 提供需要使用context传递的数据
需要传递的值存放于value属性中,多个值组成为对象后在进行传递
*/}
<userContext.Provider value={this.state}>
<Profile />
</userContext.Provider>
<hr />
{/* 不使用userContext.Provider包裹,则使用默认值 */}
<Profile />
</div>
)
}
}
类组件
import React, { Component } from 'react'
const userContext = React.createContext({
nickname: 'default name',
level: -1
})
class ProfileHeader extends Component {
// 在类组件中有一个属性,context
// 默认情况下,其值是空对象,因为其不知道需要使用哪个ctx中的值
// 所以类中有一个静态属性contextType
// 用来指定context属性,应该使用哪个ctx中提供的值
static contextType = userContext
render() {
const { nickname, level } = this.context
return (
<div>
<h2>nickname: { nickname }</h2>
<h2>level: { level }</h2>
</div>
)
}
}
class Profile extends Component {
render() {
return <ProfileHeader />
}
}
export default class App extends Component {
constructor() {
super()
this.state = {
nickname: 'Klaus',
level: 6
}
}
render() {
return (
{/* 提供数据 */}
<userContext.Provider value={this.state}>
<Profile />
</userContext.Provider>
)
}
}
多个context共存
函数组件
import React,{ Component } from 'react'
const userContext = React.createContext({
nickname: 'default name',
level: -1
})
const themeContext = React.createContext({
color: 'red'
})
function ProfileHeader() {
return (
<userContext.Consumer>
{
info => (
<themeContext.Consumer>
{
theme => (
<div style={{color: theme.color}}>
<h2>nickname: { info.nickname }</h2>
<h2>level: { info.level }</h2>
</div>
)
}
</themeContext.Consumer>
)
}
</userContext.Consumer>
)
}
function Profile(props) {
return <ProfileHeader />
}
export default class App extends Component {
constructor() {
super()
this.state = {
nickname: 'Klaus',
level: 6
}
}
render() {
return (
<div>
<userContext.Provider value={this.state}>
<themeContext.Provider value={{color: 'blue'}}>
<Profile />
</themeContext.Provider>
</userContext.Provider>
</div>
)
}
}
类组件
import React, { Component } from 'react'
const userContext = React.createContext({
nickname: 'default name',
level: -1
})
const themeContext = React.createContext({
color: 'red'
})
class ProfileHeader extends Component {
static contextType = userContext
render() {
const { nickname, level } = this.context
return (
<themeContext.Consumer>
{
theme => (
<div style={{ color: theme.color }}>
<h2>nickname: { nickname }</h2>
<h2>level: { level }</h2>
</div>
)
}
</themeContext.Consumer>
)
}
}
class Profile extends Component {
render() {
return <ProfileHeader />
}
}
export default class App extends Component {
constructor() {
super()
this.state = {
nickname: 'Klaus',
level: 6
}
}
render() {
return (
<userContext.Provider value={this.state}>
<themeContext.Provider value={{ color: 'blue' }}>
<Profile />
</themeContext.Provider>
</userContext.Provider>
)
}
}
此时,可以发现如果我们需要使用多个context进行数据共享的时候,很容易就出现层级过深的问题
此时推荐使用redux来取代context进行数据共享