React学习记录二:高级内容


chrome安装调试工具

打开chrome应用商店,搜索React Developer Tools,点击添加至Chrome即可。添加后,使用chrome打开react项目后,调试模式会多出React调试标签,其中可以看到每一步的组件及其内容,方便调试。

搜索redux devtools,添加至Chrome,添加后chrome多出redux标签。注意使用redux时如果提示no store found,则根据官网提示在创建store时reducer后面添加window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()。有关redux和store创建的内容见Redux部分。

PropTypes和DefaultProps

前面学习的TodoList中,父子组件传值时,对传递参数的类型并没有限制,有时我们需要对参数进行严格限制,或者实际开发中为方便在出现问题时提示警告等,此时就需要使用PropTypes。PropTypes和DefaultProps的简单使用如下,具体可以查看官方文档中Typechecking With PropTypes

import React, { Component } from 'react';
import PropTypes from 'prop-types';

class TodoItem extends Component {
    constructor(props) {
        super(props);
        this.handleItemDelete = this.handleItemDelete.bind(this);
    }
    render() {
        // 假设有一个必须的字符串参数,名为test
        const {content, testVal} = this.props;
        return (
            <li
                onClick={this.handleItemDelete}
            >
                {testVal}, {content}
            </li>
        )
    }

    handleItemDelete() {
        const { handleDelete, index } = this.props;
        handleDelete(index);
    }
}

TodoItem.propTypes = {
    content: PropTypes.string,
    handleDelete: PropTypes.func,
    index: PropTypes.number,
    testVal: PropTypes.string.isRequired
}

TodoItem.defaultProps = {
    testVal: 'hello'
}

export default TodoItem;

props、state和render函数的关系

  • 当组件的state或props发生改变时,render函数将重新执行;
  • 父组件的render函数执行时,其子组件的render函数也会重新执行;

实际上,props改变也是因为其父组件的state发生了改变,即调用了setState()方法。

setState()函数使用异步加载的方式,其目的也是为提升性能。如在短暂时刻内调用了多次setState()方法,则使用异步加载时可以实现只渲染一次页面而不是三次。

虚拟DOM

React作为数据驱动页面的框架,如果每次数据变化都替换实际DOM会非常耗性能,因此React中使用虚拟DOM的技术。

所谓虚拟DOM,其实就是用来描述真实DOM一个JS对象。例如:<div id="header"><span>Hello</span></div>这么一个DOM,可以使用['div', {id: 'header'}, ['span', {}, 'hello']]来表示(底层通过React.createElement()方法实现)。

使用虚拟DOM可以方便的对比每次数据变化时页面是否变化及变化的内容,高效的实现了页面刷新。

虚拟DOM比对的DIFF算法使用同层比对的方式由上到下对比,如果上层(父级)存在差异,则不再对比下层的元素直接进行更新替换。

列表元素建议使用key属性也是为了在DOM比对时提示性能,而使用index作为key达不到初衷。

ref的使用

React中使用DOM元素时,可以使用ref属性。如在TodoList功能中,使用了e.target来获取事件源元素,这里可以使用ref属性:

