react基础

react基础(组件+组合组件)

这篇文章的教程

Mosh的react课程

假设我们已经了解一些JavaScript的基础和OOP编程思想了

vscode中ctrl+p 实现文件查找

下载自动优化插件prettier,在保存时自动优化代码增加可读性

Component

Setting Up the Project

1
2
create-react-app XXXX;
npm start;

Your First React Component

我们接下来想做一个类似购物车的计数器

在src中新建文件夹components,里面新建counter.jsx

JSX是一种JavaScript的语法扩展,运用于 React 架构中,其格式比较像是模版语言,但事实上完全是在 JavaScript 内部实现的。元素是构成React应用的最小单位,JSX就是用来声明React当中的元素,React使用JSX来描述用户界面。

JSX可以使用引号来定义以字符串为值的属性:

const element = \

\</div>;

也可以使用大括号来定义以JavaScript表达式为值的属性:

const element = \;

下载 simple React Snippets 插件,imrc+cc即可生成代码

1
2
3
4
5
6
7
8
9
import React, { Component } from 'react';
class extends Component {
state = { }
render() {
return ( );
}
}

export default ;

修改一下即可

1
2
3
4
5
6
7
import React, { Component } from "react";
class Counter extends Component {
render() {
return <h1>Hello World</h1>;
}
}
export default Counter;

index.js中做如下修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
/*添加component*/import Counter from './components/counter';
ReactDOM.render(
<React.StrictMode>
/*修改APP*/ <Counter />
</React.StrictMode>,
document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

结果如下:

Specifying Children

我如果想在h1后面放一个按钮,我可以这么写

1
2
3
4
5
6
7
import React, { Component } from "react";
class Counter extends Component {
render() {
return <div><h1>Hello World</h1><button>Increment</button></div>
}
}
export default Counter;

Babel 会 自动React.createElement(‘div’)

因为下载了prettier,所以在保存时自动优化成下列代码

1
2
3
4
5
6
7
8
9
10
11
12
import React, { Component } from "react";
class Counter extends Component {
render() {
return (
<div>
<h1>Hello World</h1>
<button>Increment</button>
</div>
);
}
}
export default Counter;

观察源码我们可以发现我们生成的代码和刚才写的是一样的。当然我们也可以实现移除div标签。

通过把原来的div改成 React.Fragment即可

1
2
3
4
5
6
7
8
9
10
class Counter extends Component {
render() {
return (
/*修改*/ <React.Fragment>
<h1>Hello World</h1>
/*修改*/ <button>Increment</button>
</React.Fragment>
);
}
}

Embedding Expressions

我想在原来HelloWorld基础上变为动态文字,这时候我们在Counter类中添加一个新的属性state

此外我们需要把原来的h1 改成span 。在花括号中可以使用任何js的变量,包括表达式,或者这个类中的某个属性

现在我们只要修改count的值,网页会实现动态的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component } from "react";
class Counter extends Component {
state = {
count: 10,
};
render() {
return (
<React.Fragment>
/*修改*/ <span>{this.state.count}</span>
<button>Increment</button>
</React.Fragment>
);
}
}

export default Counter;

此外,花括号里还可以这样写:

1
<span>{2+2}</span>

也可以传入一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from "react";
class Counter extends Component {
state = {
count: 10,
};
render() {
return (
<React.Fragment>
<span>{this.formatCount()}</span>
<button>Increment</button>
</React.Fragment>
);
}
formatCount() {
const { count } = this.state;// 对象解构
return count === 0 ? "Zero" : count;
}
}

export default Counter;

那么count改成0,显示Zero

1
2
3
4
formatCount() {
const { count } = this.state;// 对象解构
return count === 0 ? <h1>Zero</h1> : count;
}

我们也可以直接输出一个标题,源码则变成了span中又一对h1

Setting Attributes

怎么样给元素设置属性?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { Component } from "react";
class Counter extends Component {
state = {
count: 0,
imageUrl: "http://picsum.photos/200",//一个随机获取图片的网站
};
render() {
return (
<React.Fragment>
<img src={this.state.imageUrl} alt="" />
<span>{this.formatCount()}</span>
<button>Increment</button>
</React.Fragment>
);
}
formatCount() {
const { count } = this.state;
return count === 0 ? "Zero" : count;
}
}

export default Counter;

上面的代码放到一边,这里说一下 className 因为js中的class是关键词,所以jsx中class标签对应的名称就是className

利用bootstrap中的css样式我们可以做以下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { Component } from "react";
class Counter extends Component {
state = {
count: 0,
};
render() {
return (
<React.Fragment>
<span className="badge badge-primary m-2">{this.formatCount()}</span>
<button className="btn btn-secondary btn-sm">Increment</button>
</React.Fragment>
);
}
formatCount() {
const { count } = this.state;
return count === 0 ? "Zero" : count;
}
}
export default Counter;

有时候我想用一些确切的有针对性地样式,那么可以使用style

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { Component } from "react";
class Counter extends Component {
state = {
count: 0,
};
styles = {
fontSize: 10,
fontWeight: "bold",
};
render() {
return (
<React.Fragment>
<span style={this.styles} className="badge badge-primary m-2">
{this.formatCount()}
</span>
<button style={this.styles} className="btn btn-secondary btn-sm">
Increment
</button>
</React.Fragment>
);
}
formatCount() {
const { count } = this.state;
return count === 0 ? "Zero" : count;
}
}
export default Counter;

