React的组件化开发
什么是组件化开发
组件化是一种分而治之的思想:
- 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展;
- 但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。
我们需要通过组件化的思想来思考整个应用程序,我们将一个完整的页面分成很多个组件,每个组件都用于实现页面的一个功能块,而每一个组件又可以进行细分,而组件本身又可以在多个地方进行复用。
React中的组件化
组件化是React的核心思想,也是我们后续课程的重点,前面我们封装的App本身就是一个组件,组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用,任何的应用都会被抽象成一颗组件树。
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 组件中唯一必须实现的方法。
下面是一个简单的示例:
import React from "react";
export default class HelloWorld extends React.Component {
render() {
return (
<h2>hello world</h2>
)
}
}
当render
被调用时,它会检查 this.props
和 this.state
的变化并返回以下类型之一:
- React 元素:通常通过 JSX 创建。例如,
<div />
会被 React 渲染为 DOM 节点,<MyComponent/>
会被 React 渲染为自定义组件;无论是<div />
还是<MyComponent />
均为 React 元素。 - 数组或 fragments:使得 render 方法可以返回多个元素
- Portals:可以渲染子节点到不同的 DOM 子树中。
- 字符串或数字类型:它们在 DOM 中会被渲染为文本节点
函数组件
函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容,函数组件有自己的特点 (当然,后面我们会讲hooks,就不一样了) :
- 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
- this关键字不能指向组件实例(因为没有组件实例);
- 没有内部状态(state)。
下面是一个函数式组件:
function App() {
return (
<h2>hello world</h2>
)
}
常用的生命周期
很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期,React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能。
以下是官方的常用生命周期示例:
生命周期和生命周期函数的关系
生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段:
- 比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程;
- 比如更新过程(Update),组件状态发生变化,重新更新渲染的过程;
- 比如卸载过程(Unmount)组件从DOM树中被移除的过程。
React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调这些函数就是生命周期函数:
- 比如实现componentDidMount函数: 组件已经挂载到DOM上时,就会回调;
- 比如实现componentDidUpdate函数: 组件已经发生了更新时,就会回调;
- 比如实现componentWillUnmount函数: 组件即将被移除时,就会回调;
- 我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能。
我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的(后面我们可以通过hooks来模拟一些生命周期的回调)
渲染时的生命周期
下面来个代码示例:
import React, { Component } from 'react'
import HelloWorld from './HelloWorld';
export class App extends Component {
render() {
return (
<div>
<HelloWorld />
</div>
)
}
}
export default App;
更新时的生命周期
卸载时的生命周期
import React, { Component } from 'react'
import HelloWorld from './HelloWorld';
export class App extends Component {
constructor() {
super();
this.state = {
isShow: true
}
}
click() {
this.setState({ isShow: !this.state.isShow });
}
render() {
const { isShow } = this.state;
return (
<div>
<button onClick={() => this.click()}>卸载</button>
{ isShow && <HelloWorld /> }
</div>
)
}
}
export default App;
以下是实现效果:
总结
- Constructor:如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数constructor中通常只做两件事情:
- 通过给 this.state 赋值对象来初始化内部的state;
- 为事件绑定实例 (this)。
- componentDidMount:componentDidMount 会在组件挂载后 (插入 DOM 树中)立即调用。componentDidMount中通常进行什么操作呢?
- 依赖于DOM的操作可以在这里进行;
- 在此处发送网络请求就最好的地方; (官方建议)
- 可以在此处添加一些订阅 (会在componentWillUnmount取消订阅)
- componentDidUpdate:componentDidUpdate 会在更新后会被立即调用,首次渲染不会执行此方法:
- 当组件更新后,可以在此处对 DOM 进行操作;
- 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求,例如当 props 未发生变化时,则不会执行网络请求)。
- componentWillUnmount:componentWillUnmount 会在组件卸载及销毁之前直接调用:
- 在此方法中执行必要的清理操作;
- 例如,清除 timer,取消网络请求或清除在 componentDidMount0 中创建的订阅等;
不常用的生命周期
除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数
- getDerivedStateFromProps: state 的值在任何时候都依赖于 props时使用;该方法返回一个对象来更新state;
- getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的些信息 (比如说滚动位置);
- shouldComponentUpdate:该生命周期函数很常用,但是我们等待讲性能优化时再来详细讲解
另外,React中还提供了一些过期的生命周期函数,这些函数已经不推荐使用。更详细的生命周期相关的内容,可以参考React.Component
以下是官方提供的不常用的生命周期:
组件间的通信
父传子通信
在开发过程中,我们会经常遇到需要组件之间相互进行通信,比如App可能使用了多个Header其进行展示,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据让其展示,又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给他们来进行展示,也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件。
总之,在一个React项目中,组件之间的通信是非常重要的环节,父组件在展示子组件,可能会传递一些数据给子组件口父组件通过 属性=值
的形式来传递给子组件数据,子组件通过 props
参数获取父组件传递过来的数据。
下面举个例子:
import React, { Component } from 'react'
import Banner from './Banner';
import ProductList from './ProductList';
export class Main extends Component {
constructor() {
super();
this.state = {
banner: ['banner1', 'banner2', 'banner3'],
product: ['product1', 'product2', 'product3'],
}
}
render() {
const { banner, product } = this.state;
return (
<div>
<h2>Main</h2>
<Banner banner={banner} />
<ProductList product={product} />
</div>
)
}
}
export default Main;
以下是效果图:
参数propsType
对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说,当然,如果你项目中默认继承了Flow
或者TypeScript
,那么直接就可以进行类型验证,但是,即使我们没有使用Flow
或者TypeScript
,也可以通过 prop-types
库来进行参数验证。从 React v15.5
开始,React.PropTypes
已移入另一个包中: prop-types
库。
下面举个例子:
import React, { Component } from 'react';
import propTypes from 'prop-types';
export class Banner extends Component {
// ...
}
Banner.propTypes = {
banner: propTypes.array.isRequired
}
export default Banner;
如果我们给Banner组件的banner
传入数字则会报错:
更多的验证方式,可以参考使用 PropTypes 进行类型检查:
- 比如验证数组,并且数组中包含哪些元素;
- 比如验证对象,并且对象中包含哪些key以及value是什么类型;
- 比如某个原生是必须的,使用 requiredFunc: PropTypes.funcisRequired。
如果想提供默认值呢?
import React, { Component } from 'react';
import propTypes from 'prop-types';
export class Banner extends Component {
//...
}
// 指定 props 的默认值:
Banner.defaultProps = {
banner: ['default banner1', 'default banner2', 'default banner3'],
};
效果展示如下:
❌ 如果你正在使用像
plugin-proposal-class-properties
(之前名为 plugin-transform-class-properties)的 Babel 转换工具,你也可以在 React 组件类中声明defaultProps
作为静态属性。此语法提案还没有最终确定,需要进行编译后才能在浏览器中运行。
子传父通信
通过函数回调的形式
// App.jsx
import React, { Component } from 'react'
import Count from './Count';
export class App extends Component {
constructor() {
super();
this.state = {
count: 100
}
}
add(n) {
this.setState({ count: this.state.count + n });
}
render() {
const { count } = this.state;
return (
<div>
<h1>App</h1>
<p>当前count: {count}</p>
<Count add={(n) => this.add(n)} />
</div>
)
}
}
export default App
// Count.jsx
import React, { Component } from 'react'
export class Count extends Component {
add(n) {
this.props.add(n);
}
render() {
return (
<div>
<button onClick={() => this.add(1)}>+1</button>
<button onClick={() => this.add(5)}>+5</button>
<button onClick={() => this.add(10)}>+10</button>
</div>
)
}
}
export default Count;
最终效果如下:
组件通信案例练习
案例效果如下:
最终代码就不贴了,源代码在GitHub仓库中查看。
React中的插槽
在开发中,我们抽取了一个组件,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素,我们应该让使用者可以决定某一块区域到底存放什么内容。
这种需求在Vue当中有一个固定的做法是通过slot来完成的,React对于这种需要插槽的情况非常灵活,有两种方案可以实现:
- 组件的children子元素;
- props属性传递React元素。
组件的children子元素实现插槽
// NavBar.jsx
import React, { Component } from 'react'
export class NavBar extends Component {
render() {
const { children } = this.props;
return (
<div>
<h2>NavBar</h2>
<div className="box">
<div className="left">{children[0]}</div>
<div className="center">{children[1]}</div>
<div className="right">{children[2]}</div>
</div>
</div>
)
}
}
export default NavBar
// App.jsx
import React, { Component } from 'react'
import NavBar from './NavBar'
export class App extends Component {
render() {
return (
<div>
<h1>组件的插槽实现</h1>
<NavBar>
<span>left</span>
<span>center</span>
<span>right</span>
</NavBar>
</div>
)
}
}
export default App
💡 如果在NavBar
中只传入一个子元素,那么children
是一个对象,否则是一个数组。示例如下:
传入一个子元素
<NavBar>
<span>left</span>
</NavBar>
打印children
如下:
传入多个子元素
<NavBar>
<span>left</span>
<span>center</span>
<span>right</span>
</NavBar>
打印children
如下:
使用propsType限制只传入一个子元素
NavBar.propTypes = {
children: PropTypes.element
}
使用propsType限制传入多个子元素
NavBar.propTypes = {
children: PropTypes.array
}
props属性传递React元素实现插槽
通过children实现的方案虽然可行,但是有一个弊端,那就是通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生。
// App.jsx
import React, { Component } from 'react'
import NavBar2 from './NavBar2';
export class App extends Component {
render() {
return (
<div>
<NavBar2
left={<button>left</button>}
center={<span>center</span>}
right={<em>right</em>}
/>
</div>
)
}
}
export default App
// NavBar2.jsx
import React, { Component } from 'react'
export class NavBar2 extends Component {
render() {
const { left, center, right } = this.props;
return (
<div>
<h2>NavBar2</h2>
{/* props属性 */}
<div className="left">{left}</div>
<div className="center">{center}</div>
<div className="right">{right}</div>
</div>
)
}
}
export default NavBar2
效果如下:
作用域插槽的实现
export class App extends Component {
// ...
getType(data) {
switch (data) {
case 'tab1':
return <em>{data}</em>;
case 'tab2':
return <button>{data}</button>;
case 'tab3':
return <h3>{data}</h3>;
}
}
render() {
const { tabs, currentIndex } = this.state;
return (
<div>
<h1>组件通信案例练习</h1>
<Tab
// ...
itemType={(data) => this.getType(data)}
/>
当前点击的Tab为:{tabs[currentIndex]}
</div>
)
}
}
export class Tab extends Component {
// ...
render() {
const { tabs, itemType } = this.props;
// ...
return (
<div>
{
tabs.map((tab, index) => <span
// ...
>
{itemType(tab)}
</span>
)
}
</div>
)
}
}
Context
Context的应用场景
非父子组件数据的共享:在开发中,比较常见的数据传递方式是通过props属性自上而下(由父到子)进行传递。但是对于有一些场景,比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)。如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种几余的操作。
但是,如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的,因此React提供了一个Context API
。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props;Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。
🎉 同时补充一个小的知识点:深入JSX的属性展开
示例
❌ 用Context API的示例就不展示了,以后有空再补上吧,可能还是用的Redux多一点 🤪。
写在最后
如果大家喜欢的话可以收藏本专栏,之后会慢慢更新,然后大家觉得不错可以点个赞或收藏一下 🌟。
博客内的项目源码在react-app
分支,大家可以拷贝下来。