react 入门与进阶教程 二
组件之间的交互
作为react学习中的一个非常重要的点,组件之间的交互还是需要我们认真掌握的。这个时候hello world就满足不了我们学习的欲望了,所以我们可以先把它给删掉。
那么组件之间的交互,大概可以分为如下两种:
父组件与子组件之间交互
子组件与子组件之间交互
悟空源码有一套腾讯课堂的react前端课程讲的很不错,需要的可以下载看下 一线大厂React实践宝典视频教程
当然可能有的人会问,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则是表示弹窗的提示内容。
我们自定义的钩子函数submit
和close
则分别是与子组件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的打算。
因此这里我就不多讲,提供一些参考学习资料。
版权声明:本文来源于互联网,如有侵权,请联系下方邮箱,一个工作日删除!