这就是style的修改,我们先定义一个对象,然后在jsx中引用,当然我们如果用 行内样式表 。

Rendering Classes Dynamically

如何动态渲染css样式呢?现在0是蓝底的,我们想要在0的时候变成黄色的,其他情况是蓝色的,怎么办呢?

蓝色:badge badge-primary 黄色:badge badge-warning

通过{}的性质,我们可以新建一个classes字符串,然后对其做一个逻辑判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { Component } from "react";
class Counter extends Component {
state = {
count: 0,
};
styles = {
fontSize: 10,
fontWeight: "bold",
};
render() {
let classes = "badge m-2 badge-";
classes += this.state.count === 0 ? "warning" : "primary";//逻辑判断
return (
<React.Fragment>
<span style={this.styles} className={classes}>
{this.formatCount()}
</span>
<button style={this.styles} className="btn btn-secondary btn-sm">
Increment
</button>
</React.Fragment>
);
}
formatCount() {
const { count } = this.state;
return count === 0 ? "Zero" : count;
}
}
export default Counter;

但是上面的写法有缺点,它污染了render()方法。更好的方法是把这两行放到单独的方法当中去这样计算的细节就不会和渲染耦合了

通过vscode中的重构,我们可以这样。ctrl+shift+R, 把那两行新建一个方法getBadgeClasses()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { Component } from "react";
class Counter extends Component {
state = {
count: 10,
};
styles = {
fontSize: 10,
fontWeight: "bold",
};
render() {
return (
<React.Fragment>
<span style={this.styles} className={this.getBadgeClasses()}>
{this.formatCount()}
</span>
<button style={this.styles} className="btn btn-secondary btn-sm">
Increment
</button>
</React.Fragment>
);
}
getBadgeClasses() {
let classes = "badge m-2 badge-";
classes += this.state.count === 0 ? "warning" : "primary";
return classes;
}

formatCount() {
const { count } = this.state;
return count === 0 ? "Zero" : count;
}
}
export default Counter;

Rendering Lists

怎么渲染一个列表呢?可以利用map函数。同样的,想要动态渲染利用花括号

但是我们在每个列表项生成的时候,需要设置它的key属性。每个key的唯一性,只在当前列表有效。这样当react中的虚拟dom改变的时候,react能够迅速反应过来什么组件改变了,然后重新渲染浏览器dom以确保同步

在这里,我们直接把tag作为key值即可,因为tag是两两不同的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, { Component } from "react";
class Counter extends Component {
state = {
count: 0,
tags: ["tag1", "tag2", "tag3"],
};
styles = {
fontSize: 10,
fontWeight: "bold",
};
render() {
return (
<React.Fragment>
<span style={this.styles} className={this.getBadgeClasses()}>
{this.formatCount()}
</span>
<button style={this.styles} className="btn btn-secondary btn-sm">
Increment
</button>
<ul>
{this.state.tags.map((tag) => (
<li key={tag}>{tag}</li>
))}
</ul>
</React.Fragment>
);
}
getBadgeClasses() {
let classes = "badge m-2 badge-";
classes += this.state.count === 0 ? "warning" : "primary";
return classes;
}

formatCount() {
const { count } = this.state;
return count === 0 ? "Zero" : count;
}
}
export default Counter;

Conditional Rendering

条件渲染

如果数组列表中至少有一项,那么我们就渲染它,否则我们就输出”No Tags”

在jsx中没有if之类的条件判断,因为jsx不是模板化引擎。所以我们要回到js层面构建这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { Component } from "react";
class Counter extends Component {
state = {
count: 0,
// tags: ["tag1", "tag2", "tag3"]
tags: [],
};
renderTags() {
if (this.state.tags.length === 0) return <p>There are No Tags!</p>;
return (
<ul>
{this.state.tags.map((tag) => (
<li key={tag}>{tag}</li>
))}
</ul>
);
}
render() {
return <React.Fragment>
{/* 我们也可以直接在这里判断,通过&& */}
{this.state.tags.length ===0&&"Please create a new tag!"}
{this.renderTags()}</React.Fragment>;
}
}
export default Counter;

在render中我们用&&来进行简单的逻辑运算如果tags.length===0 才可以运行下面的代码。如果下面一个条件不是是非判断,而是字符串,那么如果是空串即false,非空串即为true

Handling Events

所有的react元素都有基于dom事件的属性,例如按钮元素有一个onClick属性,还有双击事件、鼠标移入事件(onKey…)。

处理onClick事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Counter extends Component {
state = {
count: 0
};
handleIncrement() {
console.log('Increment Clicked');
}
render() {
return (
<div>
<button
onClick={this.handleIncrement}
className='btn btn-secondary btn-sm'>
Increment
</button>
</div>
);
}
}

值得注意的是,我们在”onClick={this.handleIncrement()}”中并没有调用方法,只是传入了一个引用。这与在js中的处理方式不同:在写行内代码的时候,我们直接调用 js 函数。