import React, {Component, Fragment} from 'react';
import TodoItem from './TodoItem';

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>
                {/*ref中,参数为当前元素,将当前元素绑定到该组件的this.input*/}
                <input
                    value={this.state.inputValue}
                    onChange={this.handleInputChange}
                    ref={(input)=>{this.input = input}}
                />
                <button
                    onClick={this.handleBtnClick}
                >
                    提交
                </button>
                <ul>
                    {this.getTodoItem()}
                </ul>
            </Fragment>
        )
    }

    getTodoItem() {
        return this.state.list.map((item, index)=>{
            return (
                <TodoItem
                    content={item}
                    key={index}
                    index={index}
                    handleDelete={this.handleItemDelete}
                />
            )
        })
    }

    handleInputChange(e) {
        // const value = e.target.value;
        // 使用ref时,这里可以直接使用input.value获取。参数e可以去掉。
        this.setState(() => ({
            // inputValue: value
            inputValue: this.input.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;

虽然可以通过ref属性获取DOM元素,但实际上不建议这么做,因为React本身是操作数据的框架。而且setState()方法是异步加载,在不熟练时容易出现问题。如现在在添加todoitem后要查看当前有几个todo项:

import React, {Component, Fragment} from 'react';
import TodoItem from './TodoItem';

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>
                <input
                    value={this.state.inputValue}
                    onChange={this.handleInputChange}
                />
                <button
                    onClick={this.handleBtnClick}
                >
                    提交
                </button>
                {/*假设要获取ul里todoitem数*/}
                <ul ref={(ul)=>{this.ul=ul}}>
                    {this.getTodoItem()}
                </ul>
            </Fragment>
        )
    }

    getTodoItem() {
        return this.state.list.map((item, index)=>{
            return (
                <TodoItem
                    content={item}
                    key={index}
                    index={index}
                    handleDelete={this.handleItemDelete}
                />
            )
        })
    }

    handleInputChange(e) {
        const value = e.target.value;
        this.setState(() => ({
            inputValue: value
        }));
    }

    handleBtnClick() {
        this.setState((prevState)=>({
            list: [...prevState.list, prevState.inputValue],
            inputValue: '',
        }));
        // 假设在添加todoitem项后想知道当前已有几项
        console.log(this.ul.querySelectorAll('li').length);
    }

    handleItemDelete(index) {
        this.setState((prevState)=>{
            const list = [...prevState.list];
            list.splice(index, 1)
            return {list}
        })
    }
}

export default TodoList;

查看控制台会发现,此时打印的项数永远比实际少1,这是因为setState()异步,导致实际为先打印项数再添加。为防止该类问题,实际应该将打印的逻辑放在setState()方法的第二个参数位置,表示setState()方法执行完成后执行的回调函数:

import React, {Component, Fragment} from 'react';
import TodoItem from './TodoItem';

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>
                <input
                    value={this.state.inputValue}
                    onChange={this.handleInputChange}
                />
                <button
                    onClick={this.handleBtnClick}
                >
                    提交
                </button>
                {/*假设要获取ul里todoitem数*/}
                <ul ref={(ul)=>{this.ul=ul}}>
                    {this.getTodoItem()}
                </ul>
            </Fragment>
        )
    }

    getTodoItem() {
        return this.state.list.map((item, index)=>{
            return (
                <TodoItem
                    content={item}
                    key={index}
                    index={index}
                    handleDelete={this.handleItemDelete}
                />
            )
        })
    }

    handleInputChange(e) {
        const value = e.target.value;
        this.setState(() => ({
            inputValue: value
        }));
    }

    handleBtnClick() {
        this.setState((prevState) => ({
            list: [...prevState.list, prevState.inputValue],
            inputValue: '',
        }), () => {
            console.log(this.ul.querySelectorAll('li').length);
        });
    }

    handleItemDelete(index) {
        this.setState((prevState)=>{
            const list = [...prevState.list];
            list.splice(index, 1)
            return {list}
        })
    }
}

export default TodoList;

生命周期函数

生命周期函数可以理解为,在某一时刻组件会自动调用的函数。

由React渲染页面的过程可以定义其生命周期函数包括:

  • 挂载时:componentWillMount、render、componentDidMount;
  • 更新时:
    • props更新:componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render、componentDidUpdate;
    • state更新:shouldComponentUpdate、componentWillUpdate、render、componentDidUpdate;
  • 销毁时:componentWillUnmount;

可以通过在代码中添加周期函数执行打印语句查看各个周期函数的执行顺序,方便理解其意义。总结如下:

  • 挂载中componentWillMount和componentDidMount,以及销毁的componentWillUnmount只会执行一次,即分别在挂载和销毁时执行;
  • render函数在每次数据更新(setState()方法调用)执行;
  • 更新中的函数中,shouldComponentUpdate方法的返回值决定后面的周期函数是否执行,如果返回false,则表示无需更新,此时不会执行后面的方法(包括render);
  • componentWillReceiveProps定义在有父组件,并且由父组件传递参数的子组件中。当子组件的参数发生变化时(非第一次加载)会执行该方法;
  • React组件中除render方法外,其余周期函数都有默认实现,因此定义组件时其它周期函数可以不定义,但render方法必须定义;

生命周期函数的使用场景:

  1. 在前面完成的TodoList功能中,通过在父子组件中render()函数添加打印可以发现,每次文本框输入内容都会导致父子组件重新渲染,这样比较浪费性能,因为这里已经加载出的列表项当没有删除时无需再次渲染。此时可以通过使用shouldComponentUpdate函数进行优化:
