react 入门与进阶教程 二

组件之间的交互

作为react学习中的一个非常重要的点,组件之间的交互还是需要我们认真掌握的。这个时候hello world就满足不了我们学习的欲望了,所以我们可以先把它给删掉。

那么组件之间的交互,大概可以分为如下两种:

当然可能有的人会问,2个不相干的组件之间如何交互?如果,你的代码里,出现了两个不相干的组件还要交互,那说明你的组件划分肯定是有问题的。这就是典型的给自己挖坑找事儿。即使确实有,那也是通过react-redux把他们变成子组件对吧。但是,通常情况下,不到万不得已,并不建议使用react-redux,除非你的项目确实非常庞大了,需要管理的状态非常多了,已经不得不使用,一定要记住,react-redux这类状态管理器是最后的选择。

我们来想想一个简单常见的场景:页面里有一个submit提交按钮,当我们点击提交后,按钮前出现一个loading图,并变为不可点击状态,片刻之后,接口请求成功,飘出一个弹窗,告诉你,提交成功。大家可以想一想,这种场景,借助react组件应该如何做?

首先可以很简单的想到,将按钮与弹窗分别划分为两个不同的组件:

。然后创建一个父组件来管理这两个子组件

那么在父组件中,我们需要考虑什么因素?Button的loading图是否展示,弹窗是否展示对吧。

OK,根据这些思考,我们开始来实现这个简单的场景。

首先创建一个Button组件。在src目录下创建一个叫做Button.jsx的文件,代码如下:

 

 

 

 

 

JavaScript

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

// src/Button.jsx

import React from 'react';

 

const Button = (props) => {

    const { children, loading, submit } = props

    return (

        <button onClick={submit} disabled={ loading ? 'disabled' : null }>

            { loading && <i className="loading"></i> }

            { children }

        </button>

    )

 

}

 

export default Button;

 

注意,当你引入了一个新创建的文件时,可能需要重新启动服务才会找得到新的组件

由于这里的Button组件仅仅是简单的展示,并无额外的逻辑需要处理,因此我们使用无状态的组件。在这个组件里,出现了一个新的知识点:children

 

 

 

 

 

JavaScript

 

1

2

3

4

5

6

7

8

9

// 假如我们这样使用Button组件时

<Button>确认</Button>

 

// 那么标签中间的确认二字就会放入props的children属性中

// 无状态组件中

props.children = '确认'

 

// 有状态组件中

this.props.children = '确认'

当然,children还可以是更多的元素,这和我们熟知的DOM元素的children保持一致。

还有一个需要注意的知识点,则是在jsx模板中,我们可以使用JavaScript表达式来执行简单的逻辑处理

我们可以列举一些常见的表达式:

 

 

 

 

 

JavaScript

 

1

2

3

4

5

6

7

<div>{ message }</div>

 

<Button disabled={ loading ? 'disabled' : null }></Button>

 

{ dialog && <Dialog /> }

 

{ pending ? <Aaaa />  : <Bbbb /> }

 

如果对于JavaScript表达式了解不够多的朋友,建议深入学习一下相关的知识。

理解了这些知识之后,相信对于上面的Button组件所涉及到的东西也就能够非常清楚知道是怎么回事了。接下来,我们需要创建一个弹窗组件,Dialog.jsx

 

 

 

 

 

 

JavaScript

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

// src/Dialog.jsx

import React, { Component } from 'react';

 

const Dialog = (props) => {

    const { message, close } = props;

    return (

        <div className="dialog-backdrop">

            <div className="dialog-container">

                <div className="dialog-header">提示</div>

                <div className="dialog-body">{ message }</div>

                <div className="dialog-footer">

                    <button className="btn" onClick={ close }>确定</button>

                </div>

            </div>

        </div>

    )

}

 

export default Dialog;

 

 

这个组件没有太多特别的东西,唯一需要关注的一点是,我们也可以通过props传递一个函数给子组件。例如这里的close方法。该方法在父组件中定义,但是却在子组件Dialog中执行,他的作用是关闭弹窗。

我们很容易知道父组件想要修改子组件,只需要通过改变传入的props属性即可。那么子组件想要修改父组件的状态呢?正是父组件通过向子组件传递一个函数的方式来改变。

该函数在父组件中定义,在子组件中执行。而函数的执行内容,则是修改父组件的状态。这就是close的原理,我们来看看父组件中是如何处理这些逻辑的。

创建一个父组件App.jsx

 

 

 

 

 

 

JavaScript

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

// src/App.jsx

import React, { Component } from 'react';

import Button from './Button';

import Dialog from './Dialog';

import './style.css';

 

class App extends Component {