和下面的调用不同,因为我们需要一个返回值来给className赋值,所以我们要调用一个方法

1
2
3
<span  className={this.getBadgeClasses()}>
{this.formatCount()}
</span>

现在按按钮的时候console就会显示Increment Clicked了

现在我们要实现当鼠标点击Increment的时候,显示\标签中的数字。

我们不能直接这么写!这样浏览器就爆炸了。

1
2
handleIncrement() {
console.log('Increment Clicked'this.state.count);

如果我们单单显示this,那么会指向一个undefined。

Binding Event Handlers

回到上面的问题。为什么会出现这个问题呢?明明是在类里面啊。

js中,调用函数的方式不同,this指代的对象也可能不同。如果一个函数被方法调用,那么this总是返回那个对象的引用。但是因为这个函数被以独立函数的方式引用,this会返回默认的全局对象(严格模式下为undefined)

那你会说我tm也没独立调用啊,我onClick={this.handleIncrement} 中有this啊。。

但是注意,这不是调用一个方法!!!

如果我们改成onClick={this.handleIncrement()} 那么我们会发现确实会显示 Increment Clicked 0(或者别的数字) 。而且点击按钮是没用的,因为他会默认你已经调用了这个方法了,而不是触发调用。

这就很矛盾了。如何解决,我们需要用下面这篇文章提到的bind()方法

https://jasonxqh.github.io/2020/05/26/OOP-in-JavaScript/#more

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React, { Component } from "react";
class Counter extends Component {
state = {
count: 0,
};
// 在这里我们定义一个 Counter 的构造函数,因为Counter继承自Component,所以我们先要super一下
// 我们知道当调用bind的时候,会返回一个指向括号内对象的方法,我们再用handleIncrement接收
// 接受好以后我们就实现了让handleIncrement中的所有的this都指向了一个对象,也就是Counter对象
constructor() {
super();
this.handleIncrement = this.handleIncrement.bind(this);
}

handleIncrement() {
console.log("Increment Clicked", this.state.count);
}

render() {
return (
<React.Fragment>
<span className={this.getBadgeClasses()}>{this.formatCount()}</span>
<button
onClick={this.handleIncrement}
className="btn btn-secondary btn-sm"
>
Increment
</button>
</React.Fragment>
);
}
getBadgeClasses() {
let classes = "badge m-2 badge-";
classes += this.state.count === 0 ? "warning" : "primary";
return classes;
}

formatCount() {
const { count } = this.state;
return count === 0 ? "Zero" : count;
}
}
export default Counter;

当然,我们也可以不适用constructor来实现这个功能,那就老样子,箭头函数,箭头函数中的this就继承自构造这个函数的对象。

1
2
3
handleIncrement=()=>{
console.log("Increment Clicked", this.state.count);
}

而且我认为这种方式更加直观,能更好的帮助我们理解为什么调用引用而不是直接调用方法。

Updating the State

现在我们要学习更新状态。也就是说当我们click的时候,要实现前面的数字+1

我们不能想的太简单,直接这么写.这是没有用的。

1
2
3
handleIncrement=()=>{
this.state.count++;
}

事实上我们代码中的count确实++ 了,但是react不知道,所以视图并没有更新。为了解决这个问题,我们需要用到继承自component对象的一个方法setState() 这个方法告诉react state已经更新了,然后让react去同步视图

这里我们可以向setState方法传入我们需要实时修改的参数,我们传入一个对象,对象中的this.state.count属性会和state中的合并。或者当state中有相同的属性时,就会被覆盖

1
2
3
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
};

What Happens When State Changes

原理就是当我们点击了increment按钮的时候,调用了setState方法。这个方法告诉react,state要变化了。react会计划调用一次render方法,也许未来某个时候会调用。这就是异步调用,他将在未来发生。所以在未来某一点调用render的时候 这个方法会返回新的react元素。

就像这样,然后react会对比他们俩修改了什么地方。在这里,它会发现是span的内容变换了,因为span调用的是count属性的值。然后react会找到真实的DOM,找到对应的span,修改它让它与虚拟的DOM同步。所以除了span标签以外的所有地方都没有发生改变

Passing Event Arguments

如何传递事件参数?现在我们的handleIncrement 没有参数,但事实上我们在日常生活中常常需要传入参数。 比如这里我们处理了装有商品的购物车。当我们点击增加按钮的时候想传入一个商品对象的id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React, { Component } from "react";
class Counter extends Component {
state = {
count: 0,
};

handleIncrement = (product) => {
console.log(product);
this.setState({ count: this.state.count + 1 });
};

render() {
return (
<React.Fragment>
<span className={this.getBadgeClasses()}>{this.formatCount()}</span>
<button
onClick={() => this.handleIncrement({ id: 1 }) }
className="btn btn-secondary btn-sm"
>
Increment
</button>
</React.Fragment>
);
}
getBadgeClasses() {
let classes = "badge m-2 badge-";
classes += this.state.count === 0 ? "warning" : "primary";
return classes;
}

formatCount() {
const { count } = this.state;
return count === 0 ? "Zero" : count;
}
}
export default Counter;

