React学习笔记(三)—— 组件高级
一、列表和keys
1.1、Lists and Keys (列表和键)
首先,我们回顾一下在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。
1.2、Basic List Component(一个基础的列表组件)
我们经常会在一个组件里面输出一个列表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') );
1.3、Keys(如何设置的key属性值)
那我们为什么非要需要这个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)得到更多有关的信息!
1.4、Extracting Components with Keys(当我们提取一个组件到另一个组件的时候,需要注意怎么管理key)
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属性就对了!
1.5、Keys Must Only Be Unique Among Siblings(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属性。
二、受控组件与非受控组件
2.1、受控组件
如果一个表单元素的值是由React 来管理的,那么它就是一个受控组件。React 组件渲染表单元素,并在用户和表单元素发生交互时控制表单元素的行为,从而保证组件的 state 成为界面上所有元素状态的唯一来源对于不同的表单元素, React 的控制方式略有不同,下面我们就来看一下三类常用表单元素的控制方式。
2.1.1、文本框
文本框包含类型为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几乎一致,这里不再赘述。
2.1.2、高阶函数
高阶函数:如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。
1.若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数。
常见的高阶函数有:Promise、setTimeout、arr.map()等等
2.若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数。
常见的高阶函数有:Function.bind
自定义高阶函数
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script> /** * *高阶函数:如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。 1.若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数。 2.若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数。 常见的高阶函数有:Promise、setTimeout、arr.map()等等 */ /** * cal 是一个函数 * n1数字,参数2 * n2数字,参数3 */ function superCal(cal, n1, n2) { if (cal && n1 > 0 && n2 > 0) { return () => { return cal(n1 * 2, n2 * 2); }; } } let result = superCal((a, b) => a + b, 1, 2)(); console.log(result); </script> </body> </html>
示例1:
import React, { Component } from "react"; export default class LoginForm extends Component { state = { uid: "abc", pwd: "123", code: "5678", }; handleSubmit = (e) => { console.log(`uid:${this.state.uid} - pwd:${this.state.pwd}`); //阻止默认事件 e.preventDefault(); }; //高阶函数+闭包 formChange = (propKey) => { return (e) => { this.setState({ [propKey]: e.target.value }); }; }; render() { return ( <div> <h2>用户登录</h2> <form onSubmit={this.handleSubmit}> <fieldset> <legend>用户信息</legend> <p> <label htmlFor="uid">帐号:</label> <input type="text" name="uid" id="uid" value={this.state.uid} onChange={this.formChange("uid")} ></input> </p> <p> <label htmlFor="pwd">密码:</label> <input type="password" name="pwd" id="pwd" value={this.state.pwd} onChange={this.formChange("pwd")} ></input> </p> <p> <label htmlFor="pwd">验证:</label> <input type="text" name="code" id="code" value={this.state.code} onChange={this.formChange("code")} ></input> </p> <p> <button>登录</button> </p> </fieldset> {`uid:${this.state.uid} - pwd:${this.state.pwd} - code:${this.state.code}`} </form> </div> ); } }
运行效果:
2.1.3、柯里化
函数的柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script> /** * *函数的柯里化: 通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。 */ /** * cal 是一个函数 */ function superCal(cal) { if (cal) { return (n1) => { return (n2) => { return cal(n1 * 2, n2 * 2); }; }; } } let result = superCal((a, b) => a + b)(1)(2); console.log(result); </script> </body> </html>
结果:
2.1.4、列表
列表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> ); } }
运行
2.1.5、复选框与单选框
复选框是类型为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> ); } }
运行结果:
2.1.6、升级后的BBS
让每个帖子支持编辑功能:
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;
运行结果:
参考代码二:
PostItem.js
import React, { Component } from "react"; import like from "./images/dz.png"; /** * 受控组件 */ export default class PostItem extends Component { constructor(props) { super(props); this.state = { editing: false, //编辑中 post: props.post, //当前帖子 }; } //当props变化时更新本组件的状态 static getDerivedStateFromProps(props, state) { console.log(props, state); if (props.post.vote !== state.post.vote) { return { post: props.post }; } return null; } //投票 onVote = (id) => { this.props.onVote(id); }; //保存事件 saveHandle = (e) => { if (this.state.editing) { let newPost = { ...this.state.post, date: new Date().toLocaleString(), }; this.props.onSave(newPost); this.setState({ editing: !this.state.editing, post: newPost }); } this.setState({ editing: !this.state.editing, }); }; //标题修改事件 titleChange = (e) => { this.setState({ post: { ...this.state.post, title: e.target.value }, }); }; render() { let post = this.state.post; return ( <li> <div> {this.state.editing ? ( <form> <textarea value={post.title} cols="40" rows="3" onChange={this.titleChange} ></textarea> </form> ) : ( post.title )} </div> <div> 创建人:<span>{post.author}</span> </div> <div> 创建时间:<span>{post.date}</span> </div> <div> <img src={like} alt="点赞" onClick={() => { this.onVote(post.id); }} /> {post.vote} </div> <p> <button onClick={this.saveHandle}> {this.state.editing ? "保存" : "编辑"} </button> </p> </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 ); console.log(newPosts); //更新状态,刷新UI this.setState({ posts: newPosts }); } //保存事件 saveHandle = (newPost) => { const newPosts = this.state.posts.map((item) => item.id === newPost.id ? newPost : item ); this.setState({ posts: newPosts }); console.log(newPosts); }; render() { return ( <div> <h2>帖子列表:</h2> {this.state.posts.map((item) => ( <PostItem key={item.id} post={item} onVote={this.voteHandle} onSave={this.saveHandle} /> ))} </div> ); } } export default PostList;
效果
2.2、非受控组件
2.2.1、使用非受控组件
在大多数情况下,我们推荐使用 受控组件 来处理表单数据。在一个受控组件中,表单数据是由 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 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。
import React, { Component } from "react"; export default class SearchForm extends Component { constructor(props) { super(props); //创建一个表单元素的引用 this.keyword = React.createRef(); } submitHandle = (e) => { console.log(this.keyword.current.value); this.keyword.current.style.backgroundColor = "lightgreen"; e.preventDefault(); //return false; }; render() { return ( <div> <form onSubmit={this.submitHandle}> <p> <label htmlFor="keyword">关键字:</label> <input type="text" id="keyword" name="keyword" ref={this.keyword} /> <button>搜索</button> </p> </form> </div> ); } }
2.2.2、默认值
在 React 渲染生命周期时,表单元素上的 value
将会覆盖 DOM 节点中的值。在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue
属性,而不是 value
。在一个组件已经挂载之后去更新 defaultValue
属性的值,不会造成 DOM 上值的任何更新。
如果直接给value赋值则会提示错误:
使用defaultValue
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
。
import React, { Component } from "react"; export default class SearchForm extends Component { constructor(props) { super(props); //创建一个表单元素的引用 this.keyword = React.createRef(); } submitHandle = (e) => { console.log(this.keyword.current.value); this.keyword.current.style.backgroundColor = "lightgreen"; e.preventDefault(); //return false; }; render() { return ( <div> <form onSubmit={this.submitHandle}> <p> <label htmlFor="keyword">关键字:</label> <input defaultValue="请输入搜索关键字" type="text" id="keyword" name="keyword" ref={this.keyword} /> <button>搜索</button> </p> <p> 同样, <input type="checkbox" defaultChecked={true} /> 和{" "} <input type="radio" defaultChecked={false} /> 支持 defaultChecked, <select defaultValue="usa"> <option value="zh-cn">中国</option> <option value="usa">美国</option> </select>{" "} 和 <textarea defaultValue="请输入多行文本" cols="40" rows="3" />{" "} 支持 defaultValue。 </p> </form> </div> ); } }
结果:
2.2.3、文件输入
在 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 />);
三、React新特性
3.1、render新的返回类型
React16之前render方法必须返回单个元素,现在render可以返回多种不同的元素:
render()
方法是 class 组件中唯一必须实现的方法。
当 render
被调用时,它会检查 this.props
和 this.state
的变化并返回以下类型之一:
- React 元素。通常通过 JSX 创建。例如,
<div />
会被 React 渲染为 DOM 节点,<MyComponent />
会被 React 渲染为自定义组件,无论是<div />
还是<MyComponent />
均为 React 元素。 - 数组或 fragments。 使得 render 方法可以返回多个元素。欲了解更多详细信息,请参阅 fragments 文档。
- Portals。可以渲染子节点到不同的 DOM 子树中。欲了解更多详细信息,请参阅有关 portals 的文档。
- 字符串或数值类型。它们在 DOM 中会被渲染为文本节点。
- 布尔类型或
null
。什么都不渲染。(主要用于支持返回test && <Child />
的模式,其中 test 为布尔类型。)
render()
函数应该为纯函数,这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互。
如需与浏览器进行交互,请在 componentDidMount()
或其他生命周期方法中执行你的操作。保持 render()
为纯函数,可以使组件更容易思考。
注意
如果
shouldComponentUpdate()
返回 false,则不会调用render()
。
3.1.1、返回数组
export default class Hi extends Component { render() { return [<span>Hello</span>, <span>React</span>, <span>render()!</span>]; } }
import React, { Component } from "react"; export default class Render1 extends Component { render() { return [ <h2 key={1}>Hello </h2>, <h2 key={2}> React</h2>, <h2 key={3}> Render!</h2>, ]; } }
需要增加key,不然报错。
3.1.2、fragments
- 可以将子列表分组,而无需向DOM添加额外节点
- 简单理解:空标签
- <React.Fragment></React.Fragment> 或 <></>
-
render() { return ( <React.Fragment> <ChildA /> <ChildB /> <ChildC /> </React.Fragment> ) }
- 以下面的代码为例,如果Columns组件返回多个td元素才能实现效果,但是如果我们在Columns组件中使用了div父元素,则会使td元素失效。Fragment则可以解决这个问题。
-
//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>
短语法
- 可以使用一种新的,且更简短的类似空标签的语法来声明 Fragments
- <> </>
- 不支持 key 或属性
-
const Cloumns = () => { render() { return ( <> <td>Hello</td> <td>World</td> </> ) } }
带key 的Fragments
- 使用显式 <React.Fragment> 语法声明的片段可能具有 key
- key 是唯一可以传递给 Fragment 的属性
-
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> ) }
3.1.3、Portals
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); }
3.2、错误边界
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 (......)
}
防止 页面 级别的崩溃~
示例:
ErrorCom.js
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 === 3) { throw new Error("发生了错误"); } } ); } }; render() { return ( <div> <h2>组件</h2> 点击3次就异常了 <button onClick={this.clickHandle}>{this.state.n}</button> </div> ); } }
index.js
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>
);
发生错误时,仅当前控制失效了:
3.3、自定义DOM属性
React 16 之前会忽略不是把的HTML和SVG属性,现在React会把不识别的属性传递给DOM。
React16之前:
<div cust-attr="someting"></div>
会被渲染成:
<div></div>
React 16渲染出来的节点:
<div cust-attr="someting"></div>
3.4、组件的state
3.4.1、组件state
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条依据判断:
- 这个变量是否通过props从父组件中获取?如果是,那么它不是一个状态
- 这个变量是否在生命周期中都保持不变?如果是,那么它不是一个状态
- 这个变量是否可以通过其他状态(state)或者属性(props)计算得到?如果是,那么它不是一个状态
- 这个变量是否在组件的render方法中使用?如果不是,那么它不是一个状态,这种情况更适合定义为组件的一个普通属性
3.4.2、正确修改state
①不能直接修改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; }))
3.4.3、state的更新是一个合并的过程
后设置的state会覆盖前面的状态,如果不存在则添加。
3.4.4、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"}
对象更新示例:
import React, { Component } from 'react' export default class StuList extends Component { constructor(props){ super(props); this.state={ stu:{id:1001,name:'tom',age:19} } } //普通属性 a=100; //普通属性 users = [ { id: 1001, name: "John" }, { id: 1002, name: "Jack" }, { id: 1003, name: "Rose" }, { id: 1004, name: "Lili" }, { id: 1005, name: "Jery" }, ]; handleAddProp=(e)=>{ //方法一 //Object.assign是重新定义对象,使用后面的对象覆盖前面的对象,有就替换,没有就添加 let newStudent=Object.assign({},this.state.stu,{height:198}); this.setState({stu:newStudent}); //方法二 this.setState({stu:{...this.state.stu,weight:70,age:88}}); } render() { return ( <div> <h2>学生信息:{JSON.stringify(this.state)}</h2> <p> <button onClick={this.handleAddProp}>增加属性height,weight</button> </p> </div> ) } }
数组更新示例:
import React, { Component } from 'react' export default class StuList extends Component { constructor(props){ super(props); this.state={ stus:[1,2,3,4,5,6,7,8,9,10] } } handleEditArray=(e)=>{ //追加11到数组结尾 //this.setState({stus:[...this.state.stus,11]}); //this.setState({stus:this.state.stus.concat(11)}); //过滤 //this.setState({stus:this.state.stus.filter(n=>n%2===1)}); //映射 //this.setState({stus:this.state.stus.map(n=>n%2===1?n:-1)}); //返回的是的移除的数组 // let newStus=[]; // let index=1; // for(var i=0; i<this.state.stus.length;i++){ // if(i!==index){ // newStus.push(this.state.stus[i]); // } // } let newStus=JSON.parse(JSON.stringify(this.state.stus)); newStus.splice(1,1); this.setState({stus:newStus}); } render() { return ( <div> <h2>数组:{JSON.stringify(this.state.stus)}</h2> <p> <button onClick={this.handleEditArray}>修改数组</button> </p> </div> ) } }
13.5、Axios
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
源代码与英文帮助:https://github.com/axios/axios
3.5.1、特性
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
3.5.2、浏览器支持

3.5.3、安装
使用 npm:
$ npm install axios
|
使用 bower:
$ bower install axios
|
使用 cdn:
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
3.5.4、案例
执行 GET
请求
// 为给定 ID 的 user 创建请求
|
执行 POST
请求
axios.post('/user', {
|
执行多个并发请求
function getUserAccount() {
|
3.5.5、axios API
可以通过向 axios
传递相关配置来创建请求
axios(config)
// 发送 POST 请求
|
// 获取远端图片
|
axios(url[, config])
// 发送 GET 请求(默认的方法)
|
3.5.6、请求方法的别名
为方便起见,为所有支持的请求方法提供了别名
axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.options(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
注意
在使用别名方法时, url
、method
、data
这些属性都不必在配置中指定。
3.5.7、并发
处理并发请求的助手函数
axios.all(iterable)
axios.spread(callback)
3.5.8、创建实例
可以使用自定义配置新建一个 axios 实例
axios.create([config])
const instance = axios.create({
|
3.5.9、实例方法
以下是可用的实例方法。指定的配置将与实例的配置合并。
axios#request(config)
axios#get(url[, config])
axios#delete(url[, config])
axios#head(url[, config])
axios#options(url[, config])
axios#post(url[, data[, config]])
axios#put(url[, data[, config]])
axios#patch(url[, data[, config]])
3.5.10、请求配置
这些是创建请求时可以用的配置选项。只有 url
是必需的。如果没有指定 method
,请求将默认使用 get
方法。
{ |