文章目录
1. React的组件化开发
1.1 什么是组件化开发
组件化是一种分而治之的思想:
- 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。
- 但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。
我们需要通过组件化的思想来思考整个应用程序:
- 如将一个完整的页面分成很多个组件;
- 每个组件都用于实现页面的一个功能块;
- 而每一个组件又可以进行细分;
- 而组件本身又可以在多个地方进行复用;
1.2 React的组件化
组件化是React的核心思想,也是学习React的重点
- 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。
- 任何的应用都会被抽象成一颗组件树。
组件化思想的应用:
- 有了组件化的思想,我们在之后的开发中就要充分的利用它。
- 尽可能的将页面拆分成一个个小的、可复用的组件。
- 这样让我们的代码更加方便组织和管理,并且扩展性也更强。
React 的组件相对于 Vue 更加的灵活和多样,按照不同的方式可以分成很多类别组件:
- 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component);
- 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component);
- 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component);
上面概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离:
- 函数组件、无状态组件、展示型组件主要关注UI的展示;
- 类组件、有状态组件、容器型组件主要关注数据逻辑;
- 还有很多组件的其他概念:比如异步组件、高阶组件等(后续学习)。
1.3 类组件和函数组件
1.3.1 类组件
类组件的定义有如下要求:
- 组件的名称是大写字母开头(无论类组件还是函数组件)
- 类组件需要继承自
React.Component
- 类组件必须实现
render
函数
使用class定义一个组件:
constructor
是可选的,我们通常在 constructor 中初始化一些数据;this.state
中维护的就是我们组件内部的数据;render()
方法是 class 组件中唯一必须实现的方法;
import React from "react";
// 1. 类组件
class App extends React.Component {
constructor() {
super();
this.state = {
message: "hello react"};
}
render() {
// const { message } = this.state;
// 1. react元素:通过jsx编写的代码就会被编译成React.createElement
// 所有返回的是 react 元素
return <div>{
message}</div>;
// 2. 返回数组或 fragments
// return ["aaa", "bbb", "ccc"];
//return [<h1>元素1</h1>, <div>元素2</div>, <button>元素3</button>];
// 3. 可以返回 字符串 或数值 或null , true, false 等,
// return "hello";
}
}
export default App;
当 render 被调用时,它会检查 this.props
和 this.state
的变化并返回以下类型之一:
- React 元素:
- 通常通过 JSX 创建。
- 例如,
<div />
会被 React 渲染为 DOM 节点,<MyComponent />
会被 React 渲染为自定义组件; - 无论是
<div />
还是<MyComponent />
均为 React 元素。
- 数组或 fragments:使得 render 方法可以返回多个元素。
- Portals:可以渲染子节点到不同的 DOM 子树中。
- 字符串或数值类型:它们在 DOM 中会被渲染为文本节点
- 布尔类型 null undefined:什么都不渲染。
1.3.2 函数组件
函数组件是使用 function
来进行定义的函数,这个函数会返回和类组件中render函数返回一样的内容。
函数组件有自己的特点(限制)(后面学到 hooks
会解决如下限制):
- 也会被更新并挂载,但是没有生命周期函数;
- this关键字不能指向组件实例(因为没有组件实例);
- 没有内部状态(state);
很简洁,无需导入任何依赖, 即可定义一个函数组件:
// 函数式组件
function App() {
// 返回值要求:和类组件中的 render 函数返回值规则相同
return <h2>hello react</h2>;
}
export default App;
2. React组件生命周期
2.1 认识生命周期
很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期;
React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能;
生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段;
- 比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程;
- 比如更新过程(Update),组件状态发生变化,重新更新渲染的过程;
- 比如卸载过程(Unmount),组件从DOM树中被移除的过程;
React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:
- 比如实现
componentDidMount
函数:组件已经挂载到DOM上时,就会回调; - 比如实现
componentDidUpdate
函数:组件已经发生了更新时,就会回调; - 比如实现
componentWillUnmount
函数:组件即将被移除时,就会回调; - 我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能;
当前谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的;(后面会学习 hooks 来模拟一些生命周期的回调)
2.2 生命周期图
先来学习一下最基础、最常用的生命周期函数:
下面这张图同时显示了一些不常用的 周期函数
2.3 生命周期函数
1、Constructor
- 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
- constructor中通常只做两件事情:
- 通过给 this.state 赋值对象来初始化内部的state;
- 为事件绑定实例(this);
2、componentDidMount
componentDidMount()
会在组件挂载后(插入 DOM 树中)立即调用。componentDidMount
中通常会做的操作:- 依赖于DOM的操作可以在这里进行;
- 在此处发送网络请求就最好的地方;(官方建议)
- 可以在此处添加一些订阅(同时要在
componentWillUnmount
取消订阅,否则容易内存泄露);
3、componentDidUpdate
componentDidUpdate()
会在更新后会被立即调用,首次渲染不会执行此方法。- 当组件更新后,可以在此处对 DOM 进行操作;
- 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。
4、componentWillUnmount
componentWillUnmount()
会在组件卸载及销毁之前直接调用。- 在此方法中执行必要的清理操作;
- 例如,清除 timer,取消网络请求或清除在
componentDidMount()
中创建的订阅等;
class App extends React.Component {
// 1. 执行构造方法
constructor() {
super();
this.state = {
message: "hello world",count: 0};
console.log("constructor 被执行了");
}
// 2. 执行render函数
render() {
console.log("render 被执行了");
const {
message, count } = this.state;
return (
<div>
<h2>{
message}-{
count}</h2>
<button onClick={
(e) => this.change()}>修改</button>
</div>
);
}
// 3. 组件被渲染(挂载)到DOM上
componentDidMount() {
console.log("componentDidMount 被执行了");
}
// 4. 组件的DOM更新完成
componentDidUpdate() {
console.log("componentDidUpdate 被执行了");
}
// 5. 组件从DOM中卸载(移除)掉
componentWillUnmount() {
console.log("componentWillUnmount 被执行了");
}
change() {
this.setState({
message: "hello react", count: this.state.count + 1 });
}
}
2.4 不常用生命周期函数
除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数:
getDerivedStateFromProps
:state 的值在任何时候都依赖于 props时使用;该方法返回一个对象
来更新state;getSnapshotBeforeUpdate
:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一
些信息(比如说滚动位置);shouldComponentUpdate
:该生命周期函数其实也会用,后续学习性能优化时再做深入;
更详细的生命周期相关的内容,可以参考官网:https://zh-hans.reactjs.org/docs/react-component.html
3. React组件间的通信
3.1 组件之间的嵌套
组件之间存在嵌套关系:
- 若一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护;
- 所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件;
- 再将这些组件组合嵌套在一起,最终形成我们的应用程序;
// 这里就是在App组件中 嵌入了 Header、Main、Footer组件,
// 这里组件又可以嵌套其他组件
import React, {
Component } from 'react'
import Footer from './childrenComponent/Footer'
import Header from './childrenComponent/Header'
import Main from './childrenComponent/Main'
class App extends Component {
render() {
return (
<div>
<Header></Header>
<Main></Main>
<Footer></Footer>
</div>
)
}
}
export default App
3.2 认识组件间的通信
在开发过程中,我们会经常遇到需要组件之间相互进行通信:
- 比如App可能使用了多个 Header 组件,每个地方的 Header 展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示;
- 又比如我们在 Main组件 中一次性请求了 Banner 数据和 ProductList 数据,那么就需要传递给他们来进行展示;
- 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;
总之,在一个React项目中,组件之间的通信是非常重要的环节;
3.3 父传子
3.3.1 类或函数组件的父传子
父组件在展示子组件,可能会传递一些数据给子组件:
- 父组件通过 属性=值 的形式来传递给子组件数据;
- 子组件通过 props 参数获取父组件传递过来的数据;
import React, {
Component } from "react";
// App 组件 包含子组件 Child
class App extends Component {
constructor() {
super();
this.state = {
age: 20};
}
render() {
const {
age } = this.state;
return (
<div>
<Child name="foo" age={
age}></Child>
</div>
);
}
}
class Child extends Component {
constructor(props) {
super();
}
render() {
const {
name, age } = this.props;
return (
<div>
<h2>我是子组件</h2>
<p>父组件传递过来的 name为:{
name},age为{
age}</p>
</div>
);
}
}
上面为传给类组件的形式;下面为传给函数组件的形式
function Child(props) {
const {
name } = props;
return (
<div><p>父组件传递过来的 name为:{
name},</p></div>
);
}
class App extends Component {
render() {
return (
<div>
<Child name="foo"></Child>
</div>
);
}
}
3.3.2 参数propTypes
对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说:
- 当然,如果你项目中默认继承了 Flow 或者 TypeScript,那么直接就可以进行类型验证;
- 但是,即使我们没有使用Flow或者TypeScript,也可以通过 prop-types 库来进行参数验证;
- 从 React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types 库
import PropTypes from 'prop-types';
class Greeting extends React.Component {
render() {
return (
<h1>Hello, {
this.props.name}</h1>
);
}
}
// 校验 父组件传入的 name 为 string 且为必传项,否则报警告
Greeting.propTypes = {
name: PropTypes.string.isRequired
};
// 指定 props 的默认值:
// Greeting.defaultProps = {
// name: 'Stranger'
// };
- 更多的验证方式,可以:参考官网
3.4 子传父
某些情况,我们也需要子组件向父组件传递消息:
- 在vue中是通过自定义事件来完成的;
- 在React中同样是通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;
【案例说明】
- 将按钮封装到子组件中:CounterButton;
- CounterButton 发生点击事件,将内容传递到父组件App中,修改counter的值;
- 为了方便展示,这里就把父子组件放同一个文件中
// 父组件App
class App extends Component {
constructor() {
super();
this.state = {
count: 100, // 父组件的状态
};
}
changeCount(step) {
// 预留修改 count 的方法
this.setState({
count: this.state.count + step});
}
render() {
const {
count } = this.state;
return (
<div>
<h2>当前计数为:{
count}</h2>
{
/* 下面给子组件传递一个函数,让子组件来触发它 */}
<CounterButton addEvent={
(step) => {
this.changeCount(step)}}></CounterButton>
</div>
);
}
}
// 子组件 CounterButton
class CounterButton extends Component {
addCount(step) {
// 触发父组件传过来的函数,并传递参数
this.props.addEvent(step);
}
render() {
return (
<div>
<button onClick={
(e) => this.addCount(1)}>+1</button>
<button onClick={
(e) => this.addCount(5)}>+5</button>
</div>
);
}
}
4. 插槽(slot)
4.1 介绍
- 在开发中,我们抽取了一个组件,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的 div 、span 等等这些元素。
- 我们应该让使用者可以决定某一块区域到底存放什么内容。
- 这种需求在 Vue 当中有一个固定的做法是通过
slot
来完成的 - React对于这种需要插槽的情况非常灵活,有两种方案可以实现:
- 组件的 children 子元素;
- props属性传递React元素;
4.2 children实现插槽
每个组件都可以获取到 props.children
:它包含组件的开始标签和结束标签之间的内容。
// 子组件
class NavBar extends Component {
render() {
const {
children } = this.props;
return (
<div className="nav-bar">
{
/* 如过插入了多个元素,则children为数组,
若为插入一个元素,则children为那个元素
*/}
<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() {
return (
<div>
<NavBar>
{
/* 这里传入三个子元素 */}
<button>左侧内容</button>
<input type="text" placeholder="中间内容" />
<h2>右侧内容</h2>
</NavBar>
</div>
);
}
}
通过 children 实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的元素;
4.3 props实现插槽
另外一种方案就是使用 props 实现:通过具体的属性名,可以让我们在传入和获取时更加的精准;
class NavBar extends Component {
render() {
const {
leftSlot, centerSlot, rightSlot } = this.props;
return (
<div className="nav-bar">
{
/* 如过插入了多个元素,则children为数组,
若为插入一个元素,则children为那个元素
*/}
<div className="left">{
leftSlot}</div>
<div className="center">{
centerSlot}</div>
<div className="right">{
rightSlot}</div>
</div>
);
}
}
class App extends Component {
render() {