这样当我们渲染一组购物车商品的时候 我们可以用map方法访问商品的对象

我们当然可以新建一个doHandleIncrement 方法,但是这样会让代码更加凌乱,可读性不好,我们可以直接箭头函数。

当然我们可以不输出id:1 那么我们就直接传入商品对象

onClick = {() => this.handleIncrement(product)}即可

所以我们如果想对一个event handler 传递一个参数的话,我们就写一个箭头函数,在箭头函数中调用event handler并传入参数即可

Setting Up the Vidly Project 新建vidly工程

https://www.bilibili.com/video/BV1Sb411P79t?p=36

Exercises

在网页中做一张类似的表格。

re

我们需要在app.js中进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
import React from "react";
import Movies from "./components/Movies";
//把原来的<app/>换成新的元素<Movies/>
function App() {
return (
<main role="main" className="container">
<Movies/>
</main>
);
}

export default App;

Building the Movies Component

和上面一样,我们先从fakeMovieService中引入getMovies()方法。

然后建一个state来存放movies对象

然后见一张表格,可以用快捷键 table.table>thead>tr>th*4 (表头)和 tbody>tr>td*4 (表身)

利用map函数,对movies中的每一个movie对象进行渲染,每部电影生成一行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React, { Component } from "react";
import { getMovies } from "../services/fakeMovieService";
class Movies extends Component {
state = {
movies: getMovies(),
};
handleDelete = (movie) => {
//..
};
render() {
return (
<table className="table">
<thead>
<tr>
<th>Title</th>
<th>Genre</th>
<th>Stock</th>
<th>Rate</th>
</tr>
</thead>
<tbody>
{this.state.movies.map((movie) => (
<tr key = {movie._id}>
<td>{movie.title}</td>
<td>{movie.genre.name}</td>
<td>{movie.numberInStock}</td>
<td>{movie.dailyRentalRate}</td>
</tr>
))}
</tbody>
</table>
);
}
}
export default Movies;

Deleting a Movie

要实现删除,也就是说我们在点击的时候要新建一张表格,新的这张表格不包含被删除的那行,然后再通过setState函数实现更新