    state = {

        loading: false,

        dialog: false,

        message: 'xxx'

    }

 

    submit = () => {

        this.setState({

            loading: true

        })

 

        // 模拟数据请求的过程,假设数据请求会经历1s得到结果

        setTimeout(() => {

 

            // 通过随机数的方式模拟可能出现的成功与失败两种结果

            const res = Math.random(1);

            if (res > 0.5) {

                this.setState({

                    dialog: true,

                    message: '提交成功!'

                })

            } else {

                this.setState({

                    dialog: true,

                    message: '提交失败!'

                })

            }

            this.setState({ loading: false })

        }, 1000)

    }

 

    close = () => {

        this.setState({

            dialog: false

        })

    }

 

    render () {

 

        const { loading, dialog, message } = this.state;

 

        return (

            <div className="app-wrap">

                <Button loading={ loading } submit={ this.submit }>提交</Button>

                { dialog && <Dialog message={ message } close={ this.close } /> }

            </div>

        )

    }

}

 

export default App;

App组件的state中,loading用于判断Button按钮是否显示loading图标,dialog用于判断是否需要显示弹窗,message则是表示弹窗的提示内容。

我们自定义的钩子函数submitclose则分别是与子组件Button与Dialog交互的一个桥梁。前面我们说过了,想要在子组件中改变父级的状态,就需要通过在父组件中创建钩子函数,并传递给子组件执行的方式来完成。

在App.jsx中我们还看到代码中引入了一个css文件。这是构建工具帮助我们整合的方式,我们可以直接将css文件当做一个单独的模块引入进来。我们还可以通过同样的方式引入图片等资源。

style.css也是在src目录下创建的。

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

// src/style.scss

button {

    background: none;

    border: none;

    outline: none;

    width: 100px;

    height: 30px;

    border: 1px solid orange;

    border-radius: 4px;

    font-size: 16px;

    display: block;

    margin: 20px auto;

}

 

.loading {

    display: inline-block;

    width: 10px;

    height: 10px;

    border: 2px solid #ccc;

    border-radius: 10px;

    margin-right: 10px;

    border-bottom: transparent;

    border-top: transparent;

    animation-name: loading;

    animation-duration: 1s;

    animation-timing-function: linear;

    animation-iteration-count: infinite;

}

 

.dialog-backdrop {

    background: rgba(0, 0, 0, 0.2);

    position: fixed;

    top: 0;

    left: 0;

    right: 0;

    bottom: 0;

}

 

.dialog-container {

    width: 300px;

    background: #FFFFFF;

    border-radius: 4px;

    position: absolute;

    top: 20%;

    left: 50%;

    transform: translate(-50%, -50%);

    padding: 10px;

}

 

.dialog-header {

    height: 20px;

    text-align: center;

    line-height: 20px;

}

 

.dialog-body {

    line-height: 1.6;

    text-align: center;

    margin-top: 20px;

}

 

.dialog-footer {

    margin-top: 20px;

}

.dialog-footer button {

    margin: 0 auto;

    border: none;

    background: orange;

    color: #fff;

}

 

@keyframes loading {

    from {

        transform: rotate(0);

    }

 

    to {

        transform: rotate(360deg);

    }

}

最后修改index.js,即可将程序运行起来。

 

 

1

2

3

4

5

6

7

8

// src/index.js

import React from 'react';

import { render } from 'react-dom';

import App from './App';

 

const root = document.querySelector('#root');

 

render(, root);

 

点击之后loading出现

1s之后得到结果,提示框出现

那么总结一下组件之间的交互。

父组件改变子组件,通过改变传入的props属性值即可。
而子组件改变父组件的状态,则需要在父组件中创建钩子函数,然后让钩子函数通过props传递给子组件,并在子组件中执行。

那么子组件与子组件之间的交互方式,也就是通过影响共同的父组件来进行交互的。正如我们这个例子中的点击按钮,出现弹窗一样。这就是react组件之间交互的核心。

异步组件

在学习异步组件之前,可能还需要大家去折腾一下如何禁用浏览器的跨域限制。禁用跨域限制可以让我们使用更多的公共api进行学习,但是很多人并不知道还可以这样玩。总之一句话,知道了如何禁用浏览器的跨域限制,会让你的学习速度提升很多,很多项目你就可以动手自己尝试了。

我这里只能提供在mac环境下如何禁用chrome浏览器的跨域限制。在命令行工具中输入以下指令启动chrome即可。

 

 

1

> open -a "Google Chrome" --args --disable-web-security  --user-data-dir

 

启动成功之后会有这样的提示

在safari浏览器中则更加简单。

开发 -> 停用跨源限制

windows环境下如何做需要大家自己去研究。

