首先,我们回顾一下在javascript中怎么去变换列表。
下面了的代码,我们用到了数组函数的map方法来实现数组的每一个值变成它的2倍,同时返回一个新数组,最后打印出了这个数组:
const numbers = [1,2,3,4,5];
const doubled = numbers.map(number=>number * 2);
console.log(doubled);
最终在控制台的打印结果是:[2,4,6,8,10]。
在React中,转换一个数组到列表,几乎是相同的。
下面,我们依次通过调用数组的map方法,并返回一个用li标签包含数组值当元素,最后分配它们到listItems数组里面:
const numbers = [1,2,3,4,5];
const listItems = numbers.map(number => <li>{number}</li>);
现在我们把完整的listItems用一个ul标签包起来,同时render it to the DOM。
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);
这样就会在页面中显示一个带列表符号的ul列表,项目编号是从1到5。
我们经常会在一个组件里面输出一个列表elements。
好,我们来写一个组件实现前面同样的功能,这个组件接受一个数字数组,最后返回一个无序列表。
function NumberList(props){
const numbers = props.numbers;
const listItems = numbers.map(number => <li>{number}</li>);
return (
<ul>
{listItems}
</ul>
);
};
const numbers = [1,2,3,4,5];
ReactDOM.render(
<NumberList numbers={numbers}/>,
document.getElementById('root')
);
如果你运行上面的代码,你将会得到一个警告:你需要为每一个li元素提供一个key属性,这个“Key”字符串属性当你创建一个列表元素的时候必须添加。我们将在接下来讨论一下它为什么这么重要。
让我们在numbers.map()分配一个key属性到列表里面的li标签里来解决上面给出的警告提示问题,代码如下:
function NumberList(props){
const numbers = props.numbers;
const listItems = numbers.map(number => <li key={number.toString()}>{number}</li>);
return (
<ul>{listItems}</ul>
);
};
const numbers = [1,2,3,4,5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
那我们为什么非要需要这个key属性呢?其实这个key属性可以帮助React确定一下那个列表选项改变了、是新增加的、或者被删除了,反正这个key属性就是用来让react跟踪列表在过去的时间发生了什么变化。key属性值应该在数组里面指定,这样就能保证列表元素就能有一个稳定的身份验证值。
const numbers = [1,2,3,4,5];
const listItems = numbers.map(number => <li key={number.toString()}>{number}</li>);
最好的方法设置key值就是指定一个独一无二的字符串值来把当前列表元素同它的兄弟列表元素分离开来。但是通常情况下,你的后台给你的接口数据中都应该有一个当前数据为一个的”id”值,那么你就可以用这个id值来设置key属性值。代码大概像这样子:
const todoItems = todos.map(todo => <li key={todo.id}>{todo.text}</li>);
如果你的接口没有这样的一个id值来确定列表元素的key属性,那么最后的办法就是把当前列表的元素的索引值设置为key属性值了。如:
const todoItems = todos.map((todo,index) => (
//只有在todo数据里面没有独一无二的id值的情况才这么做
<li key={index}>
{todo.text}
</li>
));
我们不推荐你使用索引值来作为key属性值,因为你的列表重新排序的时候,这样会严重使程序变得很慢。如果你愿意的话,可以在这里(in-depth explanation about why keys are necessary)得到更多有关的信息!
key属性只有在数组数据环境中才有意义,其它地方是没有意义的。
例如,当我们实现一个ListItem组件的时候,这个组件封装了一个li元素,那么我们不应该在li元素上直接设置key属性,因为没有意义,key是用来跟踪数组才有意义,于是我们在NumberList组件使用到ListItem组件的时候,在数组方法里面设置key属性才有意义。好,我们先来看一个错误设置key属性的版本:
function ListItem(props){
const value = props.value;
return (
//这里是错误的,因为这里不需要指定key属性
<li key={value.toString()}>
{value}
</li>
);
};
function NumberList(props){
const numbers = props.numbers;
const listItems = numbers.map(number => (
//这里也是错误的,因为这里才是真的需要指定key属性值的地方
//记住一个要点就是:key属性只会在用到有关js处理数组有关的环境中用到
<ListItem value={number} />
));
return (
<ul>
{listItems}
</ul>
);
};
const numbers = [1,2,3,4,5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
正确地设置key属性版本的例子在下面:
function ListItem(props){
//正确的,这儿不需要设置key属性
return (
<li>{props.value}</li>
);
};
function NumberList(props){
const numbers = props.numbers;
const listItems = numbers.map((number) => (
//正确,这儿才是真的需要设置key属性的地方
<ListItem key={number.toString()} value={number}/>
));
return (
<ul>
{listItems}
</ul>
);
};
const numbers = [1,2,3,4,5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
为了帮你这里理解这一层,我自己的理解就是:并不是渲染到页面中的li标签需要key属性,(同时li标签也是没有关系的,我们在这里之所有用到li标签,只是更形象的说明问题,其实你也可以用div等等其它标签)之所要设置key属性,是React内部用来方便管理一个数组数据,跟踪数组里面的每一个数据!所以一个最好的法则就是,凡是需要调用map方法的时候你使用key属性就对了!
key属性值只会跟当前(同一个)数组数据之间是独一无二的,而不用是全局独一无二的,例如,有两个数组,那么它们的key就可以是一样的。如:
function Blog(props){
const sidebar = (
<ul>
{props.posts.map((post) => (
<li key={post.id}>
{post.title}
</li>
))}
</ul>
);
const content = props.posts.map((post) => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
));
return (
<div>
{sidebar}
<hr />
{constent}
</div>
);
};
const posts = [
{id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
{id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
ReactDOM.render(
<Blog posts={posts} />,
document.getElementById('root')
);
key属性只是给react一个用于跟踪数据的线索而已,并不是传递给组件的,如果你需要个组件设置一样一个属性,那么可以用不同的属性名代替:
const content = posts.map((post) => (
<Post key={post.id} id={post.id} title={post.title} />
));
在这个例子中,Post组件可以读id属性,但是不能读key属性。
如果一个表单元素的值是由React 来管理的,那么它就是一个受控组件。React 组件渲染表单元素,并在用户和表单元素发生交互时控制表单元素的行为,从而保证组件的 state 成为界面上所有元素状态的唯一来源对于不同的表单元素, React 的控制方式略有不同,下面我们就来看一下三类常用表单元素的控制方式。
文本框包含类型为text 的input 无素和 textarea元素。它们受控的主要原理是,通过表单元素的 value属性设置表单元素的值,通过表单元素的onChange 事件监听值的变化,并将变化同步到React 组件的 state中。
LoginForm.js
import React, { Component } from "react";
export default class LoginForm extends Component {
constructor(props) {
super(props);
this.state = {
username: "",
password: "",
};
}
changeHandle = (e) => {
let target = e.target;
this.setState({ [target.name]: target.value });
};
handleSubmit = (e) => {
console.log(
"username:" + this.state.username + " password:" + this.state.password
);
e.preventDefault();
};
render() {
return (
<div>
<div>
<h2>用户登录</h2>
<form onSubmit={this.handleSubmit}>
<fieldset>
<legend>用户信息</legend>
<p>
<label>帐号:</label>
<input
type="text"
name="username"
value={this.state.username}
onChange={this.changeHandle}
/>
</p>
<p>
<label>密码:</label>
<input
type="password"
name="password"
onChange={this.changeHandle}
value={this.state.password}
/>
</p>
<p>
<button>登录</button>
</p>
</fieldset>
</form>
</div>
<div>
uid:{this.state.username} - pwd:{this.state.password}
</div>
</div>
);
}
}
运行结果:
用户名和密码两个表单元素的值是从组件的 state中获取的,当用户更改表单元素的值时,onChange事件会被触发,对应的 handleChange处理函数会把变化同步到组件的 state,新的 state又会触发表单元素重新渲染,从而实现对表单元素状态的控制。
这个例子还包含一个处理多个表单元素的技巧:通过为两个 input元素分别指定name属性,使用同一个函数 handleChange处理元素值的变化,在处理函数中根据元素的name属性区分事件的来源。这样的写法显然比为每一个 input元素指定一个处理函数简洁得多。textarea的使用方式和input几乎一致,这里不再赘述。
列表select元素是最复杂的表单元素,它可以用来创建一个下拉列表:
<select>
<option value="react">React</option>
<option value="redux">Redux</option>
<option selected value="mobx">MobX</ option>
</select>
通过指定selected属性可以定义哪一个选项(option)处于选中状态,所以上面的例子中,Mobx这一选项是列表的初始值,处于选中状态。在React中,对select的处理方式有所不同,它通过在select上定义 value属性来决定哪一个option元素处于选中状态。这样,对select的控制只需要在select 这一个元素上修改即可,而不需要关注 option元素。下面是一个例子:
import React, { Component } from "react";
export default class SelectForm extends Component {
constructor(props) {
super(props);
this.state = {
value: "mobx",
};
}
changeHandle = (e) => {
let target = e.target;
this.setState({ value: target.value });
};
handleSubmit = (e) => {
console.log("value:" + this.state.value);
e.preventDefault();
};
render() {
return (
<div>
<div>
<h2>使用技术</h2>
<form onSubmit={this.handleSubmit}>
<fieldset>
<legend>技术信息</legend>
<p>
<label>技术列表:</label>
<select value={this.state.value} onChange={this.changeHandle}>
<option value="react">React</option>
<option value="redux">Redux</option>
<option value="mobx">MobX</option>
</select>
</p>
<p>
<button>提交</button>
</p>
</fieldset>
</form>
</div>
<div>value:{this.state.value}</div>
</div>
);
}
}
运行
复选框是类型为checkbox的input元素,单选框是类型为 radio的input元素,它们的受控方式不同于类型为text 的 input元素。通常,复选框和单选框的值是不变的,需要改变的是它们的checked 状态,因此React 控制的属性不再是value属性,而是checked属性。例如:
import React, { Component } from "react";
export default class SelectForm extends Component {
constructor(props) {
super(props);
this.state = {
react: false,
redux: false,
mobx: false,
};
}
handleChange = (e) => {
let target = e.target;
this.setState({ [e.target.name]: target.checked });
};
handleSubmit = (e) => {
console.log(JSON.stringify(this.state));
e.preventDefault();
};
render() {
return (
<div>
<div>
<h2>使用技术</h2>
<form onSubmit={this.handleSubmit}>
<fieldset>
<legend>技术信息</legend>
<p>
<label>技术列表:</label>
<input
type="checkbox"
name="react"
value="react"
checked={this.state.react}
onChange={this.handleChange}
/>
React
<input
type="checkbox"
name="redux"
value="redux"
checked={this.state.redux}
onChange={this.handleChange}
/>
Redux
<input
type="checkbox"
name="mobx"
value="mobx"
checked={this.state.mobx}
onChange={this.handleChange}
/>
Mobx
</p>
<p>
<button>提交</button>
</p>
</fieldset>
</form>
</div>
<div>{JSON.stringify(this.state)}</div>
</div>
);
}
}
运行结果:
让每个帖子支持编辑功能:
PostItem.js
import React, { Component } from "react";
import like from "./like.png";
export default class PostItem extends Component {
constructor(props) {
super(props);
this.state = {
editing: false,
post: props.post,
};
}
static getDerivedStateFromProps(props, state) {
if (props.post.vote !== state.post.vote) {
return { post: props.post };
}
return null;
}
handleVote = () => {
this.props.onVote(this.props.post.id);
};
handleTitleChange = (e) => {
const newPost = { ...this.state.post, title: e.target.value };
this.setState({ post: newPost });
};
handleEditPost = (e) => {
if (this.state.editing) {
this.props.onSave({
...this.state.post,
date: new Date().toLocaleString(),
});
}
this.setState({
editing: !this.state.editing,
});
};
render() {
const { post } = this.state;
return (
<li className="item">
<div className="title">
{this.state.editing ? (
<form>
<textarea
value={post.title}
onChange={this.handleTitleChange}
cols="30"
rows="3"
/>
</form>
) : (
post.title
)}
</div>
<div>
创建人:<span>{post.author}</span>
</div>
<div>
创建时间:<span>{post.date}</span>
</div>
<div>
<img src={like} alt="点赞" onClick={this.handleVote}></img>
{post.vote}
</div>
<div>
<button onClick={this.handleEditPost}>
{this.state.editing ? "保存" : "编辑"}
</button>
</div>
</li>
);
}
}
PostList.js
import React, { Component } from "react";
import PostItem from "./PostItem";
/**
* 有状态组件定义
*/
export class PostList extends Component {
constructor(props) {
super(props);
//状态初始化
this.state = {
posts: [], //所有帖子
};
this.timer = null; //时钟,模拟后台加载数据
//将voteHandle函数中的this指向组件对象
this.voteHandle = this.voteHandle.bind(this);
}
data = [
{
id: 1001,
title: "百度萝卜快跑超百台无人车落地武汉,订单量突破200",
author: "小明",
date: "2023-03-01 12:12:18",
vote: 0,
},
{
id: 1002,
title: "我国自主研制空间站双光子显微镜首获航天员皮肤三维图",
author: "小军",
date: "2022-12-15 23:15:26",
vote: 0,
},
{
id: 1003,
title: "清华大学一教授团队为村民“打印”一栋住宅!",
author: "小华",
date: "2022-11-26 18:17:44",
vote: 0,
},
];
//当组件挂载完成后
componentDidMount() {
//模拟后台AJAX加载
this.timer = setTimeout(() => {
this.setState({
posts: this.data,
});
}, 1000);
}
//当组件将被卸载时
componentWillUnmount() {
clearTimeout(this.timer);
}
//根据id点赞
voteHandle(id) {
//如果当前帖子的id是我们要投票的帖子,则生成一个新对象,并更新投票数
//如果不是要找的帖子,则直接返回
let newPosts = this.state.posts.map((item) =>
item.id === id ? { ...item, vote: item.vote + 1 } : item
);
//更新状态,刷新UI
this.setState({ posts: newPosts });
}
saveHandle = (newPost) => {
const newPosts = this.state.posts.map((item) =>
item.id === newPost.id ? newPost : item
);
this.setState({ posts: newPosts });
};
render() {
return (
<div>
<h2>帖子列表:</h2>
{this.state.posts.map((item) => (
<PostItem
key={item.id}
post={item}
posts={this.state.posts}
onVote={this.voteHandle}
onSave={this.saveHandle}
/>
))}
</div>
);
}
}
export default PostList;
运行结果:
在大多数情况下,我们推荐使用 受控组件 来处理表单数据。在一个受控组件中,表单数据是由 React 组件来管理的。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。
要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以 使用 ref 来从 DOM 节点中获取表单数据。
例如,下面的代码使用非受控组件接受一个表单的值:
class NameForm extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.input = React.createRef();
}
handleSubmit(event) {
alert('A name was submitted: ' + this.input.current.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。
在 React 渲染生命周期时,表单元素上的 value
将会覆盖 DOM 节点中的值。在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue
属性,而不是 value
。在一个组件已经挂载之后去更新 defaultValue
属性的值,不会造成 DOM 上值的任何更新。
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input
defaultValue="Bob"
type="text"
ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
同样,<input type="checkbox">
和 <input type="radio">
支持 defaultChecked
,<select>
和 <textarea>
支持 defaultValue
。
在 HTML 中,<input type="file">
可以让用户选择一个或多个文件上传到服务器,或者通过使用 File API 进行操作。
<input type="file" />
在 React 中,<input type="file" />
始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。
您应该使用 File API 与文件进行交互。下面的例子显示了如何创建一个 DOM 节点的 ref 从而在提交表单时获取文件的信息。
class FileInput extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.fileInput = React.createRef();
}
handleSubmit(event) {
event.preventDefault();
alert(
`Selected file - ${this.fileInput.current.files[0].name}`
);
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Upload file:
<input type="file" ref={this.fileInput} />
</label>
<br />
<button type="submit">Submit</button>
</form>
);
}
}
const root = ReactDOM.createRoot(
document.getElementById('root')
);
root.render(<FileInput />);
React16之前render方法必须返回单个元素,现在render可以返回多种不同的元素:
render()
方法是 class 组件中唯一必须实现的方法。
当 render
被调用时,它会检查 this.props
和 this.state
的变化并返回以下类型之一:
<div />
会被 React 渲染为 DOM 节点,<MyComponent />
会被 React 渲染为自定义组件,无论是 <div />
还是 <MyComponent />
均为 React 元素。null
。什么都不渲染。(主要用于支持返回 test && <Child />
的模式,其中 test 为布尔类型。)render()
函数应该为纯函数,这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互。
如需与浏览器进行交互,请在 componentDidMount()
或其他生命周期方法中执行你的操作。保持 render()
为纯函数,可以使组件更容易思考。
注意 如果
shouldComponentUpdate()
返回 false,则不会调用render()
。
export default class Hi extends Component {
render() {
return [<span>Hello</span>, <span>React</span>, <span>render()!</span>];
}
}
//table.js
const Table = () => {
render() {
return (
<table>
<tr>
<Columns />
</tr>
</table>
)
}
}
//columns.js
const Columns = () => {
render() {
return (
<div>
<td>Hello</td>
<td>World</td>
</div>
)
}
}
//以上代码输出:
<table>
<tr>
<div>
<td>Hello</td>
<td>World</td>
</div>
</tr>
</table>
//此时 td 是失效的,可以使用Fragemengt解决这个问题
//用法:
//columns.js
const Columns = () => {
render() {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
)
}
}
//通过上面的方法我们就可以正确的输出table啦:
<table>
<tr>
<td>Hello</td>
<td>World</td>
</tr>
</table>
const Cloumns = () => {
render() {
return (
<>
<td>Hello</td>
<td>World</td>
</>
)
}
}
function Glossary(props) {
return (
<dl>
{props.items.map(item => (
// 没有`key`,React 会发出一个关键警告
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
))}
</dl>
)
}
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
)
}
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
ReactDOM.createPortal(child, container)
第一个参数(child
)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container
)是一个 DOM 元素。
通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:
render() {
// React 挂载了一个新的 div,并且把子元素渲染其中
return (
<div> {this.props.children}
</div> );
}
然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:
render() {
// React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
// `domNode` 是一个可以在任何位置的有效 DOM 节点。
return ReactDOM.createPortal(
this.props.children,
domNode );
}
一个 portal 的典型用例是当父组件有 overflow: hidden
或 z-index
样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框:
注意:
当在使用 portal 时, 记住管理键盘焦点就变得尤为重要。
对于模态对话框,通过遵循 WAI-ARIA 模态开发实践,来确保每个人都能够运用它。
示例:
import React, { Component } from "react";
import ReactDOM from "react-dom";
export default class Hi extends Component {
constructor(props) {
super(props);
this.container = document.createElement("div");
document.body.appendChild(this.container);
}
render() {
return ReactDOM.createPortal(<h2>Portal</h2>, this.container);
}
}
运行结果:
卸载时需要移除
componentWillUnmount() {
document.body.removeChild(this.container);
}
部分 UI 的异常不应该破坏了整个应用。为了解决 React 用户的这一问题,React 16 引入了一种称为 “错误边界” 的新概念。 错误边界是用于捕获其子组件树 JavaScript 异常,记录错误并展示一个回退的 UI 的 React 组件,而不是整个组件树的异常。错误组件在渲染期间,生命周期方法内,以及整个组件树构造函数内捕获错误。
componentDidCatch(error, info)
此生命周期在后代组件抛出错误后被调用。 它接收两个参数:
error
—— 抛出的错误。info
—— 带有 componentStack
key 的对象,其中包含有关组件引发错误的栈信息。componentDidCatch()
会在“提交”阶段被调用,因此允许执行副作用。 它应该用于记录错误之类的情况:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染可以显示降级 UI
return { hasError: true };
}
componentDidCatch(error, info) { // "组件堆栈" 例子: // in ComponentThatThrows (created by App) // in ErrorBoundary (created by App) // in div (created by App) // in App logComponentStackToMyService(info.componentStack); }
render() {
if (this.state.hasError) {
// 你可以渲染任何自定义的降级 UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
React 的开发和生产构建版本在 componentDidCatch()
的方式上有轻微差别。
在开发模式下,错误会冒泡至 window
,这意味着任何 window.onerror
或 window.addEventListener('error', callback)
会中断这些已经被 componentDidCatch()
捕获的错误。
相反,在生产模式下,错误不会冒泡,这意味着任何根错误处理器只会接受那些没有显式地被 componentDidCatch()
捕获的错误。
注意 如果发生错误,你可以通过调用
setState
使用componentDidCatch()
渲染降级 UI,但在未来的版本中将不推荐这样做。 可以使用静态getDerivedStateFromError()
来处理降级渲染。
特别注意:
this.abc()
,并不会执行componentDidCatch
)setTimeout
或 requestAnimationFrame
回调函数)当render()函数出现问题时,componentDidCatch
会捕获异常并处理
此时,render()函数里面发生错误,则 componentDidCatch 会进行调用,在里面进行相应的处理
render() {
let a = [1,2,3]
let value = a[3].toString() 对 undefined 进行操作
return (......)
}
防止 页面 级别的崩溃~
示例:
import ErrorCom from "./ErrorCom";
const vnode = (
<div>
<ErrorCom />
<Hi />
</div>
);
默认情况:
发生错误时整个应用崩溃
改进,定义ErrorBoundary组件
import React, { Component } from "react";
export default class ErrorBoundary extends Component {
state = { error: null };
componentDidCatch(error, info) {
this.setState({ error });
}
render() {
if (this.state.error) {
return (
<div>
<h2>发生了错误</h2>
<div>{this.state.error && this.state.error.toString()}</div>
</div>
);
}
return this.props.children;
}
}
修改index.js文件
const vnode = (
<div>
<ErrorBoundary>
<ErrorCom />
</ErrorBoundary>
<Hi />
</div>
);
发生错误时,仅当前控制失效了:
React 16 之前会忽略不是把的HTML和SVG属性,现在React会把不识别的属性传递给DOM。
React16之前:
<div cust-attr="someting"></div> 会被渲染成:
<div></div>
React 16渲染出来的节点:
<div cust-attr="someting"></div>
1,设计合适的state
state必须能代表一个组件UI呈现的完整状态集,代表一个组件UI呈现的最小状态集。
state必须能代表一个组件UI呈现的完整状态集又可以分成两类数据:用作渲染组件时使用到的数据的来源,用作组件UI展现形式的判断依据:
class Hello extends Component {
constructor(props) {
super(props);
this.state = {
user: 'react', //用作渲染组件时使用到的数据的来源
display: true //用作组件UI展现形式的判断依据
}
}
render() {
return (
<div>
{
this.state.display ? <h1>{this.state.user}</h1> : <></>
}
</div>
)
}
}
export default Hello;
普通属性:
在es6中,可以使用this.属性名定义一个class的属性,也可以说属性是直接挂载在this下的一个变量。因此,state和props实际上也是组件的属性,只不过是react在Component class中预定义好的属性。除了state和props以外的其他组件属性称为组件的普通属性。
class Hello extends Component {
constructor(props) {
super(props);
this.timer = null; //普通属性
this.state = {
date: new Date()
}
this.updateDate = this.updateDate.bind(this);
}
componentDidMount(){
this.timer = setInterval(this.updateDate, 1000);
}
componentWillUnmount(){
clearInterval(this.timer);
}
updateDate(){
this.setState({
date: new Date()
})
}
render() {
return (
<div>
<h1>{this.state.date.toString()}</h1>
</div>
)
}
}
export default Hello;
组件中用到的一个变量是否应该作为state可以通过下面4条依据判断:
①不能直接修改state,需要使用setState()
②state的更新是异步的
React会将多次setState的状态合并成一次状态修改,不能依赖当前的state计算下一个state(props也是异步的)。
例如:连续两次点击加入购物车,实际数量只会加1,在React合并多次修改为1次的情况下,相当于执行了:
Object.assign(
previousState,
{quantity: this.state.quantity + 1},
{quantity: this.state.quantity + 1}
)
示例:
import React, { Component } from "react";
export default class ErrorCom extends Component {
state = { n: 0 };
clickHandle = () => {
for (let i = 0; i < 100; i++) {
this.setState(
{
n: this.state.n + 1,
},
() => {
if (this.state.n === 300) {
throw new Error("发生了错误");
}
}
);
}
};
render() {
return (
<div>
<h2>组件</h2>
点击3次就异常了
<button onClick={this.clickHandle}>{this.state.n}</button>
</div>
);
}
}
上面的代码虽然循环了100次,实际每次只增加了1。
这种情况下,可以使用另一个接收一个函数作为参数的setState,这个函数有两个参数,第一个是当前修改后的最新状态的前一个状态preState,第二个参数是当前最新的属性props:
this.setState((preState,props) => ({
quantity: preState.quantity + 1;
}))
后设置的state会覆盖前面的状态,如果不存在则添加。
直接修改state,组件不会render;state包含的所有状态都应该是不可变对象,当state中某个状态发生变化时,应该重新创建这个状态对象,而不是直接修改原来的状态。创建新的状态有以下三种方法:
状态的类型是不可变类型(数字、字符串、布尔值、null、undefined):因为状态是不可变类型,所以直接赋一个新值即可 状态的类型是数组:可以使用数组的concat或者es6的扩展语法,slice方法、filter方法。不能使用push、pop、shift、unshift、splice等方法修改数组类型的状态,因为这些方法会在原数组基础上修改。
this.setState((preState) => ({
arr: [...preState.arr,'react'];
}))
this.setState((preState) => ({
arr: preState.arr.concat(['react'])
}))
状态的类型是普通对象(不包含字符串、数组):使用ES6的Object.assgin方法或者对象扩展语法
Object.assign({},preState.owner,{name:"tom"});
或者
{...preState.owner,name:"tom"}
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
源代码与英文帮助:https://github.com/axios/axios
使用 npm:
$ npm install axios |
---|
使用 bower:
$ bower install axios |
---|
使用 cdn:
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
---|
执行 GET
请求
// 为给定 ID 的 user 创建请求axios.get('/user?ID=12345') .then(function (response) { console.log(response); }) .catch(function (error) { console.log(error); });// 上面的请求也可以这样做axios.get('/user', { params: { ID: 12345 } }) .then(function (response) { console.log(response); }) .catch(function (error) { console.log(error); }); |
---|
执行 POST
请求
axios.post('/user', { firstName: 'Fred', lastName: 'Flintstone' }) .then(function (response) { console.log(response); }) .catch(function (error) { console.log(error); }); |
---|
执行多个并发请求
function getUserAccount() { return axios.get('/user/12345');}function getUserPermissions() { return axios.get('/user/12345/permissions');}axios.all([getUserAccount(), getUserPermissions()]) .then(axios.spread(function (acct, perms) { // 两个请求现在都执行完成 })); |
---|
可以通过向 axios
传递相关配置来创建请求
// 发送 POST 请求axios({ method: 'post', url: '/user/12345', data: { firstName: 'Fred', lastName: 'Flintstone' }}); |
---|
// 获取远端图片axios({ method:'get', url:'http://bit.ly/2mTM3nY', responseType:'stream'}) .then(function(response) { response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))}); |
---|
// 发送 GET 请求(默认的方法)axios('/user/12345'); |
---|
为方便起见,为所有支持的请求方法提供了别名
在使用别名方法时, url
、method
、data
这些属性都不必在配置中指定。
处理并发请求的助手函数
可以使用自定义配置新建一个 axios 实例
const instance = axios.create({ baseURL: 'https://some-domain.com/api/', timeout: 1000, headers: {'X-Custom-Header': 'foobar'}}); |
---|
以下是可用的实例方法。指定的配置将与实例的配置合并。
这些是创建请求时可以用的配置选项。只有 url
是必需的。如果没有指定 method
,请求将默认使用 get
方法。
{ // `url` 是用于请求的服务器 URL url: '/user', // `method` 是创建请求时使用的方法 method: 'get', // default // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。 // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL baseURL: 'https://some-domain.com/api/', // `transformRequest` 允许在向服务器发送前,修改请求数据 // 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法 // 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream transformRequest: [function (data, headers) { // 对 data 进行任意转换处理 return data; }], // `transformResponse` 在传递给 then/catch 前,允许修改响应数据 transformResponse: [function (data) { // 对 data 进行任意转换处理 return data; }], // `headers` 是即将被发送的自定义请求头 headers: {'X-Requested-With': 'XMLHttpRequest'}, // `params` 是即将与请求一起发送的 URL 参数 // 必须是一个无格式对象(plain object)或 URLSearchParams 对象 params: { ID: 12345 }, // `paramsSerializer` 是一个负责 `params` 序列化的函数 // (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/) paramsSerializer: function(params) { return Qs.stringify(params, {arrayFormat: 'brackets'}) }, // `data` 是作为请求主体被发送的数据 // 只适用于这些请求方法 'PUT', 'POST', 和 'PATCH' // 在没有设置 `transformRequest` 时,必须是以下类型之一: // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams // - 浏览器专属:FormData, File, Blob // - Node 专属: Stream data: { firstName: 'Fred' }, // `timeout` 指定请求超时的毫秒数(0 表示无超时时间) // 如果请求话费了超过 `timeout` 的时间,请求将被中断 timeout: 1000, // `withCredentials` 表示跨域请求时是否需要使用凭证 withCredentials: false, // default // `adapter` 允许自定义处理请求,以使测试更轻松 // 返回一个 promise 并应用一个有效的响应 (查阅 [response docs](#response-api)). adapter: function (config) { /* ... */ }, // `auth` 表示应该使用 HTTP 基础验证,并提供凭据 // 这将设置一个 `Authorization` 头,覆写掉现有的任意使用 `headers` 设置的自定义 `Authorization`头 auth: { username: 'janedoe', password: 's00pers3cret' }, // `responseType` 表示服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream' responseType: 'json', // default // `responseEncoding` indicates encoding to use for decoding responses // Note: Ignored for `responseType` of 'stream' or client-side requests responseEncoding: 'utf8', // default // `xsrfCookieName` 是用作 xsrf token 的值的cookie的名称 xsrfCookieName: 'XSRF-TOKEN', // default // `xsrfHeaderName` is the name of the http header that carries the xsrf token value xsrfHeaderName: 'X-XSRF-TOKEN', // default // `onUploadProgress` 允许为上传处理进度事件 onUploadProgress: function (progressEvent) { // Do whatever you want with the native progress event }, // `onDownloadProgress` 允许为下载处理进度事件 onDownloadProgress: function (progressEvent) { // 对原生进度事件的处理 }, // `maxContentLength` 定义允许的响应内容的最大尺寸 maxContentLength: 2000, // `validateStatus` 定义对于给定的HTTP 响应状态码是 resolve 或 reject promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte validateStatus: function (status) { return status >= 200 && status < 300; // default }, // `maxRedirects` 定义在 node.js 中 follow 的最大重定向数目 // 如果设置为0,将不会 follow 任何重定向 maxRedirects: 5, // default // `socketPath` defines a UNIX Socket to be used in node.js. // e.g. '/var/run/docker.sock' to send requests to the docker daemon. // Only either `socketPath` or `proxy` can be specified. // If both are specified, `socketPath` is used. socketPath: null, // default // `httpAgent` 和 `httpsAgent` 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理。允许像这样配置选项: // `keepAlive` 默认没有启用 httpAgent: new http.Agent({ keepAlive: true }), httpsAgent: new https.Agent({ keepAlive: true }), // 'proxy' 定义代理服务器的主机名称和端口 // `auth` 表示 HTTP 基础验证应当用于连接代理,并提供凭据 // 这将会设置一个 `Proxy-Authorization` 头,覆写掉已有的通过使用 `header` 设置的自定义 `Proxy-Authorization` 头。 proxy: { host: '127.0.0.1', port: 9000, auth: { username: 'mikeymike', password: 'rapunz3l' } }, // `cancelToken` 指定用于取消请求的 cancel token // (查看后面的 Cancellation 这节了解更多) cancelToken: new CancelToken(function (cancel) { })} |
---|
某个请求的响应包含以下信息
{ // `data` 由服务器提供的响应 data: {}, // `status` 来自服务器响应的 HTTP 状态码 status: 200, // `statusText` 来自服务器响应的 HTTP 状态信息 statusText: 'OK', // `headers` 服务器响应的头 headers: {}, // `config` 是为请求提供的配置信息 config: {}, // 'request' // `request` is the request that generated this response // It is the last ClientRequest instance in node.js (in redirects) // and an XMLHttpRequest instance the browser request: {}} |
---|
使用 then
时,你将接收下面这样的响应 :
axios.get('/user/12345') .then(function(response) { console.log(response.data); console.log(response.status); console.log(response.statusText); console.log(response.headers); console.log(response.config); }); |
---|
在使用 catch
时,或传递 rejection callback 作为 then
的第二个参数时,响应可以通过 error
对象可被使用,正如在错误处理这一节所讲。
你可以指定将被用在各个请求的配置默认值
axios.defaults.baseURL = 'https://api.example.com';axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; |
---|
// Set config defaults when creating the instanceconst instance = axios.create({ baseURL: 'https://api.example.com'});// Alter defaults after instance has been createdinstance.defaults.headers.common['Authorization'] = AUTH_TOKEN; |
---|
配置会以一个优先顺序进行合并。这个顺序是:在 lib/defaults.js
找到的库的默认值,然后是实例的 defaults
属性,最后是请求的 config
参数。后者将优先于前者。这里是一个例子:
// 使用由库提供的配置的默认值来创建实例// 此时超时配置的默认值是 `0`var instance = axios.create();// 覆写库的超时默认值// 现在,在超时前,所有请求都会等待 2.5 秒instance.defaults.timeout = 2500;// 为已知需要花费很长时间的请求覆写超时设置instance.get('/longRequest', { timeout: 5000}); |
---|
在请求或响应被 then
或 catch
处理前拦截它们。
// 添加请求拦截器axios.interceptors.request.use(function (config) { // 在发送请求之前做些什么 return config; }, function (error) { // 对请求错误做些什么 return Promise.reject(error); });// 添加响应拦截器axios.interceptors.response.use(function (response) { // 对响应数据做点什么 return response; }, function (error) { // 对响应错误做点什么 return Promise.reject(error); }); |
---|
如果你想在稍后移除拦截器,可以这样:
const myInterceptor = axios.interceptors.request.use(function () {/*...*/});axios.interceptors.request.eject(myInterceptor); |
---|
可以为自定义 axios 实例添加拦截器
const instance = axios.create();instance.interceptors.request.use(function () {/*...*/}); |
---|
axios.get('/user/12345') .catch(function (error) { if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx console.log(error.response.data); console.log(error.response.status); console.log(error.response.headers); } else if (error.request) { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js console.log(error.request); } else { // Something happened in setting up the request that triggered an Error console.log('Error', error.message); } console.log(error.config); }); |
---|
Y可以使用 validateStatus
配置选项定义一个自定义 HTTP 状态码的错误范围。
axios.get('/user/12345', { validateStatus: function (status) { return status < 500; // Reject only if the status code is greater than or equal to 500 }}) |
---|
使用 cancel token 取消请求
Axios 的 cancel token API 基于cancelable promises proposal,它还处于第一阶段。
可以使用 CancelToken.source
工厂方法创建 cancel token,像这样:
const CancelToken = axios.CancelToken;const source = CancelToken.source();axios.get('/user/12345', { cancelToken: source.token}).catch(function(thrown) { if (axios.isCancel(thrown)) { console.log('Request canceled', thrown.message); } else { // 处理错误 }});axios.post('/user/12345', { name: 'new name'}, { cancelToken: source.token})// 取消请求(message 参数是可选的)source.cancel('Operation canceled by the user.'); |
---|
还可以通过传递一个 executor 函数到 CancelToken
的构造函数来创建 cancel token:
const CancelToken = axios.CancelToken;let cancel;axios.get('/user/12345', { cancelToken: new CancelToken(function executor(c) { // executor 函数接收一个 cancel 函数作为参数 cancel = c; })});// cancel the requestcancel(); |
---|
注意: 可以使用同一个 cancel token 取消多个请求
默认情况下,axios将JavaScript对象序列化为JSON。 要以application / x-www-form-urlencoded格式发送数据,您可以使用以下选项之一。
在浏览器中,您可以使用URLSearchParams API,如下所示:
const params = new URLSearchParams();params.append('param1', 'value1');params.append('param2', 'value2');axios.post('/foo', params); |
---|
请注意,所有浏览器都不支持URLSearchParams(请参阅caniuse.com),但可以使用polyfill(确保填充全局环境)。
或者,您可以使用qs库编码数据:
const qs = require('qs');axios.post('/foo', qs.stringify({ 'bar': 123 })); |
---|
或者以另一种方式(ES6),
import qs from 'qs';const data = { 'bar': 123 };const options = { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, data: qs.stringify(data), url,};axios(options); |
---|
在node.js中,您可以使用querystring模块,如下所示:
const querystring = require('querystring');axios.post('http://something.com/', querystring.stringify({ foo: 'bar' })); |
---|
您也可以使用qs库。
在axios达到1.0版本之前,破坏性更改将以新的次要版本发布。 例如0.5.1和0.5.4将具有相同的API,但0.6.0将具有重大变化。
axios 依赖原生的 ES6 Promise 实现而被支持. 如果你的环境不支持 ES6 Promise,你可以使用 polyfill.
axios包括TypeScript定义。
import axios from 'axios';axios.get('/user?ID=12345'); |
---|
componentDidMount是调用服务器API最安全的地方,也是React官方推荐的进行服务器通信的地方。除了在componentDidMount,在componentWillMount中进行服务器通信也是比较常见的一种方式。
componentWillMount会在组件被挂载前调用,因此从时间上来讲,componentWillMount中执行服务器通信要早于componentDidMount。但两者执行的时间差微乎其微,完全可以忽略不计。
componentDidMount是执行组件与服务器通信的最佳地方,原因:
在componentDidMount执行服务器通信可以保证获取到数据时,组件已经处于挂载状态,此时可以操作DOM
当组件在服务器端渲染时,componentWillMount会执行两次,一个在服务器端,一次在浏览器端,而componentDidMount能保证在任何情况下只会被调用一次,从而不会发送多余的数据请求。
服务器端使用Spring Boot:
接口提供JSON数据:
http://localhost:8081/api/students
前后端分离,前端使用React+Axios从服务器获取数据,展示学生信息:
在前端项目中依赖axios
创建StudentList组件
例如,组件需要以props中某个属性作为与服务器通信的请求采纳数,当这个属性值发生更新时,组件自然需要重新余服务器通信。
componentWillReceiveProps(nextProps){ if(nextProps.category !== this.props.category) { // fetch } }
componentWillReceiveProps的执行并不能说明props一定发生了修改
1、定义一个对话框组件,要求显示在body尾部标签内,使用portal技术,卸载时要求删除容器