一.模块与组件以及模块化与组件化慨念
- 模块:向外提供特定功能的JS文件,便于复用JS,简化JS,提升JS效率数据、对数据的操作(函数)、将想暴露的私有的函数向外暴露(暴露的数据类型是对象)
2. 模块化:形容项目编码方式,即按模块编写与组织的项目。
3. 组件:用来实现特定布局功能效果的代码与资源集合,包含html、css、js、图片资源等,例如一个页面中头部区域的资源集合
4. 组件化:形容项目的编码方式,即按组件方式编写实现的项目。
二.组件化介绍
(1)React的组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件:
- 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component);
- 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component
)和有状态组件(Stateful Component) - 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container
Component);
(2)这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离:
- 函数组件、无状态组件、展示型组件主要关注UI的展示;
- 类组件、有状态组件、容器型组件主要关注数据逻辑;
(3)当然还有很多组件的其他概念:比如异步组件、高阶组件等。
三.各类组件介绍
(一) 类组件
类组件的定义要求
- 组件的名称是**大写字符开头**(无论类组件还是函数组件)
- 类组件需要继承自 React.Component
- 类组件必须实现render函数
在ES6之前,可以通过create-react-class 模块来定义类组件,但是目前官网建议我们使用ES6的class类定义。
使用class定义一个组件
- constructor是可选的,我们通常在constructor中初始化一些数据;
- this.state中维护的就是我们组件内部的数据
- render() 方法是 class 组件中唯一必须实现的方法
// AntdTest 组件名
export default class AntdTest extends Component {
render() {
return (
<div>
<Button type="primary">按钮</Button>
</div>
)
}
}
注意:render函数的返回值
当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一
1.react 元素:div还是<MyComponent>均为 React 元素
2.数组或 fragments:使得 render 方法可以返回多个元素。
3.Portals:可以渲染子节点到不同的 DOM 子树中。
4.字符串或数值类型:它们在 DOM 中会被渲染为文本节点
5.布尔类型或 null:什么都不渲染
(二) 函数组件
函数组件是使用function来进行定义的函数,这个函数会返回和类组件中render函数返回一样的内容。
特点
- 没有生命周期,也会被更新并挂载;
- 没有this(组件实例);
- 没有内部状态(state);
// 函数类型的组件
export function Welcome1(props) {
return <div>Welcome1, {
props.name}</div>
}
(三) 无状态组件(展示组件)
React是通过更新状态,来实现页面的局部动态变化。那么对于一些只做展示的组件,或者一些原子组件,比如说列表中的某一行,一个输入框等,就可以使用无状态组件。由于无状态组件内部不存在状态,因此也可以使用函数式组件进行书写。甚至如果功能比较简单,可以直接使用箭头函数书写。一些UI组件库使用的大多数都是无状态组件。
简洁概括
- 主要用来定义模板,接收来自父组件props传递的数据。
- 使用{props.xxx}的表达式把props放入模板中
- 无状态模板应该保持模板的纯粹性,以便于组件复用。
//无状态组件
const Child = (props)=>(
<input type="text" value={
props.value} onChange={
props.fun}/>
)
或
const PureComponent = (props) => (
<div>
//use props
</div>
)
特征
- 无状态组件无法访问生命周期的方法,因为它不需要组件生命周期和状态管理。
- 无状态组件不会被实例化【无实例化过程不需要分配多余的内存,所以性能更优;同样,由于无实例化也导致了无法访问this】
注意:函数组件,展示组件属于无状态组件
(四) 有状态组件
在无状态组件的基础上,如果组件内部包含状态(state)且状态随着事件或者外部的消息而发生改变的时候,这就构成了有状态组件。有状态组件通常会带有生命周期(lifecycle),用以在不同的时刻触发状态的更新。这种组件也是通常在写业务逻辑中最经常使用到的,根据不同的业务场景组件的状态数量以及生命周期机制也不尽相同。
概括
- 主要用来定义交互逻辑和业务数据。
- 使用{this.state.xxx}的表达式把业务数据挂载到容器组件的实例上,然后传递props到展示组件上,展示组件接收到props,把props放入到模板中
class StatefulComponent extends Component {
constructor(props) {
super(props);
this.state = {
//定义状态
}
}
componentWillMount() {
//do something
}
componentDidMount() {
//do something
}
... //其他生命周期
render() {
return (
//render
);
}
}
注意:类组件,容器组件 属于有状态组件
(五) 容器组件
在具体的项目实践中,我们通常的前端数据都是通过Ajax请求获取的,而且获取的后端数据也需要进一步的做处理。为了使组件的职责更加单一,引入了容器组件(Container Component)的概念。我们将数据获取以及处理的逻辑放在容器组件中,使得组件的耦合性进一步地降低。
/ 容器组件
export default class CommentList extends Component {
constructor(props) {
super(props)
this.state = {
comments: [],
}
}
componentDidMount() {
var _this = this
axios.get('/path/to/commentsApi').then(function (response) {
_this.setState({
comments: response.data })
})
}
render() {
return (
<div>
{
this.state.comments.map((c, i) => (
<Comment key={i} {...c} />
))}
</div>
)
}
}
如上面这个容器组件,就是负责获取用户数据,然后以props的形式传递给UserList组件来渲染。容器组件也不会在页面中渲染出具体的DOM节点,因此,它通常就充当数据源的角色。目前很多常用的框架,也都采用这种组件形式。如:React Redux的connect(), Relay的createContainer(), Flux Utils的Container.create()等。
(六) 展示组件
只做展示一些数据的信息的组件
// 展示组件
function Comment({
data }) {
return (
<div>
{
' '}
<p>{
data.body}</p> <p> --- {
data.author}</p>{
' '}
</div>
)
}
展示组件 vs 容器组件
容器组件负责数据获取,展示组件负责根据props显示信息
四.组件嵌套
组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件,再将这些组件组合嵌套在一起,最终形成我们的应用程序;
我们来分析一下下面代码的嵌套逻辑:
import React, {
Component } from 'react';
function Header() {
return <h2>Header</h2>
}
function Main() {
return (
<div>
<Banner/>
<ProductList/>
</div>
)
}
function Banner() {
return <div>Banner</div>
}
function ProductList() {
return (
<ul>
<li>商品1</li>
<li>商品2</li>
<li>商品3</li>
<li>商品4</li>
<li>商品5</li>
</ul>
)
}
function Footer() {
return <h2>Footer</h2>
}
export default class App extends Component {
render() {
return (
<div>
<Header/>
<Main/>
<Footer/>
</div>
)
}
}
如代码所示:
App组件是Header、Main、Footer组件的父组件;
Main组件是Banner、ProductList组件的父组件;
六. 组件通信
1) ⽗组件向⼦组件通信
传 props 的⽅式:是通过在父组件添加属性,在子组件通过this.props.xxx(类组件使用this.props.xxx;函数组件props.xxx)的形式传值。
思路:父组件向子组件传值,通过props,将父组件的state传递给了子组件。
import React, {
Component } from 'react'
export default class Correspond extends Component {
render() {
return (
<div>
<Chindren name="小白"></Chindren>
</div>
)
}
}
2) ⼦组件向⽗组件通信
可以采用 props + 回调 的⽅式( 通过调用父组件传过来的回调函数)
思路:子组件通过调用父组件传递到子组件的方法向父组件传递消息的。父组件收到参数后将值赋给父组件的state。
import React, {
Component } from 'react'
function Chindren(props) {
return <div>Welcome, {
props.name}!</div>
}
class Welcome extends Component {
constructor(props) {
super(props)
this.state = {
msg: this.props.toChildren,
}
}
toParent = () => {
// 通过props属性获取父组件的getdata方法,并将this.state值传递过去
this.props.callback('早上好!') //子组件通过此触发父组件的回调方法
}
render() {
return (
<div>
<button onClick={
this.toParent}> 副官向上级打招呼</button>
</div>
)
}
}
export default class Correspond extends Component {
constructor(props) {
super(props)
this.state = {
name: '副官',
back: '',
}
}
//用于接收子组件的传值方法,参数为子组件传递过来的值
changeMsg = (val) => {
//把子组件传递过来的值赋给this.state中的属性
this.setState({
back: val,
})
}
render() {
return (
<div>
{
/* 子组件向父组件传参数 */}
<div>上级:Welcome, {
this.state.name}!</div>
{
this.state.back && <div>副高:{
this.state.back}</div>}
{
/* 注意:子组件中props的回调函数名称 和 父组件传递添加的方法(如:callback) 需一致 */}
<Welcome callback={
this.changeMsg}></Welcome>
</div>
)
}
}
3) 兄弟组件通信
redux实现和利用父组件(prop 一层一层传递)
思路:兄弟组件之间的传值,是通过父组件做的中转 ,流程为组件A – 传值 --> 父组件 – 传值 --> 组件B
import React, {
Component } from 'react'
// Acls组件
class Acls extends Component {
//按钮点击事件,向父组件传值
handleClick() {
this.props.data('hello...React...')
}
render() {
return (
<div>
<button onClick={
this.handleClick.bind(this)}>
Acls组件中获取数据
</button>
</div>
)
}
}
//Bcls组件
class Bcls extends React.Component {
render() {
return <div>在Bcls组件中展示数据:{
this.props.mess}</div>
}
}
export default class Correspond extends Component {
constructor(props) {
super(props)
this.state = {
mess: '',
}
}
//向子组件A 组件 提供的传值方法,参数为获取的子组件传过来的值
getDatas(data) {
this.setState({
mess: data,
})
}
render() {
return (
<div>
{
/* 兄弟组件传值 */}
<Acls data={
this.getDatas.bind(this)}></Acls>
<Bcls mess={
this.state.mess}></Bcls>
</div>
)
}
}
4) 跨组件(如祖父与孙之间通信)
常用的方式有两种,逐层传值与跨层传值。
A. 逐层传值
先父子通信,然后再子“孙”通信,传递的层级变成祖父–>子–>“孙”,同理,通过props往下传,通过回调往上传。
B. 跨层传值
组件跨层级通信可使用Context 。
React官方文档对Context做出了解释:
在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递props。
一句话概括就是:跨级传值,状态共享。
这种模式下有两个角色,Provider和Consumer
Provider为外层组件,用来提供 数据;内部需要数据时用Consumer来读取
祖父——》孙
- 新建context.js文件(与父组件同级),默认值为一个对象
// 跨级传值 上下文
import React from 'react'
const MyContext = React.createContext({
text: 'luck' })
export default MyContext
- 祖父组件编写:在祖父组件引入context,使用一个 Provider 来将当前的 value 传递给以下的组件树,value为传递的值。
import React, {
Component } from 'react'
import MyContext from './context'
import Grandson from './Grandson'
// 子组件
class Children extends React.Component {
render() {
return (
<div>
<Grandson></Grandson>
</div>
)
}
}
export default class Correspond2 extends Component {
// 使用一个 Provider 来将当前的 value 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
render() {
return (
<div
style={
{
backgroundColor: '#f7ba2a',
padding: '20px',
width: '500px',
margin: 'auto',
textAlign: 'center',
}}
>
<p>父组件</p>
{
/* 对父组件:引入context,使用一个 Provider 来将当前的 value 传递给以下的组件树,value为传递的值。 */}
<MyContext.Provider value={
{
text: '你好' }}>
<Children></Children>
</MyContext.Provider>
</div>
)
}
}
- 孙组件Grandson.js :同样需引入context,在组件内部添加static contextType = MyContext,此时将能通过this.context直接获取到上层距离最近的Provider传递的值,此时this.context = {text:good luck},即祖父组件传递value。
import React from 'react'
import MyContext from './context'
class Grandson extends React.Component {
// 孙组件: 同样需引入context,在组件内部添加static contextType = MyContext,
// 此时将能通过this.context直接获取到上层距离最近的Provider传递的值,此时this.context = {
text:good luck},即父组件传递value。
static contextType = MyContext
render() {
return (
<div
style={
{
backgroundColor: '#13ce66',
padding: '10px',
width: '200px',
margin: 'auto',
marginTop: '20px',
}}
>
<p>孙组件:</p>
<span style={
{
color: 'blue' }}>{
this.context.text}</span>
</div>
)
}
}
export default Grandson
通过this.context.text获取到传递的值
孙——》祖父传值 ,可以通过回调的方式
- 对祖父组件进行传值修改,在传过来的对象中添加一个属性,里面绑定祖父组件的方法value={ {text:‘good luck’,toParent:this.fromGranson}}
import React, {
Component } from 'react'
import MyContext from './context'
import Grandson from './Grandson'
// 子组件
class Children extends React.Component {
render() {
return (
<div>
<Grandson></Grandson>
</div>
)
}
}
export default class Correspond2 extends Component {
// 使用一个 Provider 来将当前的 value 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
constructor(props) {
super(props)
this.state = {
msg: '',
}
}
fromGranson = (val) => {
this.setState({
msg: val,
})
}
render() {
return (
<div
style={
{
backgroundColor: '#f7ba2a',
padding: '20px',
width: '500px',
margin: 'auto',
textAlign: 'center',
}}
>
<p>祖父组件</p>
<span style={
{
color: 'red' }}>{
this.state.msg}</span>
{
/* 对父组件:引入context,使用一个 Provider 来将当前的 value 传递给以下的组件树,value为传递的值。 */}
<MyContext.Provider
value={
{
text: '你好', toParent: this.fromGranson }}
>
<Children></Children>
</MyContext.Provider>
</div>
)
}
}
- 孙组件:添加一个按钮,绑定方法,执行函数回调
import React from 'react'
import MyContext from './context'
class Grandson extends React.Component {
// 孙组件: 同样需引入context,在组件内部添加static contextType = MyContext,
// 此时将能通过this.context直接获取到上层距离最近的Provider传递的值,此时this.context = {
text:good luck},即父组件传递value。
static contextType = MyContext
toParent = () => {
this.context.toParent('孙组件向祖父组件传数据')
}
render() {
return (
<div
style={
{
backgroundColor: '#13ce66',
padding: '10px',
width: '200px',
margin: 'auto',
marginTop: '20px',
}}
>
<p>孙组件:</p>
<span style={
{
color: 'blue' }}>{
this.context.text}</span>
<div>
<button onClick={
this.toParent}>向上祖父组件</button>
</div>
</div>
)
}
}
export default Grandson
注意: 不管层级有多深,都可以使用context进行向下或向上传值。在下层组件中取的context中的字段需与value中传递字段保持一致。text与toParent
官网
Redux
数据共享状态
Redux基本语法
发布订阅模式event
发布订阅模式所有组件都可以。 event的方式比较灵活,不管是父子、跨级、还是同级,甚至毫无关联的组件,都可以使用此方式进行通信。
发布订阅模式
组件间通信需要引用一个类的实例,使用单例模式实现。
在 发布/订阅模式 有 发布者 和 订阅者,它们通过信道链接到一起。其主要包含三个对象:
订阅者:订阅一个或者多个信道消息的对象。
发布者:消息的发布者,往信道中投递消息的对象。
信道:每个信道都有一个名字,信道的实现细节对用户代码来说是隐藏的。
订阅 / 发布模型API设计思路
on(): 负责注册事件的监听(订阅),指定事件触发(发布)时的回调函数;
emit(): 负责触发事件,可以通过传参使其在触发的时候携带数据
off():负责监听器的删除(解除事件)
优缺点
在这种模式中,一个目标对象(被观察者)管理所有的依赖于它的对象(观察者),并且在它本身的状态发生变化的时候主动发出通知。
其主要包含两个对象:被观察者和观察者
优点:
跨组件进行通信
利于合作,对于某些操作,其他人不需要清楚具体的逻辑,只需要去发起就行。
缺点:
耦合问题:每个观察者必须和被观察对象绑定在一起,这引入了耦合
性能问题:在最基本的实现中观察对象必须同步地通知观察者。这可能会导致性能瓶颈。
案例
安装event
npm install event -save
- 新建event.js
import { EventEmitter } from ‘events’;
export default new EventEmitter(); - 另两个组件处于同层级
import React from 'react';
import GrandsonEven from './GrandsonEven';
import GrandsonOtherEven from './GrandsonOtherEven';
class Children extends React.Component {
render(){
return (
<div>
<GrandsonEven></GrandsonEven>
<GrandsonOtherEven></GrandsonOtherEven>
</div>
)
}
}
export default Children
- 在GrandsonEven组件里。导入event,在componentDidMount阶段添加监听on,事件名称与GrandsonOtherEven组件中emit一致
import React from 'react'
import event from './event'
class GrandsonEven extends React.Component {
constructor(props) {
super(props)
this.state = {
msg: '',
}
}
getData = (params) => {
this.setState({
msg: params,
})
}
componentDidMount() {
//监听eventMsg
event.on('eventMsg', this.getData)
}
render() {
return (
<div
style={
{
backgroundColor: '#13ce66',
padding: '10px',
width: '200px',
margin: 'auto',
marginTop: '20px',
}}
>
<p>组件一</p>
<div>组件二通过event传递值过来:{
this.state.msg}</div>
</div>
)
}
}
export default GrandsonEven
注意:在componentWillUnmount移除监听removeListener 这个事件,已经失效了
4. 在GrandsonOtherEven组件,导入event,按钮绑定方法,使用event.emit触发(发布)事件。
import React from 'react'
import event from './event'
class GrandsonOtherEven extends React.Component {
constructor(props) {
super(props)
this.state = {
msg: '',
}
}
toOther = () => {
//触发test事件的同时,传入希望testHandler感知的参数
event.emit('eventMsg', '你好')
}
render() {
return (
<div
style={
{
backgroundColor: '#13ce66',
padding: '10px',
width: '200px',
margin: 'auto',
marginTop: '20px',
}}
>
<p>组件二</p>
<div>
<button onClick={
this.toOther}>event传值</button>
</div>
</div>
)
}
}
export default GrandsonOtherEven
注意:如果两个组件使用event进行通信,确保发布订阅的事件名称一致,如上例中 eventMsg