# PropTypes校验参数
每一个组件都有自己的Prop参数,这些参数是从父组件接收的一些属性。本节我们尝试校验参数的类型以及定义参数的默认值。
从TodoItem
组件代码可以看到,父组件TodoList
给子组件TodoItem
传递了参数content
、deleteItem
、index
,其中content
是一个字符串、deleteItem
是一个函数,index
是数字。接下来,我们就使用PropTypes
给TodoItem组件参数增加校验功能。
在使用PropTypes之前,我们先引入PropTypes。
import PropTypes from 'prop-types';
然后使用PropTypes对参数进行校验。
TodoItem.propTypes = {
content: PropTypes.string,
deleteItem:PropTypes.func,
index:PropTypes.number
}
为了方便测试,我们添加一个test
参数到content
参数前,并且指定其参数类型为string。
import React ,{Component} from 'react';
import PropTypes from 'prop-types';
class TodoItem extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
render() {
const {content,test} = this.props;
return <div onClick={this.handleClick}>
{test}-{content}</div>;
}
handleClick() {
const {deleteItem,index} = this.props;
deleteItem(index);
}
}
TodoItem.propTypes = {
test: PropTypes.string,
content: PropTypes.string,
deleteItem: PropTypes.func,
index: PropTypes.number
}
export default TodoItem;
尝试在输入框中输入数据并提交,React不会报任何的错误。在React中,若我们不传递某一参数,则参数校验不生效。此时若我们坚持要求父组件传递test
参数给子组件,则可以在参数校验处一个isRequired
。
TodoItem.propTypes = {
test: PropTypes.string.isRequired,
content: PropTypes.string,
deleteItem: PropTypes.func,
index: PropTypes.number
}
再次输入数据并提交,提示test
参数是必须的。
# DefaultProps定义参数默认值
有的时候,父组件确实无法传递test
参数,那么我们就可以使用DefaultProps来定义参数的默认值,避免程序报错。
TodoItem.defaultProps = {
test:'hello world!'
}
此时程序不报错,并显示了test
参数的默认值。
# Props、State与render函数的关系
我们已经知道React是由数据驱动的一门框架,当数据发生变化时,页面自动会发生变化。接下来,我们来了解一下Props、State与render函数的关系,通过了解它们的关系,我们就能知道为什么数据发生变化,页面就能自动发生变化。
可以看到,当我们在输入框输入内容后,页面就会自动将输入的内容显示出来。查看TodoList
源代码,输入框和参数inputValue
做了绑定,只要参数inputValue
值发生变化,输入框的值就会发生变化。同时,输入框还监听了自身的OnChange
事件,当数据发生变化时,OnChange
事件获取输入的内容,更新state
中的inputValue
参数的值,当state中的值发生变化,页面也会跟着变化。
用一句话概括Props、State与render函数的关系:当组件的Props或State发生改变的时候,它的render函数将会重新执行。 redner函数的作用是渲染页面,所以页面也就发生了改变。
当然,数据不仅仅指的是state
,还包括props,当props方式变化时,页面也将重新渲染。为了更好地说明,我们定义一个Test
组件。
import React,{ Component } from 'react';
class Test extends Component {
render() {
console.log('Test render');
return <div>{this.props.content}</div>
}
}
export default Test;
以上就是Test
组件的定义,我们在TodoList
组件中引用它,并将inputValue
的值传递给Test
组件。
import React,{Component,Fragment} from 'react';
import TodoItem from './TodoItem';
import './style.css';
import Test from './Test';
class TodoList extends Component{
constructor(props) {
super(props);
this.state = {
inputValue:'',
list:[]
}
this.handleInputChange = this.handleInputChange.bind(this);
this.handleBtnClick = this.handleBtnClick.bind(this);
this.handleItemDelete = this.handleItemDelete.bind(this);
}
render() {
console.log('render');
return (
<Fragment>
<div>
<label htmlFor="insertArea" >输入内容</label>
<input id="insertArea" className="input" value={this.state.inputValue} onChange={this.handleInputChange} />
<button onClick={this.handleBtnClick} >提交</button>
</div>
<ul>
{this.getTodoItem()}
</ul>
<Test content={this.state.inputValue} />
</Fragment>
)
}
getTodoItem() {
return this.state.list.map(
(item,index)=> {
return (
<TodoItem key={index} content={item} index={index} deleteItem={this.handleItemDelete} />
)
}
)
}
handleInputChange(e) {
const value = e.target.value
this.setState(() => ({
inputValue: value
}));
}
handleBtnClick() {
this.setState((prevState) =>({
list:[...prevState.list,prevState.inputValue],
inputValue:''
}));
}
handleItemDelete(index) {
this.setState((prevState) => {
const list = [...prevState.list];
list.splice(index,1);
return {list}
});
}
}
export default TodoList;
当我们在输入框中输入内容时,输入框下方的数据也会实时变化。同时,可以看到父组件重新渲染之后,子组件也重新渲染了。
从这可以看出,当父组件的render函数被运行时,它的子组件的render函数都将被运行一次。
# React中的虚拟DOM
当组件的Props或State发生改变的时候,它的render函数将会重新执行,组件也就会被重新渲染。React中实现重新渲染其性能是非常高的,因为它引入了称为虚拟DOM的东西。
假设我们自己来实现React的底层,我们可能这样做:
- 准备state数据
- 准备JSX模板
- 数据和+模板 结合,生成真实的DOM来显示
- state数据发生变化
- 数据+模板 结合,生成真实的DOM,替换原始的DOM
然而,使用新的DOM替换旧的DOM的操作将会极其耗费性能,DOM有一点变化都要使用新DOM替换旧DOM,这可不是一个好主意。
因此,我们改进实现方式:
- 准备state数据
- 准备JSX模板
- 数据+模板 结合,生成真实的DOM来显示
- state数据发生变化
- 数据+模板 结合,生成真实的DOM,并不直接替换旧的DOM
- 新的DOM和旧的DOM做对比,找差异
- 找出新旧DOM中变化的部分,替换旧的DOM变化的部分。
当然,这种实现和前一种实现相比,性能提升并不显著,因为虽然节省了替换旧DOM的性能开销,却增加了比较新旧DOM找差异的开销。因此,我们再次改进实现方法,这次我们引入虚拟DOM。
- 准备state数据
- 准备JSX模板
- 数据+模板 结合,生成虚拟DOM(虚拟DOM本质上就是一个JS对象,用于描述真实的DOM,极大地提高了程序的性能)
- 用虚拟DOM的结构生成真实的DOM
- state数据发生变化
- 数据+模板 生成新的虚拟DOM
- 比较旧虚拟DOM和新的虚拟DOM的区别,找到差异的部分(Diff算法,极大地提高了性能)
- 操作真实的DOM,修改差异的部分。保持真实DOM结构和新虚拟DOM描述的结构对应。
这就是React渲染页面的底层实现,这种方式大大提高了程序的性能。
# React中ref的使用
ref实际上是reference的简写,它是一个引用,在react中我们使用ref操作DOM。在此之前,我们删除Test组件以及Test组件引用的相关代码。
接着,我们在input组件上定义一个ref。
<input id="insertArea"
className="input"
value={this.state.inputValue}
onChange={this.handleInputChange}
ref={(input) => {this.input = input} }/>
这说明我们构建了一个ref引用,这个引用叫做this.input
,指向输入框对应的DOM节点。这样handleInputChange
函数中的e.target
可以替换为this.input
。
实际上,不推荐在React中使用ref,建议使用数据驱动的方式编写代码。
# React生命周期函数简介
生命周期函数在React中非常重要,本节开始接受React的生命周期函数。
生命周期函数是指在某一时刻组件会自动调用执行的函数。render函数属于生命周期函数,因为当组件的props或state发生改变的时候,组件会自动调用执行该函数重新渲染页面。
React的生命周期主要分为以下过程:
初始化过程:组件初始化自己的一些数据--props和state,一般调用构造函数初始化数据
挂载过程:组件第一次被挂载到页面上时的过程
componentWillMount
:在组件即将被挂载到页面的时刻自动执行componentDidMount
:在组件被挂载到页面后自动执行render
:挂载组件到页面需要注意的是:只有当组件第一次被挂载到页面上时,
componentWillMount
和componentDidMount
函数才会被执行。
更新过程:对于组件来说,props和state发生变化时,执行的生命周期函数有些许不一样。
shouldComponentUpdate
:组件被更新之前,它会被执行,其返回布尔类型的值,决定组件是否需要更新。componentWillUpdate
:组件被更新之前,它会自动执行,但是它在shouldComponentUpdate
之后执行。如果shouldComponentUpdate
返回true,它才执行,否则就不会执行。componentDidUpdate
:组件更新完成之后,它会被执行
在更新过程的生命周期函数中,有一个比较特别的生命周期函数
componentWillReceiveProps
,其执行必须满足以下条件:- 首先,这个组件必须要从父组件中接收参数
- 如果这个组件第一次存在于父组件中,该生命周期函数不会执行
- 如果这个组件之前已经存在于父组件中,则该生命周期函数会执行
卸载过程:
componentWillUnmount
:当这个组件即将从页面中剔除的时候,这个函数就会执行。
# React生命周期函数应用
在讲React生命周期函数应用之前,我们删除测试代码,删除Test
组件,删除test
字段校验和默认值,删除所有的测试代码。
TodoList
组件代码:
import React,{Component,Fragment} from 'react';
import TodoItem from './TodoItem';
import './style.css';
class TodoList extends Component{
constructor(props) {
super(props);
this.state = {
inputValue:'',
list:[]
}
this.handleInputChange = this.handleInputChange.bind(this);
this.handleBtnClick = this.handleBtnClick.bind(this);
this.handleItemDelete = this.handleItemDelete.bind(this);
}
render() {
return (
<Fragment>
<div>
<label htmlFor="insertArea" >输入内容</label>
<input id="insertArea"
className="input"
value={this.state.inputValue}
onChange={this.handleInputChange} />
<button onClick={this.handleBtnClick} >提交</button>
</div>
<ul>
{this.getTodoItem()}
</ul>
</Fragment>
)
}
getTodoItem() {
return this.state.list.map(
(item,index)=> {
return (
<TodoItem key={index} content={item} index={index} deleteItem={this.handleItemDelete} />
)
}
)
}
handleInputChange(e) {
const value = e.target.value
this.setState(() => ({
inputValue: value
}));
}
handleBtnClick() {
this.setState((prevState) =>({
list:[...prevState.list,prevState.inputValue],
inputValue:''
}));
}
handleItemDelete(index) {
this.setState((prevState) => {
const list = [...prevState.list];
list.splice(index,1);
return {list}
});
}
}
export default TodoList;
TodoItem
组件代码:
import React ,{Component} from 'react';
import PropTypes from 'prop-types';
class TodoItem extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
render() {
const {content} = this.props;
return <div onClick={this.handleClick}>
{content}</div>;
}
handleClick() {
const {deleteItem,index} = this.props;
deleteItem(index);
}
}
TodoItem.propTypes = {
content: PropTypes.oneOfType([PropTypes.string,PropTypes.number]),
deleteItem: PropTypes.func,
index: PropTypes.number
}
export default TodoItem;
子组件的render函数在两种情况下会执行:
- 父组件render函数执行的时候重新执行时
- props或state发生变化
在我们这个demo中,只有当TodoList
提交内容时才需要重新渲染子组件,其它情况下是不需要渲染子组件的。因此,我们可以在TodoItem
组件的周期函数shouldComponentUpdate
中编写如下代码,提高组件性能。
shouldComponentUpdate(nextProps,nextState) {
if(nextProps.content !== this.props.content) {
return true;
}else {
return false;
}
}
这样,我们就可以减少TodoItem
不必要的渲染次数,节省开销。
# 使用Charles模拟发送Ajax请求
React没有提供发送Ajax请求的组件,若我们需要发送Ajax请求,需要在命令行中安装axios工具。
yarn add axios
由于没有后端接口,我们需要安装一个Charles模拟一个后端对Ajax请求进行响应并在桌面创建一个todolist.json
文件写入json数据。
["Dell","Lee","IMOOC"]
接下来,我们使用Charles模拟一个接口。
首先,打开Charles,选择Tools
->Map Local
打开Map Local settings页面。
其次,新增一条Mapping用于响应Ajax请求。
接着,我们在TodoList
组件的生命周期函数componentDidMount
中编写代码发送Ajax Get请求待办列表/api/todolist
接口。
若请求成功,则打印返回的数据并弹出succee
消息框。
若请求失败,则弹出error
消息框。
componentDidMount() {
axios.get('/api/todolist')
.then(
(res)=> {
console.log(res);
alert('succee');
}
)
.catch(
()=>{
alert('error')
}
);
}
访问http://localhost:3000
,弹出error
消息框,网络请求状态码404并且Charles无法拦截到该请求。
为了解决这个问题,我们需要将Mapping配置的host项值修改为localhost.charlesproxy.com
。
访问http://localhost.charlesproxy.com:3000/
而不是http://localhost:3000
。
可以看到,在响应结果中有一个data字段,展开字段就能看到写入json文件的数据。最后,我们将数据绑定到TodoList
组件的待办列表上。
componentDidMount() {
axios.get('/api/todolist')
.then(
(res)=> {
console.log(res.data);
this.setState(
() =>({list: [...res.data]})
)
}
)
.catch(
()=>{
alert('error')
}
);
}
再次访问http://localhost.charlesproxy.com:3000/
可以看到,数据已经成功绑定到列表上了并且程序各项功能正常。