# PropTypes校验参数

每一个组件都有自己的Prop参数,这些参数是从父组件接收的一些属性。本节我们尝试校验参数的类型以及定义参数的默认值。

TodoItem组件代码可以看到,父组件TodoList给子组件TodoItem传递了参数contentdeleteItemindex,其中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的底层,我们可能这样做:

  1. 准备state数据
  2. 准备JSX模板
  3. 数据和+模板 结合,生成真实的DOM来显示
  4. state数据发生变化
  5. 数据+模板 结合,生成真实的DOM,替换原始的DOM

然而,使用新的DOM替换旧的DOM的操作将会极其耗费性能,DOM有一点变化都要使用新DOM替换旧DOM,这可不是一个好主意。

因此,我们改进实现方式:

  1. 准备state数据
  2. 准备JSX模板
  3. 数据+模板 结合,生成真实的DOM来显示
  4. state数据发生变化
  5. 数据+模板 结合,生成真实的DOM,并不直接替换旧的DOM
  6. 新的DOM和旧的DOM做对比,找差异
  7. 找出新旧DOM中变化的部分,替换旧的DOM变化的部分。

当然,这种实现和前一种实现相比,性能提升并不显著,因为虽然节省了替换旧DOM的性能开销,却增加了比较新旧DOM找差异的开销。因此,我们再次改进实现方法,这次我们引入虚拟DOM。

  1. 准备state数据
  2. 准备JSX模板
  3. 数据+模板 结合,生成虚拟DOM(虚拟DOM本质上就是一个JS对象,用于描述真实的DOM,极大地提高了程序的性能)
  4. 用虚拟DOM的结构生成真实的DOM
  5. state数据发生变化
  6. 数据+模板 生成新的虚拟DOM
  7. 比较旧虚拟DOM和新的虚拟DOM的区别,找到差异的部分(Diff算法,极大地提高了性能)
  8. 操作真实的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的生命周期函数。

React的生命周期函数

生命周期函数是指在某一时刻组件会自动调用执行的函数。render函数属于生命周期函数,因为当组件的props或state发生改变的时候,组件会自动调用执行该函数重新渲染页面。

React的生命周期主要分为以下过程:

  • 初始化过程:组件初始化自己的一些数据--props和state,一般调用构造函数初始化数据

  • 挂载过程:组件第一次被挂载到页面上时的过程

    • componentWillMount:在组件即将被挂载到页面的时刻自动执行

    • componentDidMount:在组件被挂载到页面后自动执行

    • render:挂载组件到页面

      需要注意的是:只有当组件第一次被挂载到页面上时,componentWillMountcomponentDidMount函数才会被执行。

  • 更新过程:对于组件来说,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页面。

打开Map Local settings

其次,新增一条Mapping用于响应Ajax请求。

新增Mapping

接着,我们在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

maplocal设置修正

访问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/

绑定模拟数据到待办列表

可以看到,数据已经成功绑定到列表上了并且程序各项功能正常。

LastUpdated: 3/11/2021, 7:31:36 PM