OK,禁用跨域限制以后,我们就可以自如的请求别人的接口。这个时候再来学习异步组件就能轻松很多。

异步组件并不是那么复杂,由于接口请求会经历一点时间,因此在组件第一次渲染的时候,并不能直接将我们想要的数据渲染完成,那么就得再接口请求成功之后,重新渲染一次组件。上面的知识已经告诉大家,通过使用this.setState修改state的值可以达到重新渲染的目的。

所以我们通常的做法就是在接口请求成功之后,使用this.setState

为了降低学习难度,我们暂时先使用jquery中提供的方法来请求数据。

目前比较常用的是axios

首先在我们的项目中,安装jquery库。我们通常都会使用这样的方式来安装新的组件和库。

 

 

1

> npm install jquery

然后在src目录下创建一个News.jsx,借助知乎日报的api,我们来尝试完成一个简单的异步组件。

 

 

 

 

 

 

JavaScript

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

// src/News.jsx

import React, { Component } from 'react';

import $ from 'jquery';

 

class News extends Component {

    state = {

        stories: [],

        topStories: []

    }

    componentDidMount() {

        $.get('http://news-at.zhihu.com/api/4/news/latest').then(resp => {

            console.log(resp);

            this.setState({

                stories: resp.stories,

                topStories: resp.top_stories

            })

        })

    }

    render() {

        const { stories, topStories } = this.state;

        // 观察每一次render数据的变化

        console.log(this.state);

        return (

            <div className="latest-news">

                <section className="part1">

                    <div className="title">最热</div>

                    <div className="container">

                        {

                            topStories.map((item, i) => (

                                <div className="item-box" key={i}>

                                    <img src={ item.image } alt=""/>

                                    <div className="sub-title">{ item.title }</div>

                                </div>

                            ))

                        }

                    </div>

                </section>

 

                <section className="part2">

                    <div className="title">热门</div>

                    <div className="container">

                        {

                            stories.map((item, i) => (

                                <div className="item-box" key={i}>

                                    <img src={ item.images[0] } alt=""/>

                                    <div className="sub-title">{ item.title }</div>

                                </div>

                            ))

                        }

                    </div>

                </section>

            </div>

        )

    }

}

 

export default News;

 

 

style.css中简单补上相关的css样式

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

// src/style.css

.latest-news {

    width: 780px;

    margin: 20px auto;

}

.latest-news section {

    margin-bottom: 20px;

}

.latest-news .title {

    height: 40px;

    line-height: 40px;

    font-size: 16px;

    padding: 0 10px;

}

.latest-news .container {

    display: flex;

    flex-wrap: wrap;

    justify-content: space-around;

}

.latest-news .item-box {

    width: 30%;

    overflow: hidden;

    margin-bottom: 20px;

}

 

.latest-news .item-box img {

    width: 100%;

    height: 200px;

}

.latest-news .item-box .sub-title {

    font-size: 12px;

    line-height: 1.6;

    margin-top: 10px;

}

并在App.jsx中引入使用即可。

 

 

1

2

3

4

5

// src/App.jsx

import News from './News';

 

// 将下面这一句放于render函数的jsx模板中即可

 

 

组件运行结果展示

这个组件除了获取数据,没有额外的逻辑处理,但仍然有几个需要非常注意的地方。

1、 若非特殊情况,尽量保证数据请求的操作在componentDidMount中完成。

2、 react中的列表渲染通常通过调用数组的原生方法map方法来完成,具体使用方式可参考上例。

3、为了确保性能,被渲染的每一列都需要给他配置一个唯一的标识,正入上栗中的key={i}。我们来假想一个场景,如果我们在数组的最前面新增一条数据,如果没有唯一的标识,那么所有的数据都会被重新渲染,一旦数据量过大,这会造成严重的性能消耗。唯一标识会告诉react,这些数据已经存在了,你只需要渲染新增的那一条就可以了。

4、如果你想要深入了解该组件的具体变化,你可以在render方法中,通过console.log(this.state)的方式,观察在整个过程中,组件渲染了多少次,已经每一次this.state中的具体值是什么,是如何变化的。

高阶组件

很多人写文章喜欢把问题复杂化,因此当我学习高阶组件的时候,查阅到的很多文章都给人一种高阶组件高深莫测的感觉。但是事实上却未必。我们常常有一些口头俗语,比如说“包一层”就是可以用来简单解释高阶组件的。在普通组件外面包一层逻辑,就是高阶组件。

在进一步学习高阶组件之前,我们来回顾一下new与构造函数之间的关系。在前面我有文章提到过为什么构造函数中this在运行时会指向new出来的实例,不知道还有没有人记得。我将那段代码复制过来。

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