import React, { Component } from 'react';

class TodoItem extends Component {
    constructor(props) {
        super(props);
        this.handleItemDelete = this.handleItemDelete.bind(this);
    }

    // shouldComponentUpdate会接收两个参数,分别为nextProps和nextState
    shouldComponentUpdate(nextProps, nextState) {
        // 通过判断该项的当前的component和新参数中的component是否相等来决定是否重新渲染
        if (this.props.content !== nextProps.content) {
            return true;
        }
        return false;
    }
    render() {
        console.log('child rerendered.')
        const {content} = this.props;
        return (
            <li
                onClick={this.handleItemDelete}
            >
                {content}
            </li>
        )
    }

    handleItemDelete() {
        const { handleDelete, index } = this.props;
        handleDelete(index);
    }
}

export default TodoItem;
  1. 假设在TodoList案例中,首次加载时有默认的TodoItem是通过接口查询返回的,可以将接口定义放至componentDidMount函数中;

React中ajax使用及charles数据模拟

React本身没有内置ajax发送功能,需要使用axios模块完成ajax相关功能。

在终端项目路径下执行npm install axios完成安装,在componentDidMount函数中使用:

import React, {Component, Fragment} from 'react';
import axios from 'axios';
import TodoItem from './TodoItem';

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>
                <input
                    value={this.state.inputValue}
                    onChange={this.handleInputChange}
                />
                <button
                    onClick={this.handleBtnClick}
                >
                    提交
                </button>
                <ul>
                    {this.getTodoItem()}
                </ul>
            </Fragment>
        )
    }

    componentDidMount() {
        // 模拟成功提示succ,否则提示error
        axios.get('/api/todolist').then(()=>{
            alert('succ');
        }).catch(()=>{
            alert('error');
        })
    }

    getTodoItem() {
        return this.state.list.map((item, index)=>{
            return (
                <TodoItem
                    content={item}
                    key={index}
                    index={index}
                    handleDelete={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;

此时刷新页面会提示”error”。查看network可以看到实际发送了地址为http://localhost:3000/api/todolist的get请求,不过结果为404。

使用Charles软件模拟接口。首先在本地创建数据文件todolist.json,内容为:

["Java", "Python", "Hadoop"]

打开charles,菜单栏—Tools—Map Loacl—Add,添加接口地址:http://localhost:3000/api/todolist。映射文件选择创建的todolist.json。编写接口请求和处理:

import React, {Component, Fragment} from 'react';
import axios from 'axios';
import TodoItem from './TodoItem';

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>
                <input
                    value={this.state.inputValue}
                    onChange={this.handleInputChange}
                />
                <button
                    onClick={this.handleBtnClick}
                >
                    提交
                </button>
                <ul>
                    {this.getTodoItem()}
                </ul>
            </Fragment>
        )
    }

    componentDidMount() {
        axios.get('/api/todolist').then((res)=>{
            this.setState(() => ({
                list: res.data,
              	// 这里推荐使用[...res.data]
            }));
        }).catch(()=>{
            alert('error');
        })
    }

    getTodoItem() {
        return this.state.list.map((item, index)=>{
            return (
                <TodoItem
                    content={item}
                    key={index}
                    index={index}
                    handleDelete={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;

刷新页面,可以看到首次加载时页面显示出了由接口返回的默认列表项。

注意:使用最新版本的charles软件时,映射的http地址需要使用localhost.charlesProxy.com,而不是localhost,另外访问项目时使用http://localhost.charlesProxy.com:3000。另外要打开软件的macOS Proxy。

React的CSS动画

修改index.js,创建App.js:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App'

ReactDOM.render(<App />, document.getElementById('root'));
import React, { Component, Fragment } from 'react';

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            show: true
        };
        this.handleToggle = this.handleToggle.bind(this);
    }
    render() {
        return (
            <Fragment>
                <div className={this.state.show?'show':'hide'}>Hello</div>
                <button onClick={this.handleToggle}>toggle</button>
            </Fragment>
        );
    }

    handleToggle() {
        this.setState({
            show: !this.state.show
        });
    }
}

export default App;

此时刷新页面打开元素,可以看到点击toggle按钮时div的类名在show和hide间切换。

创建style.css并在app.js中引入import './style.css'

.show {
    opacity: 1;
}
.hide {
    opacity: 0;
}

此时点击按钮可以看到hello字符串在显示和不显示间切换。

通过CSS3动画效果可以添加显示和不显示的动画:

.show {
    opacity: 1;
    transition: all 1s ease-in;
}
.hide {
    opacity: 0;
    transition: all 1s ease-in;
}

再次点击时可以看到显示和不显示时有了过渡动画。

React通过keyframes可以定义CSS动画。

.show {
    animation: show-item 2s ease-in forwards;
}
.hide {
    /*
    最后一个参数forwards使得保留最后一帧效果,
    否则走完动画后并不会隐藏
    */
    animation: hide-item 2s ease-in forwards;
}
/*入场动画*/
@keyframes show-item {
    0% {
        opacity: 0;
        color: red;
    }
    50% {
        opacity: 0.5;
        color: green;
    }
    100% {
        opacity: 1;
        color: blue;
    }
}
/*出场动画*/
@keyframes hide-item {
    0% {
        opacity: 1;
        color: red;
    }
    50% {
        opacity: 0.5;
        color: green;
    }
    100% {
        opacity: 0;
        color: blue;
    }
}

react-transition-group动画实现

上面学习了使用CSS3动画方式实现简单动画。React还可以使用react-transition-group通过js实现动画效果。

在github官网搜索react-transition-group,查看文档,使用npm install react-transition-group --save安装。可以看到react-transition-group中包括Transition、CSSTrasition、SwitchTransition和TransitionGroup。首先点击CSSTransition查看文档。

在上面CSS实现动画时,我们手动修改div上面的类名,对不通类进行样式作定义实现动画。使用CSSTransition时我们不需要手动修改类名,只需要将要实现动画的元素外添加CSSTransition标签。

import React, { Component, Fragment } from 'react';
import { CSSTransition } from 'react-transition-group';
import './style.css'

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            show: true
        };
        this.handleToggle = this.handleToggle.bind(this);
    }
    render() {
        return (
            <Fragment>
                {/*
                通过给CSSTransition添加属性来实现动画
                */}
                <CSSTransition
                    in={this.state.show}  // 相当于定义出场和入场的key
                    timeout={1000}  // 动画时间
                    classNames='fade'
                >
                    <div>Hello</div>
                </CSSTransition>
                <button onClick={this.handleToggle}>toggle</button>
            </Fragment>
        );
    }

    handleToggle() {
        this.setState({
            show: !this.state.show
        });
    }
}

export default App;
/*
给CSSTransition定义in和timeout属性后,CSSTransiton会自动为元素挂载一些样式。
样式名fade有classNames属性定义。注意是classNames。
*/
/*
定义了in属性,即定义了入场动画key。入场动画第一个时刻,CSSTransition组件会往div标签挂载fade-enter样式
*/
.fade-enter {
    opacity: 0;
}
/*入场动画执行第二个时刻到执行完,div会有fade-enter-active类名*/
.fade-enter-active {
    opacity: 1;
    transition: opacity 1s ease-in;
}
/*动画执行完成后,fade-enter-done会添加到div类名上*/
.fade-enter-done {
    opacity: 1;
}
/*同样的,出场时也会默认添加几个样式类*/
.fade-exit {
    opacity: 1;
}
.fade-exit-active {
    opacity: 0;
    transition: opacity 1s ease-in;
}
.fade-exit-done {
    opacity: 0;
}

这样就通过js实现了动画效果。此时看上去同样的效果js反而复杂一些,实际上,通过这种方式可以定义额外操作。如:

import React, { Component, Fragment } from 'react';
import { CSSTransition } from 'react-transition-group';
import './style.css'

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            show: true
        };
        this.handleToggle = this.handleToggle.bind(this);
    }
    render() {
        return (
            <Fragment>
                {/*
                通过给CSSTransition添加属性来实现动画
                */}
                <CSSTransition
                    in={this.state.show}  // 相当于定义出场和入场的key
                    timeout={1000}  // 动画时间
                    classNames='fade'
                    unmountOnExit  // 此时出场后元素在页面消失
                    onEnter={(el)=>{el.style.color='red'}}  // 钩子函数实现入场时颜色控制,参数el为元素
	                  // onExited={() => setShowButton(true)}  // 还可以调用方法(官方文档中,这里未实现)。
                >
                    <div>Hello</div>
                </CSSTransition>
                <button onClick={this.handleToggle}>toggle</button>
            </Fragment>
        );
    }

    handleToggle() {
        this.setState({
            show: !this.state.show
        });
    }
}

