React—06—组件化;组件生命周期;组件通信(父子组件通信、插槽、作用域插槽、context全局);
零、什么是组件化
组件化思想的应用:
有了组件化的思想,我们在之后的开发中就要充分的利用它。 尽可能的将页面拆分成一个个小的、可复用的组件。 这样让我们的代码更加方便组织和管理,并且扩展性也更强。
React的组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件:
- 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component);
- 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component);
- 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component);
这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离:
- 函数组件、无状态组件、展示型组件主要关注UI的展示;
- 类组件、有状态组件、容器型组件主要关注数据逻辑; 当然还有很多组件的其他概念:
比如异步组件、高阶组件等,我们后续再学习。
一、类组件
类组件的定义有如下要求:
- 组件的名称是大写字符开头(无论类组件还是函数组件)
- 类组件需要继承自 React.Component
- 类组件必须实现render函数;render() 方法是 class 组件中唯一必须实现的方法;
- constructor函数是可选的,但我们通常在constructor中初始化一些数据; this.state中维护的就是我们组件内部的数据;
(2)render函数的返回值
当this.setState修改后,render函数会被重新调用;并且返回以下类型之一:
- React元素即React.createElement(): 就是我们JSX语法创建的东西。 例如,<div /> 会被babel和React 渲染为 DOM 节点,<MyComponent /> 会被 babel和React 渲染为自定义组件; 无论是 <div /> 还是 <MyComponent /> 均为 React 元素。
- 数组:react会把数组元素遍历出来按顺序展示。
- fragments:使得 render 方法可以返回多个元素。
- Portals:可以渲染子节点到不同的 DOM 子树中。
- 字符串或数值类型:它们在 DOM 中会被渲染为文本节点
- 布尔类型或 null或undefined:什么都不渲染。
我们写的jsx本质不就是React.createElement()吗?所以jsx就是React.createElement()的语法糖,本质就是react元素。
1 class App extends React.Component { 2 constructor() { 3 super(); 4 this.state = { 5 message: 'Hello React Scaffold' 6 }; 7 } 8 9 render() { 10 const { message } = this.state; 11 12 // 1.react元素即React.createElement 13 // return ( 14 // <div> 15 // <h2>{message}</h2> 16 // <HelloWorld /> 17 // </div> 18 // ); 19 20 // // 2. 数组(数组里面什么类型都可以写,最终会把
数组元素遍历出来按顺序展示) 21 // return [<h1>Hello World</h1>, 111, '星际穿越']; 22 23 // // 3. 字符串 24 // return 'Hello World'; 25 26 // // 4. 数字 27 // return 111; 28 29 // // 5. 布尔值 30 return true; 31 } 32 }
二、函数式组件
函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容。
函数组件有自己的特点(当然,后面我们会讲hooks,就不一样了):
- 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
- this关键字不能指向组件实例(因为没有组件实例);
- 没有内部状态(state);
函数式组件,要返回的类型和类组件中render函数返回一样的类型:
1 function App() { 2 // 1.react元素即React.createElement 3 // return ( 4 // <div> 5 // <h2>{message}</h2> 6 // <HelloWorld /> 7 // </div> 8 // ); 9 10 // // 2. 数组 11 // return [<h1>Hello World</h1>, 111, '星际穿越']; 12 13 // // 3. 字符串 14 // return 'Hello World'; 15 16 // // 4. 数字 17 return 111; 18 19 // // 5. 布尔值 20 // return true; 21 }
三、生命周期
export default class App extends React.Component { constructor() { super(); console.log('[ 1111constructor ] >'); } render() { console.log('[ 2222render ] >'); return ( <div> <h2>Hello World</h2> </div> ); } componentDidMount() { console.log('[ 3333 componentDidMount ] >'); }
}
一个组件的挂载流程:
先执行constructor里的内容,然后再执行render函数里的内容,最后在渲染完成后,执行componentDidMount钩子函数。
一个组件的更新流程:
先执行render函数,更新完成后,再执行componentDidUpdate钩子函数。(挂载即首次渲染流程,不会执行componentDidUpdate)
一个组件的销毁流程:
在销毁之前,执行componentWillUnmount钩子函数。
父子组件的挂载流程:
父constructor --》父render-》子constructor-》子render-》子componentDidMount钩子-》父componentDidMount钩子。
四、 生命周期函数
1.Constructor
如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
constructor中通常只做两件事情: 通过给 this.state 赋值对象来初始化内部的state; 为事件绑定实例(this);
2.componentDidMount
componentDidMount() 会在组件挂载完成后(插入 DOM 树后)立即调用。
componentDidMount中通常进行哪里操作呢?
- 依赖于DOM的操作可以在这里进行,因为此时dom已经挂载完成。;
- 在此处发送网络请求就最好的地方;(官方建议)
- 可以在此处添加一些订阅(会在componentWillUnmount取消订阅);
3.componentDidUpdate
componentDidUpdate() 会在更新完成后会被立即调用,首次渲染不会执行此方法。
当组件更新完成后,可以在此处对 DOM 进行操作;
如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。
4.componentWillUnmount
componentWillUnmount() 会在组件卸载及销毁之前直接调用。
在此方法中执行必要的清理操作; 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等;
以下是不常用的生命周期函数:
在开发过程中,我们会经常遇到需要组件之间相互进行通信:
比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示
; 又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给他们来进行展示;
也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;
总之,在一个React项目中,组件之间的通信是非常重要的环节; 父组件在展示子组件,可能会传递一些数据给子组件:
父组件通过 属性=值 的形式来传递给子组件数据;
子组件通过 props 参数获取父组件传递过来的数据;
一、父传子
在jsx中,直接在组件占位符上写即可,和vue类似;
react:
传递字符串title='title'
传递变量banner={varData}
vue:
传递字符串title='title'
传递变量 v-bind:banner=‘varData’
render() {
const { banner } = this.state;
return (
<div>
<BodyBanner title='title' banner={banner} />
</div>
);
}
或者使用展开运算符... (注意展开运算符和解构的区别,别搞混了)
render() {
const info = {
color:red,
size:30
}
return (
<div>
<BodyBanner {...info} />
</div>
);
}
子组件接收数据:
1.在construtor里用super(props)然后这个props就绑定到实例上了
2.直接在render函数就可以通过实例的方式引用了。
export class BodyBanner extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { banner } = this.props;
return (
<div>
<ul>
{banner.map(e => (
<li>{e}</li>
))}
{banner.reduce((pre, cur) => {
pre.push(<li>{cur}</li>);
return pre;
}, [])}
{banner.reduce((pre, cur) => pre.push(<li>{cur}</li>) && pre, [])}
</ul>
</div>
);
}
}
二、props验证
就像vue一样,可以对props的类型做验证,还有给props一个默认值。
export default {
props:{
banner:{
type:Array,
default:()=>[]
}
}
}
那么react怎么做?
当然,如果你项目中默认继承了Flow或者TypeScript,那么直接就可以进行类型验证; 但是,即使我们没有使用Flow或者TypeScript,也可以通过 prop-types 库来进行参数验证
参考官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html
- 安装:npm install prop-types
- 导入:import PropTypes from 'prop-types';
- 验证:
-
默认值:
import React, { Component } from 'react'; import PropTypes from 'prop-types'; export class BodyBanner extends Component { constructor(props) { ... } render() { ... } } BodyBanner.propTypes = { banner: PropTypes.array }; BodyBanner.defaultProps = { banner: ['默认', '默认222', '默认333'] }; export default BodyBanner;
三、子传父通信
这里不需要用vue的$emit('eventName',)和@eventName语法。
react直接用js的回调函数解决。
父组件给子组件传递一个props,值为一个回调函数。
子组件直接通过this.props.onChangeCount()传递参数par,即可实现子组件给父组件传递数据。
1 import React, { Component } from 'react';
2 import PropTypes from 'prop-types';
3
4 export class BodyBanner extends Component {
5
6 render() {
7 return (
8 <div>
9 <button onClick={() => this.props.onChangeCount(1)}>点击加1</button>
10 </div>
11 );
12 }
13 }
14 BodyBanner.propTypes = {
15 onChangeCount: PropTypes.func
16 };
17
18 BodyBanner.defaultProps = {
19 onChangeCount: () => {}
20 };
21
22 export default BodyBanner;
四、插槽
React对于这种需要插槽的情况非常灵活,有两种方案可以实现:
- 组件的children子元素;
- props属性传递React元素;
方式一:通过this.props.children
父组件直接在react元素里写元素即可,它会在通过babel编译在React.createElement('NavBar‘,{},[ children])的children里,
所以子元素直接通过this.propos.children取即可。
通过children实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;
因为传递2个及以上时,this.propos.children是一个数组,但如果只传递一个元素,那么this.propos.children就变成了本元素,这种不一样的情况实在是麻烦。
方式二:直接通过props传递
这个jsx的props实在是太牛了,使用{}语法后,除了传递基本的字符串、数字、数组、对象、函数,还可以传递react元素。
直接传递给子组件即可,而且是类似vue的具名插槽,
1 render() {
2 return (
3 <div>
4 <div className="head-container">
5 <BodyHead
6 leftSlot={<button>新款</button>}
7 centerSlot={<p>流行</p>}
8 rightSlot={<i>精选</i>}
9 />
10 </div>
11 </div>
12 );
13 }
子组件直接接收使用;
1 render() {
2 const { leftSlot, centerSlot, rightSlot } = this.props;
3 return (
4 <div>
5 <p>我是子组件,下面是插槽,用来接收父组件传过来的内容</p>
6 <div className="lfet">{leftSlot}</div>
7 <div className="center">{centerSlot}</div>
8 <div className="right">{rightSlot}</div>
9 </div>
10 );
11 }
五、作用域插槽
一般的,插槽的内容、数据、样式都是由父组件写好传递过来,子组件只负责展示即可;
但是作用域插槽就是,父组件决定子组件某一个块内容用什么标签元素展示,但是数据仍然由子组件提供;
插槽的数据作用域仍然是子组件。
或者说,这一块插槽的内容,是由父子组件共同决定。
其实也简单,和子穿父通信一样,仍然是用回调函数。
父组件:
render() {
return (
<div>
<div className="head-container">
<BodyHead
leftSlot={(par)=><button>{par}</button>}
centerSlot={(par)=><p>{par}</p>}
rightSlot={(par)=><i>{par}</i>}
/>
</div>
</div>
);
}
子组件:
render() {
const { leftSlot, centerSlot, rightSlot } = this.props;
return (
<div>
<p>我是子组件,下面是插槽,用来接收父组件传过来的内容</p>
<div className="lfet">{leftSlot('星际穿越')}</div>
<div className="center">{centerSlot('功夫')}</div>
<div className="right">{rightSlot('让子弹飞')}</div>
</div>
);
}
说白了,就是让父组件从直接传递一个元素,变成了一传递一个返回值为react元素的函数,
然后子组件从直接展示元素,变成了传参调用回调函数。
六、context全局上下文
这个主要是爷孙组件传递信息,类似vue的provide和inject。
当然了,有redux尽量不要用这个context,太麻烦了。
1.单独找一个文件,建立全局context
2.爷爷组件引入这个context,并且包裹起来给父组件
<ThemeContext.Provider value={{color: "red", size: "30"}}> <Home/> </ThemeContext.Provider>
3.孙子组件有两种方式获取爷爷组件的数据
方式一:
先导入全局context,然后将类实例的contextType类型指定为全局context,最后通过this.context引入。
方式二:先导入类型,然后用标签引入,然后再标签里传递一个回调函数,回调函数的参数就是爷爷传递过来的数据。