// 先一本正经的创建一个构造函数,其实该函数与普通函数并无区别

var Person = function(name, age) {

    this.name = name;

    this.age = age;

    this.getName = function() {

        return this.name;

    }

}

 

// 将构造函数以参数形式传入

function New(func) {

 

    // 声明一个中间对象,该对象为最终返回的实例

    var res = {};

    if (func.prototype !== null) {

 

        // 将实例的原型指向构造函数的原型

        res.__proto__ = func.prototype;

    }

 

    // ret为构造函数执行的结果,这里通过apply,将构造函数内部的this指向修改为指向res,即为实例对象

    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));

 

    // 当我们在构造函数中明确指定了返回对象时,那么new的执行结果就是该返回对象

    if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {

        return ret;

    }

 

    // 如果没有明确指定返回对象,则默认返回res,这个res就是实例对象

    return res;

}

 

// 通过new声明创建实例,这里的p1,实际接收的正是new中返回的res

var p1 = New(Person, 'tom', 20);

console.log(p1.getName());

 

// 当然,这里也可以判断出实例的类型了

console.log(p1 instanceof Person); // true

在上面的例子中,首先我们定义了一个本质上与普通函数没区别的构造函数,然后将该构造函数作为参数传入New函数中。我在New函数中进行了一些的逻辑处理,让New函数的返回值为一个实例,正因为New的内部逻辑,让构造函数中的this能够指向返回的实例。这个例子就是一个“包一层”的案例。

再来看一个简单的例子:

 

 

 

 

 

JavaScript

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

import React, { Component } from 'react';

 

class Div extends Component {

    componentDidMount() {

        console.log('这是新增的能力');

    }

    render () {

        return (

            <div>{ this.props.children }</div>

        )

    }

}

 

export default Div;

 

 

在上面的例子中,我们把html的DIV标签作为基础元件。对他新增了一个输出一条提示信息的能力。而新的Div组件,就可以理解为div标签的高阶组件。所以到这里希望大家已经理解了包一层的具体含义。

react组件的高阶组件,就是在基础react组件外面包一层,给该基础组件赋予新的能力。

OK,我们来试试定义第一个高阶组件,该高阶组件的第一个能力,就是向基础组件中传入一个props参数。

在例子中,传入的参数可能没有任何实际意义,但是在实际开发中,我们可以传入非常有必要的参数来简化我们的代码和逻辑。

先来定义一个拥有上述能力的高阶组件

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

// src/Addsss.jsx

import React from 'react';

 

// 定义一个接受一个react组件作为参数的函数

function Addsss(Container) {

 

    // 该函数返回一个新的组件,我们可以在该组件中进行新能力的附加

    return class Asss extends React.Component {

        componentDidMount() {}

        render() {

            return (

                { this.props.children }

            )

        }

    }

}

 

export default Addsss;

尽管这个高阶组价足够简单,但是他已经呈现了高阶组件的定义方式。现在我们在一个基础组件中来使用该高阶组件。

 

 

 

 

 

JavaScript

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

// src/basic.jsx

import React, { Component } from 'react';

import Addsss from './Addsss';

 

class Basic extends Component {

    componentDidMount() {

        console.log(this.props.name);

    }

    render() {

        return (

            <div className={this.props.name}>{ this.props.children }</div>

        )

    }

}

 

export default Addsss(Basic);

 

 

我们看到其实在基础组件中,对外抛出的接口是Addsss(Basic),这是高阶组件里定义的函数运行的结果。也就是说,其实基础组件中返回的是高阶组件中定义的Asss中间组件。这和new的思路几乎完全一致。

当然,想要理解,并熟练使用高阶组件并不是一件容易的事情,大家初学时也不用非要完全掌握他。当你对react慢慢熟练之后,你可以尝试使用高阶组件让自己的代码更加灵活与简练。这正是向函数式编程思维转变的一个过程。

在进步学习的过程中,你会发现无论是路由组件react-router,或者react-redux都会使用高阶组件来实现一些功能。只要你遇到他们的时候,你能明白,哦,原来是这么回事儿就行了。

react路由

react提供了react-router组件来帮助我们实现路由功能。

但是react-router是一个不太好讲的知识点。因为由于react-router 4进行了颠覆性的更新,导致了react-router 3与react-router 4的使用方式大不一样。也正是由于变化太大,所以很多项目仍然正在使用react-router3,并且没有过渡到react-router4的打算。

因此这里我就不多讲,提供一些参考学习资料。


本文链接:/study/develop/18325.html

版权声明:本文来源于互联网,如有侵权,请联系下方邮箱,一个工作日删除!