我们在这里通过filter函数,对movies中的每一个元素进行判断是否与被删除id相等。然后用movies来接收filter函数返回一个新的列表。
利用setState,传入一个对象,然后和state中重复的属性会被新的属性覆盖,达到删除效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React, { Component } from "react";
import { getMovies } from "../services/fakeMovieService";
class Movies extends Component {
state = {
movies: getMovies(),
};

handleDelete = (movie) => {
const movies = this.state.movies.filter((m) => m._id !== movie._id);
this.setState({ movies: movies });
};
render() {
return (
<table className="table">
<thead>
<tr>
<th>Title</th>
<th>Genre</th>
<th>Stock</th>
<th>Rate</th>
</tr>
</thead>
<tbody>
{this.state.movies.map((movie) => (
<tr key={movie._id}>
<td>{movie.title}</td>
<td>{movie.genre.name}</td>
<td>{movie.numberInStock}</td>
<td>{movie.dailyRentalRate}</td>
<td>
<button
onClick={() => this.handleDelete(movie)}
className="btn btn-danger btn-sm"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
);
}
}

export default Movies;

Conditional Rendering

怎么进行条件渲染呢?就是如果电影数是0,那么显示There are no movies in the database

否则显示Showing n(行数) movies in the database

注意,react中return一个component只能返回一个元素,如果想要把两个元素合并,需要用到\

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import React, { Component } from "react";
import { getMovies } from "../services/fakeMovieService";
class Movies extends Component {
state = {
movies: getMovies(),
};
handleDelete = (movie) => {
const movies = this.state.movies.filter((m) => m._id !== movie._id);
this.setState({ movies: movies });
};
render() {
if (this.state.movies.length === 0)
return <h1>There are no movies in the database </h1>;
return (
<React.Fragment>
<h2>Showing {this.state.movies.length} movies in the database</h2>
<table className="table">
<thead>
<tr>
<th>Title</th>
<th>Genre</th>
<th>Stock</th>
<th>Rate</th>
</tr>
</thead>
<tbody>
{this.state.movies.map((movie) => (
<tr key={movie._id}>
<td>{movie.title}</td>
<td>{movie.genre.name}</td>
<td>{movie.numberInStock}</td>
<td>{movie.dailyRentalRate}</td>
<td>
<button
onClick={() => this.handleDelete(movie)}
className="btn btn-danger btn-sm"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</React.Fragment>
);
}
}

export default Movies;

效果如下:

Composing Components 把组键组合

Introduction

react程序基本上就是一个组件树,我们可以将组件组合在一起,变成一个复杂的UI

Composing Components

现在我们要通过一个组件来渲染一组counters

在component文件夹中新建counters.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from "react";
import Counter from "./counter";
class Counters extends Component {
state = {
counters:[
{id: 1,value:4},
{id: 2,value:0},
{id: 3,value:0},
{id: 4,value:0}
]
};
render() {
return (
<div>
{this.state.counters.map(counter=><Counter key = {counter.id}/>)}
</div>
);
}
}

export default Counters;

但是我们现在如果修改counters列表中对象的value值,在Dom上并不会显示。接下来我们就要解决这个问题

Passing Data to Components

每个组件都有他的props属性他会用用我们向counter组件传递的所有属性,在\ 中我们不仅可以添加key(key值不算在props属性内),还可以添加value值,selected值(默认为true),就像这样

1
2
3
4
5
6
7
render() {
return (
<div>
{this.state.counters.map(counter=><Counter key = {counter.id} value = {counter.value} selected = {true}/>)}
</div>
);
}

然后我们回到counter,把state.value改成 this.props.value,即可显示

Passing Children传递子元素

有个很特殊的props叫做children,也就是说当我们在传递组件的时候。有时候想要传递内容,比如说对话框等等。那么我们需要把原来的但标签\ 变成双标签,然后在里面传入我们想要传入的东西

这里有value属性,是之前设置的,还有children属性,type是h4

在counter的return中加上{this.props.children},我们看到这样的ui

当然,我们也可以把这个变成动态的,只要把原来的 Title 变成 Counter #{counter.id}即可

1
2
3
4
5
6
7
8
9
10
11
render() {
return (
<div>
{this.state.counters.map((counter) => (
<Counter key={counter.id} value={counter.value} selected={true}>
<h4>Counter #{counter.id}</h4>
</Counter>
))}
</div>
);
}

事实上,我们可以直接把id当作一个props放在单个\ 标签当中去

1
<Counter key={counter.id} value={counter.value} selected={true} id={counter.id}/>

然后呢在counter 的render中。

1
<h4>{this.props.id}</h4>

如下图

Props vs State

props 就是我们给组件的 数据。state是组件本地或者私有的数据容器,其他组件是没有办法访问的。他完全只能在组件内被访问。

换句话说我们在counters中给Counter标签设置的属性,都传入到了Counter的props中。但我们不能在Counters中访问Counter组建的state,因为他是私有和本地的。

类似的,Counters有着自己的组件state,这个state也是对其他组件完全不可见的。

有时候组件是没有state的,他用props处理所有的数据。

props 和state的区别还在于props是只读的。换句话说我们不能在组件内部改变传入的数据。如果我们想在组件声明周期 修改输入的数据,那么我们需要把输入的内容复制到state当中而不是修改props

Raising and Handling Events

利用快捷键新建一个按钮 btn.btn.btn-danger.btn.m-2

效果如图

1
<button className="btn btn-danger m-2">Delete</button>

在这里我想实现点击Delete按钮,然后这行的\就被删除了,怎么实现?

这将引入一个react非常重要的原则

The component that owns a piece of state, should be the one modifying it

也就是说只有组件自己才能修改自己的state

所以我们没有办法在counter.jsx终实现,因为这四个\ 是在\中实现的,所以我们也要在counters.jsx中实现删除操作

所以我们需要让Counter组件发起一个事件,我们把这个时间叫做onDelete事件。然后Counters组件将会处理这个事件,也就是说我们要在Counters组件当中实现一个handleDelete()的方法

理解了原理之后我们在Counters组件中添加一个方法(并非最终实现),然后通过props给Counter组件传递一个方法的引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Counters extends Component {
state = {
counters: [
{ id: 1, value: 4 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
],
};

handleDelete = () => {
console.log("Event Handler Called");
};

render() {
return (
<div>
{this.state.counters.map((counter) => (
<Counter
key={counter.id}
// 在这里加一个onDelete的prop, 我们把这个设置成{this.handleDelete}方法
onDelete={this.handleDelete}
value={counter.value}
selected={true}
id={counter.id}
/>
))}
</div>
);
}
}

然后再Counter组件中刚刚实现的button标签中,通过this.props.onDelete来调用该方法

1
2
3
4
5
<button 
onClick={this.props.onDelete}
className="btn btn-danger m-2">
Delete
</button>

Updating the State

接下来我们要正式做删除操作

向上文提及的,我们先测试一下handleDelete函数能不能正确打印counterID,如果能打印,那么我们可以放心删除

counters.jsx中做如下修改

1
2
3
handleDelete =  counterID  => {
console.log("Event Handler Called",counterID);
};

counter.js 如下

1
2
3
<button onClick={()=>this.props.onDelete(this.props.id)} className="btn btn-danger m-2">
Delete
</button>

看来是成功的

现在我们回去更新Counters组件中的元素了,像以前说的,我们不直接修改state,而是重新生成一个数组然后让setState去更新原来的数组

1
2
3
4
5
handleDelete = (counterID) => {
const counters = this.state.counters.filter((c) => c.id !== counterID);
this.setState({ counters });
//待更新的state.counters和我们新的counters 名称一样,所以可以简写
};

关于id和key,这里有一点要补充。

key是react组件内部的信息,我们在Counter组件的prop中是没有办法访问它的。但是id我们可以通过组件内部的props来读取。但是问题又出现了,我这里定义了很多props,每个props都是独立的,那么当随着我们传入props的增多,代码的管理会变得混乱不堪。所以我们直接把他们打包: counter = {counter} 这个counte 本来就包含了所有counter对象的数据。然后我们只需要在Counter组件中做如下修改

1
2
3
4
5
6
7
8
9
  state = {
value: this.props.counter.value,
};
<button
onClick={() => this.props.onDelete(this.props.counter.id)}
className="btn btn-danger m-2"
>
Delete
</button>

Single Source of Truth

接下来我们想要做的是一个Reset按钮,按下以后所有现有的Counter的数值都会归零。那么,这时候我们就需要解决一个问题——

我们知道如果我们要重置,那么就要新建一个列表,把里面所有counter的value都置为0,然后setState,但是这对handleDelete和handleIncrement 都是可行的,因为Delete是直接对Counters进行修改,Increment则是通过调用Counters中\标签对counter.value的修改。

而对reset却不可以。

在counter中,他的value继承自我们传入的props属性,这个属性一旦传入就没有办法修改了,而我们一开始已经传入了四个counter的值分别是4,0,0,0 当我们尝试着重置的时候,counters中的state.counter已经被修改,但是counter中的state.value 却并没有得到修改。而当我们显示数字的时候,我们显示的是this.state.value,所以并不会改变。

1
2
3
state = {
value: this.props.counter.value,
};

所以问题就在这,我们对父子关系的组件(Counter是子组件,Counters是父组件)却有两套值得来源,这是万万不可的,所以我们要删除Counter的本地state数据,Counter中对本地state操作都转移到对父组件Counters操作中props的操作。

我们把Counter这种类型的组件 叫做受控组件,受控组件没有自己的state,他所有的数据都来自props,之后在数据需要要改变的时候发起事件。所以这种空间完全是被它的父控件控制的

Removing the Local State

修改过后的counter.jsx如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import React, { Component } from "react";
class Counter extends Component {
//这里删去了state
//这里删去了handleIncrement方法,而改成在button中发起一个event
//就是和onDelete一样
render() {
console.log(this.props);
return (
<React.Fragment>
<span className={this.getBadgeClasses()}>{this.formatCount()}</span>
<button
onClick={() => this.props.onIncrement(this.props.counter)}
className="btn btn-secondary btn-sm"
>
Increment
</button>
<button
onClick={() => this.props.onDelete(this.props.counter.id)}
className="btn btn-danger m-2"
>
Delete
</button>
<p></p>
</React.Fragment>
);
}
getBadgeClasses() {
let classes = "badge m-2 badge-";
//这里原本是this.state.value 我们把它改成了props中的value
classes += this.props.counter.value === 0 ? "warning" : "primary";
return classes;
}

formatCount() {
// 下面这行等于 const value = this.props.counter.value ;
const { value } = this.props.counter;
return value === 0 ? "Zero" : value;
}
}
export default Counter;

然后我们在counters.jsx中处理counter发起的events

我们先在render中添加一个reset标签和一个onIncrement 的prop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
render() {
return (
<div>
<button
onClick={this.handleReset}
className="btn btn-primary btn-sm m-2"
>
Reset
</button>
<br></br>
{this.state.counters.map((counter) => (
<Counter
key={counter.id}
onDelete={this.handleDelete}
onIncrement={this.handleIncrement}//一个onIncrement的prop
counter={counter}
/>
))}
</div>
);
}

然后我们来处理 handleIncrement

1
2
3
4
5
6
7
8
9
10
11
handleIncrement = (counter) => {
//首先我们通过 spread运算符来克隆一个和counters一模一样的数组
const counters = [...this.state.counters];
//然后我们获得传入的counter在counters数组当中的位置
const index = counters.indexOf(counter);
//然后我们再单独克隆一个该传入的counter对象,并把它赋值给新的counters
counters[index] = { ...counter };
//最后我们把这行的counter.value++
counters[index].value++;
this.setState({ counters });
};

最后我们写一个reset函数

1
2
3
4
5
6
7
8
9
 handleReset = () => {
//我们通过map函数把所有的counter.value都置为0,然后 用新的counters数组接收
const counters = this.state.counters.map((c) => {
c.value = 0;
return c;
});
//利用setState更新
this.setState({ counters });
};

Multiple Components in Sync 多组件同步

现在我想在网页上加一个导航栏,里面记录当前有多少的counter,这样我们就需要把组件树改成这样子

现在我们需要把index.js中的\改成\ 然后再App.js中把这些组件放进去

当然了我们要在component当中新建一个navbar.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component } from "react";
class NavBar extends Component {
render() {
return (
<nav className="navbar navbar-light bg-light">
<a class="navbar-brand" href="#">
Navbar
</a>
</nav>
);
}
}

export default NavBar;

然后对App.js做如下修改

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from "react";
import "./App.css";
import NavBar from "./components/navbar";
import Counters from "./components/counters";
function App() {
return (
<React.Fragment>
<NavBar />
<main className="container">
<Counters />
</main>
</React.Fragment>
);
}
export default App;

这就是我们结果了,我们看到组件树和我们要达到的是一致的

Lifting the State Up

我们现在想在导航栏中显示当前还有几个counter,那么问题就来了

之前我们是通过props的特性,把Counters组件当中的state传到Counter组件当中的,

因为Counters和Counter是上下层的关系。但是Counters组件和NavBar没有父子关系。怎么办呢?

当我们想要在两个没有上下父子级别的关系的组件中共享和同步数据的时候物品,我们就需要上移组件。在这里我们需要把Counters和NavBar组件上移到他们共同的父级App组件当中。然后我们在App的所有的子组件全部使用props。如下图

(奇怪,为什么app.js中的app还是function而不是class呢。。。所以我改了一下)

我们已经知道App.js中已经包含了我们的\ 那么我们现在就把Counters中的所有的方法都移到App组件当中,把Counters变成一个受控组件(就如当初Counter被变成受控组件一样) ,然后通过props来抛出一个event交给App来处理

移动过后的counters.js(一滴都不剩了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, { Component } from "react";
import Counter from "./counter";
class Counters extends Component {
render() {
return (
<div>
<button
//原来的onReset现在变成了一个prop
onClick={this.props.onReset}
className="btn btn-primary btn-sm m-2"
>
Reset
</button>
<br></br>
{/*原来的state.counters现在变成了一个prop.counters
原来的onDelete 变成了props.onDelete
....
*/}
{this.props.counters.map((counter) => (
<Counter
key={counter.id}
onDelete={this.props.onDelete}
onIncrement={this.props.onIncrement}
counter={counter}
/>
))}
</div>
);
}
}
export default Counters;

同样的我们在App.js中需要接受并处理这些events

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import React, { Component } from "react";
import "./App.css";
import NavBar from "./components/navbar";
import Counters from "./components/counters";
import { render } from "react-dom";
class App extends Component {
state = {};
state = {
counters: [
{ id: 1, value: 4 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
],
};

handleDelete = (counterID) => {
const counters = this.state.counters.filter((c) => c.id !== counterID);
this.setState({ counters });
};

handleIncrement = (counter) => {
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
counters[index] = { ...counter };
counters[index].value++;
this.setState({ counters });
};

handleReset = () => {
const counters = this.state.counters.map((c) => {
c.value = 0;
return c;
});
this.setState({ counters });
};
//上边的,都是原来counters.js中的内容
render() {
return (
<React.Fragment>
<NavBar
//在NavBar中,我们需要传入一个totalCounters 的prop供给NavBar组件显示
totalCounters={this.state.counters.filter(c => c.value > 0).length}
/>
<main className="container">
<Counters
//给Counters几个props行行好吧。。让Counters的events能被处理吧
counters={this.state.counters}
onReset={this.handleReset}
onIncrement={this.handleIncrement}
onDelete={this.handleDelete}
/>
</main>
</React.Fragment>
);
}
}

export default App;

最后在NavBar组件当中显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { Component } from "react";
class NavBar extends Component {
render() {
return (
<nav className="navbar navbar-light bg-light">
<a class="navbar-brand" href="#">
Navbar{" "}
{/*新建一个span标签来显示我们的数字*/}
<span className="badge badge-pill badge-secondary">
{this.props.totalCounters}
</span>
</a>
</nav>
);
}
}

export default NavBar;

Stateless Functional Components

我们回到NavBar组件,他没有state,没有方法,他只从props中获取数据,对于这种组件我们就可以把它变成

Stateless Functional Components,也就是我们说的无state的功能性属性

这时候我们就可以用function来定义了

1
2
3
4
5
6
7
8
9
10
11
12
13
const NavBar = () => {
return (
<nav className="navbar navbar-light bg-light">
<a class="navbar-brand" href="#">
Navbar{" "}
<span className="badge badge-pill badge-secondary">
{this.props.totalCounters}
</span>
</a>
</nav>
);
};
export default NavBar;

原来如此,这就解决了刚才为什么App是一个函数的原因了

Destructuring Arguments

现在我们来简化代码,我已经不想漫天打props了,我们怎么才能解决啊?

可以用解构(Destructuring)

在函数中解构props

在NavBar中我们直接收了一个prop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component } from "react";
//那么我们就可以在传入参数的时候解构
const NavBar = ({totalCounters}) => {
return (
<nav className="navbar navbar-light bg-light">
<a class="navbar-brand" href="#">
Navbar{" "}
<span className="badge badge-pill badge-secondary">
{/*这样在span里就可以不用打this.props.totalCounters了,直接totalCounters就完事了*/}
{totalCounters}
</span>
</a>
</nav>
);
};
export default NavBar;

在class中解构props

那么在Counters组件中我们有多个props,我们也可以解构他

解构的顺序不要紧,只要名字对上即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { Component } from "react";
import Counter from "./counter";
class Counters extends Component {
render() {
//那么这时候我们就直接用解构的语法来写了
const { onReset, counters, onDelete, onIncrement } = this.props;
return (
<div>
<button onClick={onReset} /*简化*/className="btn btn-primary btn-sm m-2">
Reset
</button>
<br></br>
{/*简化*/counters.map((counter) => (
<Counter
key={counter.id}
onDelete={onDelete}/*简化*/
onIncrement={onIncrement}/*简化*/
counter={counter}
/>
))}
</div>
);
}
}

export default Counters;

Lifecycle Hooks

一个组件会经历很多的状态,在他的生命周期中,第一个状态是mount 状态,这是组件被实例化并被创建到DOM当中。这里有一些可以加入组件的特殊方法,react会自动调用这些方法

我们叫这些方法为Lifecycle Hooks。它们允许我们在整个生命周期中勾住某个特定的时刻,并做一些事情。

在Mount 状态中有三个 Lifecycle Hooks:constructor,render,componentDidMount,react会顺序调用这些方法

第二个状态是Update状态,他在state或者组件的props改变的时候发生。在这个状态中有render 和 componentDidUpdate两个Lifecycle Hooks。当我们改变state或者传入新的props的时候,会依次调用这两个Lifecycle Hooks

第三个状态是UnMount.这个状态是当一个组件被DOM移除(删除)的时候发生,里面有componentWillUnmount一个Lifecycle Hooks,当我们要删除一个组建的时候,会被react调用

还有很多其他的Lifecycle Hooks,我们很少用到

Mounting Phase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class App extends Component{
state = {};
//因为构造器只调用一次,所以这是设置属性的最佳时机
constructor(props){
super(props);
console.log('App-Constructor');
this.state = this.props.something;
};
componentDidMount(){
//Ajax Call
console.log('App-Mounted');
}
handleIncrement = counter=>{

};
render(){
console.log('App-Rendered');
//....
}
}

Updating Phase

//…

Unmounting Phase

//…

Exercise- Decrement Button

加一个Decrement 按钮实现-1 操作

Solution - Decrement Button

在counter.jsx 中加一个按钮

1
2
3
4
5
6
<button
onClick={()=>this.props.onDecrement(this.props.counter)}
className="btn btn-secondary btn-sm m-2"
>
Decrement
</button>

在counters.jsx中处理这个事件,向上(App)抛

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Counters extends Component {
render() {
const { onReset, counters, onDelete, onIncrement ,onDecrement } = this.props;
return (
<div>
<button onClick={onReset} className="btn btn-primary btn-sm m-2">
Reset
</button>
<br></br>
{counters.map((counter) => (
<Counter
key={counter.id}
onDelete={onDelete}
onIncrement={onIncrement}
onDecrement={onDecrement}
counter={counter}
/>
))}
</div>
);
}
}

在App.js中处理这个事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  handleDerement = (counter) => {
const counters = [...this.state.counters];
const index = counters.indexOf(counter);
counters[index] = { ...counter };
counters[index].value--;
this.setState({ counters });
};

//....
render() {
return (
<React.Fragment>
<NavBar
totalCounters={this.state.counters.filter((c) => c.value > 0).length}
/>
<main className="container">
<Counters
counters={this.state.counters}
onReset={this.handleReset}
onIncrement={this.handleIncrement}
onDecrement={this.handleDerement}//加一个功能
onDelete={this.handleDelete}
/>
</main>
</React.Fragment>
);
}

Exercise- Like Component

设计一个接口实现点赞操作

Solution- Like Component

首先我们要在https://fontawesome.com/v4.7.0/icon/heart-o 复制 心形代码,注意,实心的和空心的差了一个-o:

1
<i class="fa fa-heart-o" aria-hidden="true"></i>

然后我们在components 中新建一个common文件夹,里面包含Like组件

like组件声明如下,Like是一个受控组件,我们也可以把它当成funtcion来写,但这里还是class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { Component } from "react";

// Input: liked:boolean
// Output: onClick
class Like extends Component {
render() {
let classes = "fa fa-heart";
//如果不喜欢(liked=false) 那么就变成空心,这里liked是一个prop,是父组件传给Like的
if (!this.props.liked) classes += "-o";
//返回一个心型组件
return (
<i
//组件中有onClick方法,当点击的时候会调用props中的onClick属性
onClick={this.props.onClick}
//当鼠标移动到心上的时候,会变成手状
style={{ cursor: "pointer" }}
//className 就不用我多说了8
className={classes}
aria-hidden="true"
></i>
);
}
}

export default Like;

那么我们需要在Movies中添加一个onClick的props,然后新建一个handler处理这个event

首先新添加一列,让他囊括我Like组件,并且传入一个liked和一个onClick props

1
2
3
4
5
6
7
<td>
<Like
liked={movie.liked}
// 其中 onClick因为是要把那行的心变成实心的,所以我们要通过箭头函数传入一个movie
onClick={() => this.handleLike(movie)}
/>
</td>

然后我们开始处理handleLike,一样的套路我们新建一个movies列表,然后把对应的movies的liked属性取反。(原来喜欢的变成不喜欢,原来不喜欢的变成喜欢,因为我默认liked属性是不喜欢的,所以一开始都是空心的)

1
2
3
4
5
6
7
handleLike = (movie) => {
const movies = [...this.state.movies];
const number = movies.indexOf(movie);
movies[number] = { ...movies[number] };
movies[number].liked = !movies[number].liked;
this.setState({ movies });
};

Summary

这一章节我们学了

如何利用props传递数据

发起和处理时间

怎么上移state(利用这种技巧我可以让多个组件共享一个数据源)

Functional Component

Lifecycle Hooks

后文请移步

react基础2

react基础3表单与后端

react基础4授权与部署

-------------本文结束,感谢您的阅读-------------