export default App;

此时已经实现了带有动画的显示和隐藏,但刷新页面首次展示时并没有动画效果,为此,添加appear

属性:

import React, { Component, Fragment } from 'react';
import { CSSTransition } from 'react-transition-group';
import './style.css'

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            show: true
        };
        this.handleToggle = this.handleToggle.bind(this);
    }
    render() {
        return (
            <Fragment>
                {/*
                通过给CSSTransition添加属性来实现动画
                */}
                <CSSTransition
                    in={this.state.show}  // 相当于定义出场和入场的key
                    timeout={1000}  // 动画时间
                    classNames='fade'
                    unmountOnExit  // 此时出场后元素在页面消失
                    onEnter={(el)=>{el.style.color='red'}}  // 钩子函数实现入场时颜色控制,参数el为元素
                    // onExited={() => setShowButton(true)}  // 还可以调用方法(官方文档中,这里未实现)。
                    appear={true}  // 为使首次加载页面时也有动画
                >
                    <div>Hello</div>
                </CSSTransition>
                <button onClick={this.handleToggle}>toggle</button>
            </Fragment>
        );
    }

    handleToggle() {
        this.setState({
            show: !this.state.show
        });
    }
}

export default App;
/*
给CSSTransition定义in和timeout属性后,CSSTransiton会自动为元素挂载一些样式。
样式名fade有classNames属性定义。注意是classNames。
*/
/*
定义了in属性,即定义了入场动画key。入场动画第一个时刻,CSSTransition组件会往div标签挂载fade-enter样式
增加appear属性后,第一帧自动增加fade-appear,第二帧到完成增加fade-appear-active
*/
.fade-enter, .fade-appear {
    opacity: 0;
}
/*入场动画执行第二个时刻到执行完,div会有fade-enter-active类名*/
.fade-enter-active, .fade-appear-active {
    opacity: 1;
    transition: opacity 1s ease-in;
}
/*动画执行完成后,fade-enter-done会添加到div类名上*/
.fade-enter-done {
    opacity: 1;
}
/*同样的,出场时也会默认添加几个样式类*/
.fade-exit {
    opacity: 1;
}
.fade-exit-active {
    opacity: 0;
    transition: opacity 1s ease-in;
}
.fade-exit-done {
    opacity: 0;
}

