前言
兄弟们点个关注
点点赞,有什么建议在评论里留言说一下,一定要和我多多互动啊,这样我才有动力创作出更有品质的文章。
这节课我们用前两节课的知识做一个实践,在实战中巩固我们所学。本来我想借用官方的示例翻译一下,再补充一点就完事了,当写好了后仔细审核一翻后感觉不行,有点乱。官方那个示例真不适合用来做基础教程,就没有发,已经删了。白瞎了我一个下午的时间。本着老码出品必属精品
的原则,今天自己亲手撸一个,咱要么不做,做就要做好。开撸。
目录调整
考虑后面还有好几个知识章节要讲,每个章节肯定还少不了写示例,所以把目录归类一下是有必要的。这节课是第三节,就在src源目录下建个目录Test03
。现在的目录结构应该如下图所示:
启动项目
我们还是用上次的工程项目,用VSCode打开并运行。
cd my-react-app
yarn dev
同样打开浏览器的控制台,以便及时的查看调试信息;
组件创建
React的组件有两种构建形式,一个叫函数组件,一种叫类组件,理解这两种形式非常重要。
1. 函数式组件
React规定,组件名必须以大写的字母开头
,必须要返回一个元素
构成。函数组件以函数的形式构建,这里说的函数就是我们通常理解的函数。如下所示:
//MyButton.jsx
function MyButton(){
return (
<button> 这是一个按钮 </button>
)
}
export default MyButton;
上面我们定义了一个组件叫 MyButton
, 其应用格式为<MyButton></MyButton>
或 <MyButton/>
, 总之,组件标签一定要闭合。在html中我们称为元素,这儿我们称组件, 注意大小写是敏感的。
后面我就用 export default MyButton
语句导出了这个组件,这样我们在其它文件中才能用 import
语句引入。
注意,export 导出有两种格式,一种就是上面的 export default 语句。 一种是 export 语句;我们来看看这两个的区别:
一个组件文件里只能有一个 export default
语句,意思是默认导出项。当引用一个默认导出的组件时,这个名称不一定要求完全匹配。以上面导出示例为列,下面的文件引用时可以这样写:
import MyButton from `./MyButton`;
也可以这样导入
import CustomBtn from './MyButton';
这相当于导入 MyButton 组件的同时给它重新命了个名。那么在这个文件中使用的时候你就要用你重命名的组件名去使用就可以了。
有时候我们把几个相关的组件放在一个文件里一起导出,怎么做呢,比如我们定义了三个按钮,这就是第二种导出方法:
//MyButtons.jsx
export function MyButton1(){
return (
<button> 这是第一个按钮 </button>
)
}
export function MyButton2(){
return (
<button> 这是第二个按钮 </button>
)
}
export function MyButton2(){
return (
<button> 这是第三个按钮 </button>
)
}
export default function MyCompButton(){
return (
<>
<MyButton1/>
<MyButton2/>
<MyButton3/>
</>
)
}
你会发现我的写法有点不一样。是的。由于一个文件里只能有一个 export default
, 所以,其它的组件想要导出就只能以 export
导出了。export 导出的组件在引用的时候名称一定要匹配,除非你显示的指定别名,否则就会出错。
至于这个导出的写法,也有多种格式。 上面我把 export 直接写在了函数前面。当然这是合法的。你还可以这样写:
//MyButtons.jsx
function MyButton1(){
return (
<button> 这是第一个按钮 </button>
)
}
function MyButton2(){
return (
<button> 这是第二个按钮 </button>
)
}
function MyButton3(){
return (
<button> 这是第三个按钮 </button>
)
}
function MyCompButton(){
return (
<>
<MyButton1/>
<MyButton2/>
<MyButton3/>
</>
)
}
export { MyButton1, MyButton2, MyButton3 };
export default MyCompButton;
哪一种方式都行。那么在引用 export 导出的组件的时候也是有区别的:
import MyCompButton, { MyButton1, MyButton2 } from './MyButtons';
你会发现,引用默认导入的组件没有用大括号括起来,而非默认导出的组件我们在引用的时候要用大括号给括起来,这就是区别。现在VSCode很智能,一般在我们书写的时候会有提示。
这里再次强调一下,组件只能返回一个元素,哪怕这个元素是复合元素。就像上面的 MyCompButton
组件,它返回的是一个用空元素 <></>
包裹的多个元素元素。当然也可以是其它元素,比如 <div>...</div>
。如果你的元素结构比较精简,可以直接写在 return 语句的后面,不用小括号。下面的两种写法是等效的。
//写法一
function MyCompButton(){
return <> <MyButton1/> <MyButton2/> <MyButton3/> </>
}
//写法二,结构比较复杂。一行不好写清楚
function MyCompButton(){
return (
<>
<MyButton1/>
<MyButton2/>
<MyButton3/>
</>
);
}
以上是函数组件的写法,这也是官方推荐的写法。还有一种是类组件方式,以React.Component
为基类的组件。
1. 类定义
import React, { Component } from 'react';
class MyComponent extends Component {
// 组件的定义将在这里
}
所有的组件必须继承React.Component
, 以class
关键字来定义;
2. 构造函数Constructor)
在类组件中,构造函数用于初始化组件的状态(state)和绑定事件处理函数。
constructor(props) {
super(props);
this.state = {
// 初始化组件的状态
};
}
3. 渲染方法(Render)
每个React
类组件都必须有一个render
方法,它返回React
元素用于渲染到页面。
render() {
return (
// JSX代码,描述组件的外观, 这个retrun和函数组件内的是一样的。
);
}
4. 状态(State)
类组件可以包含局部状态,状态是组件的数据存储。通过this.state
来访问和更新状态。
this.state = {
key: value,
};
函数组件里的状态的使用方法有点不一样,关于这个状态后面会讲到。状态就是用来动态更新我们组件的。也就是说我们组件中有变化的数据要显示的话,一般就是通过这个state
来更新和引用的。
5. 事件处理函数
事件我们都很熟悉了,由于类组件中的this
指向的问题,事件处理函数通常在构造函数中绑定,用于处理组件中的用户交互。例如,处理按钮点击等事件。
handleClick = () => {
// 处理点击事件的逻辑
};
6. 生命周期方法
React
类组件具有生命周期方法,这些方法在组件的不同阶段被调用。一些常用的生命周期方法包括:
componentDidMount
: 组件挂载到DOM后调用componentDidUpdate
: 组件更新后调用componentWillUnmount
: 组件即将被卸载时调用
componentDidMount() {
// 在组件挂载后执行的逻辑
}
componentDidUpdate(prevProps, prevState) {
// 在组件更新后执行的逻辑
}
componentWillUnmount() {
// 在组件即将被卸载时执行的逻辑
}
7. Props
通过this.props
访问从父组件传递过来的属性。
const { prop1, prop2 } = this.props;
下面是一个简单的示例:
//ClassComponentTest.jsx
import React, { Component } from 'react';
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
handleClick = () => {
this.setState((prevState) => ({
count: prevState.count + 1,
}));
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}
export default Counter;
这是一个简单的计数器组件,演示了一个React
类组件的基本构成。类组件提供了更多的功能和生命周期方法,使得在复杂的应用中更容易管理状态和响应用户交互。
主要概念详解
上面我们走马观花式的大概了解了基本常识,下面就一些主要的功能进行详解。
状态
我们先看下面的例子:
//定义Clock组件 ./Clock.jsx
function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}
打开src目录下的main.jsx文件,修改如下:
//main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import Clock from './Test03/Clock.jsx'
//
function tick() {
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Clock date={new Date()} />
</React.StrictMode>
)
}
setInterval(tick, 1000);
这是一个实时显示当前时间的应用。但这里有个问题,我们理想的应用是Clock组件内就应该实现这个每秒更新UI的功能,而不是在main.jsx
中添加任何代码。
理解的代码应该是我们在App.jsx中这样调用就可以了:
import './App.css'
import "./styles.css";
import Clock from './Test03/Clock';
function App() {
return (
<Clock />
)
}
export default App
显然,目录我们还做不到。
将上面的函数组件转化为类组件
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
props
是组件的传入参数对象。有且只能有这一个参数对象。不管你有没有参数传入,它始终都在。函数组件要显式的传入props
。也就是说组件内部所有的参数的获取都是通过props
这个对象引入的。类组件和函数组件都是如此,一定要记住。
比如上面的 Clock
组件,内部要一个date
的参数,那么外部就要传一个date
参数给组件的props
。像下面这样传入:
<Clock date={date()} />
//因为没有子组件,就不需要向下面这样展开,除非你非要这么做的话。
<Clock date={date()}></Clock>
类组件的props
要用this
来引用。UI元素内要引用变量一定要用 大括号括起来。如:<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
好,有了上面的铺垫,我们继续下面的话题。
将状态添加到类
//Clock.jsx
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
类型组件中的状态(state)
一定要在构造函数中初始化。
constructor(props) {
super(props);
this.state = {date: new Date()};
}
其中 super(props)
一定要是第一行,初始化组件。然后再添加其它初始化代码。状态数据可以是多个,但只能以对象的方式赋给 this.state
这个类属性。上面我们添加了一个date
状态数据。
现在我们将main.jsx还原。
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
在 App.jsx中调用Clock组件
import './App.css'
import "./styles.css";
// import Profile from './Profile'
// import ButtonClickTest from './buttonClickTest';
// import MyCompButton from './Test03/MyButtons';
// import Counter from './Test03/ClassComponentTest';
import Clock from './Test03/Clock';
function App() {
return (
<Clock />
)
}
export default App
添加生命周期
我们可以在组件类上声明特殊方法,以便在组件挂载和卸载时运行一些代码:
我们的目的有以下几个:
- 在具有许多组件的应用程序中,当组件被销毁时,释放组件占用的资源非常重要
- 我们希望在第一次渲染到
DOM
时设置一个计时器。这在React
中称为“挂载” - 我们还希望在删除 生成的
DOM
时清除该计时器。这在React
中称为“卸载” - 该方法在组件输出呈现到
DOM
后运行。这是设置计时器的好地方:componentDidMount()
虽然它是由 React 本身设置的,并且具有特殊的含义,但如果你需要存储一些不参与数据流的东西(如计时器 ID),你可以自由地手动向类添加额外的字段。
class Clock extends React.Component {
//当传递给们这个 <Clock /> 组件时,React 调用组件的构造函数。由于需要显示当前时间,因此它使用包含
//当前时间的对象进行初始化。我们稍后将更新此状态。
//请注意我们如何将计时器 ID 保存在 `this.timerID` 上。
constructor(props) {
super(props);
this.state = {date: new Date()};
}
//然后 React 调用组件的方法。这个方法定时的通过 setState语句更改 state
//然后,React 更新 DOM 以匹配渲染输出
tick() {
this.setState({
date: new Date()
});
}
//在组件第一次加载时执行,注意是第一次加载,也就是说这个componentDidMount生命周期只执行一次。
componentDidMount() {
//给类添加一个timerID属性。js类属性不需要先定义,没那么严格。或都你想更加标准一点,你就在构造函数中添加
//一个这样的语句:this.timerID = null;
this.timerID = setInterval(
() => this.tick(),
1000
);
}
//我们将在生命周期方法中释放计时器
componentWillUnmount() {
clearInterval(this.timerID);
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
现在这个时钟每一秒都在滴答作响。是不是很优美。
正确的使用State
以下三点必须要牢记
- 不直接修改
state
, 如:
// 这种用法是错误的
this.state.comment = 'Hello';
//正确的用法是这样的:通过setState来更改state的值
this.setState({comment: 'Hello'});
this.state
的初始化一定要在构造函数中完成。对于类组件来说一定要切记这一点。
- 状态更新可能是异步的, 因此我们不应依赖它们的值来计算下一个状态。
错误的做法
this.setState({
counter: this.state.counter + this.props.increment,
});
正确的做法是:请传递一个函数而不是传递一个对象的形式。该函数将接收先前的状态作为第一个参数,并将应用更新时的 props
作为第二个参数:
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
我们在上面使用了箭头函数,但它也适用于常规函数:
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});
- 状态的更新是合并操作的。 当你调用
setState()
时,React
会将你提供的对象合并到当前状态中。
例如,您的状态可能包含几个自变量:
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
然后,您可以分别调用setState()
单独更新它们:
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
这种状态也叫本地状态,因为除了拥有和设置它的组件之外,任何组件都无法访问它。但是你要传递状态到子组件怎么办呢,我们可以将状态作为 props 传递给其子组件。
<FormattedDate date={this.state.date} />
假如子组件定义如下,这里用函数组件定义了子组件:
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
函数组件中的状态
函数组件中我们如果要定义状态,则比较直观。我们用 useState(value)
这个Hook来定义。可以定义多个。react会自动管理我们的状态。
//ClockFunc.jsx
import React, { useState } from 'react';
function ClockFunc(props) {
const [number1, setNumber1] = useState(30);
const [number2, setNumber2] = useState(100);
return <div> { `${namber1} : ${namber2}` } </div>
// 上面的return 等同于下面的return ,上面采用了ES中的字符串模板表达式的形式;
// return <div> { namber1 + " : " + number2 } </div>
}
export default ClockFunc;
上面我们定义了两个state
, 一个是number1
, 一个是number2
, 两个状态都有一个专用的更新工具分别是 seNumber1
和 setNumber2
, 这个更新函数的名称可以任意取,但为了直观,我们一般用set...
的形式来取名。
如果我们想要更新number1
, 则可认这样写: setNumber1(60)
, 那么number1
这时就被修改为60
, 与之相关的渲染也会得到更新。关于函数组件中的其它用法和形式我们后面会专门讲解。
事件处理
在 html 中,事件一般是这样的:
<button onclick="activateLasers()">
Activate Lasers
</button>
而在jsx中,是这样写的,略有不同
<button onClick={activateLasers}>
Activate Lasers
</button>
另一个区别是你不能返回false
以防止 React 中的默认行为。您必须显式调用preventDefault
。例如,对于纯 HTML,为了防止提交的默认表单行为,您可以这样编写:
<form onsubmit="console.log('You clicked submit.'); return false">
<button type="submit">Submit</button>
</form>
但React
中是这样的:
function Form() {
function handleSubmit(e) {
e.preventDefault();
console.log('You clicked submit.');
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
);
}
React
根据 W3C
规范定义了这些合成事件,因此您无需担心跨浏览器兼容性。React
事件的工作方式与原生事件并不完全相同。
使用 React 时,通常不需要在创建 DOM 元素后调用addEventListener
来添加监听器。相反,只需在最初呈现元素时提供侦听器。
使用 ES6
类定义组件时,常见的模式是将事件处理程序作为该类的方法。例如,此组件Toggle
呈现一个按钮,允许用户在“ON”和“OFF”状态之间切换:
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
//在类组件中,事件必须要在构造函数中进行绑定 this 后才能使用, 不然事件内部无法捕获 this
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
如果你不想在构造函数中绑定 this
,那么就要用显式的调用回调函数。如下所示:
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// 下面的 this 将被传递到 handleClick 事件中。
return (
<button onClick={() => this.handleClick()}>
Click me
</button>
);
}
}
给事件传递参数
在循环中,通常希望将额外的参数传递给事件处理程序。例如下面是等效的。
<button onClick={(e) => this.deleteRow(id, e)}> 删除行 </button>
<button onClick={this.deleteRow.bind(this, id)}> 删除行 </button>
在这两种情况下,代表React事件的e
参数将作为ID
之后的第二个参数传递。使用箭头功能,我们必须明确地将其传递,但是使用bind
绑定。
好了,今天就到这里。下节继续;