Transition是CSSTransition更底层的使用,因此当CSSTranstion无法实现一些想要的动画时,理论上查找Transition的API会得到解决。

以上是针对一个DOM元素的动画使用,下面学习对多个元素添加动画效果:

import React, { Component, Fragment } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import './style.css'

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            list: []
        };
        this.handleAddItem = this.handleAddItem.bind(this);
    }
    render() {
        return (
            <Fragment>
                {/*使用transitionGroup对多个元素进行动画渲染*/}
                <TransitionGroup>
                {
                    this.state.list.map((item, index)=>{
                        return (
                            <CSSTransition
                                key={index}  // key值得放在最外层元素
                                // 外层使用TransitionGroupbao包裹后,内层除in属性外其余一样
                                // in={this.state.show}  // 相当于定义出场和入场的key
                                timeout={200}  // 动画时间
                                classNames='fade'
                                unmountOnExit  // 此时出场后元素在页面消失
                                onEnter={(el)=>{el.style.color='skyblue'}}  // 钩子函数实现入场时颜色控制,参数el为元素
                                // onExited={() => setShowButton(true)}  // 还可以调用方法(官方文档中,这里未实现)。
                                appear={true}  // 为使首次加载页面时也有动画
                            >
                                <div>{item}</div>
                            </CSSTransition>
                        )
                    })
                }
                </TransitionGroup>
                <button onClick={this.handleAddItem}>Add</button>
            </Fragment>
        );
    }

    handleAddItem () {
        this.setState((preState)=>({
            list: [...preState.list, 'item']
        }));
    }
}

export default App;

style.css文件内容不变。

可以看到,对多个元素进行动画渲染时,外层使用TransitionGroup包裹,单个元素只把in属性去掉即可。