无论是 vue、React 还是 Angular,主流框架都支持并提倡组件化开发,因为组件化开发不仅可以增强代码的能动性和复用性,还能够加快团队协作的速度。组件化开发就像搭积木,首先把一个个积木(组件)设计好,甚至将小积木(容器组件、展示组件)组装成具备一定功能的积木(比如一个房子),最终再将功能化的积木摞成最终的成品(比如一个社区)。
本文简单介绍 React 中组件的定义,以及容器组件、展示组件、高阶组件、复合组件等常见组件的应用,并介绍组件间的通信方式。
1. 如何定义一个组件
1.1 一般组件
React 中组件的定义有两种方式,一种是使用 Class
关键字以类的形式来定义组件,另一种是使用函数方式定义。比如定义一个网站的欢迎提示组件:
- 类定义
class WelcomeTip extends React.Component {
render() {
return (
<div>
Welcome to this website!
</div>
)
}
}
- 函数定义
function WelcomeTip(props) {
return (
<div>
Welcome to this website!
</div>
)
}
无论使用哪一种方式定义组件,组件的调用都是一致的
<WelcomeTip></WelcomeTip>
但是,组件内状态管理、生命周期却有着很大的不同,本文中主要采用类定义的方式来构建组件,关于函数定义组件的应用可以移步 “React Hook” 的介绍。
- 组件状态
class Counter extends React.Component {
// 写了 constructor 就要调用 super
constructor(props) {
super(props)
// 状态声明
this.state = {
count: 0
}
}
// state 的调用:this.state.xxx
// state 的修改:this.setState({count: 1})
// 或者 this.setState(state => ({count: 1}))
// 支持同时设置多个 key 值,key 值相同时后者覆盖前者
// setState 是一个异步函数
render() {
return (
<div>
<p>Welcome, {this.props.name}! You have click {this.state.count} times!</p>
<button
onClick={() => this.setState(state => {count: state.count + 1})}
>Click</button>
</div>
)
}
}
- 组件的生命周期
- 初始化:
constructor
,用于完成组件的初始化工作,如定义state
的初始内容、定义组件内部变量等 - 组件的挂载:
-
componentWillMount
,发生在组件挂载到 DOM 之前,此处修改state
不会引起组件的重新渲染。该部分的功能也可以提前到constructor
中,因此很少在项目中使用。 -
render
,根据组件的props
和state
(两者的重传递和重赋值,无论值是否有变化,都可以引起组件重新 render),返回⼀个 React 元素(描述组件,即UI),不负责组件实际渲染⼯作,之后由 React ⾃身根据此元素去渲染出⻚⾯DOM。纯函数,返回结果只依赖于传入的参数,执行过程中没有副作用。不能在该阶段执行setState
,会造成死循环。 -
componentDidMount
,组件挂载到 DOM 之后调用,且只会被调用一次。
-
- 组件的更新:当
props
或state
被重新赋值时,无论值是否发生改变,都会触发组件的更新。因此有如下两种情况会触发组件的更新:1. 父组件重新 render,由于子组件的props
被传值,触发子组件的更新;2. 组件本身调用setState
,无论state
有没有改变,组件都会更新-
componentWillReceiveProps(nextProps)
,props
重传时被调用,该函数中调用setState
不会引起组件的二次更新,因此即便在该函数中执行this.setState
更新了state
,shouldComponentUpdate
componentWillUpdate
中的this.state
依旧是原来的值。 -
shouldComponentUpdate(nextProps, nextState)
,此⽅法通过⽐较nextProps
,nextState
及当前组件的this.props
,this.state
,返回true
时当前组件将继续执⾏更新过程,返回false
则当前组件更新停⽌,以此可⽤来减少组件的不必要渲染,优化组件性能。 -
componentWillUpdate(nextProps, nextState)
,此⽅法在调⽤render
⽅法前执⾏,在这边可执⾏⼀些组件更新发⽣前的⼯作,⼀般较少⽤。 -
render
:同挂载时的 render。 -
componentDidUpdate(prevProps, prevState)
,此⽅法在组件更新后被调⽤,可以操作组件更新的 DOM ,prevProps
和prevState
这两个参数指的是组件更新前的props
和state
。
-
- 组件的卸载:
-
componentWillUnmount
:此⽅法在组件被卸载前调⽤,可以在这⾥执⾏⼀些清理⼯作,⽐如清除组件中使⽤的定时器,componentDidMount
中⼿动创建的 DOM 元素等,以避免引起内存泄漏。
-
- 【注意】
componentWillMount
componentWillReceiveProps
和componentWillUpdate
在 React 17.x 版本之后将不再支持,目前使用会提示 warning。在 16.3 之后,使用getDerivedStateFromProps
代替上述三个函数-
static getDerivedStateFromProps(props, state)
,在组件创建时和更新时的 render ⽅法之前调⽤, 它应该返回⼀个对象来更新状态,或者返回 null 来不更新任何内容。 -
getSnapshotBeforeUpdate
,被调⽤于render之后,此时可以读取但还不能操作更新 DOM ,因此可以按需调整滚动条等。 返回值(必须有)将作为参数传递给componentDidUpdate
。
-
- 初始化:
1.2 组件拆分——容器组件&展示组件
在涉及复杂的数据预处理时,可以考虑将组件拆分成容器组件和展示组件。其中容器组件负责请求并处理数据,展示组件负责根据 Props 显示信息。如此可以减小组件的体积,使开发人员可以跟专注于某一功能开发,并提高组件的重用性和可用性,同时易于测试和提高系统性能。
// 容器组件
class CommentList extends React.Component {
state = {
list: []
}
componentDidMount() {
setTimeout(() => {
this.setState({
list: [
{id: 1, text: '我喜欢苹果', author: '小A'},
{id: 2, text: '我喜欢橙子', author: '小B'},
{id: 3, text: '我喜欢西瓜', author: '小C'},
]
})
})
}
render() {
return (
<div>
{this.state.list.map(l => {
return <Item key={l.id} text={l.text} author={l.author}/>
})}
</div>
)
}
}
// 展示组件
function Item({text, author}) {
return (<div>
{text} -- <span style={{color: 'blue'}}>{author}</span>
</div>)
}
1.3 PureComponent
在组件生命周期中组件更新过程中,提及只要发生重新挂载,无论 props
state
是否变化,都会出发更新。纯组件就是定制了 shouldComponentUpdate
后的Component,仅有依赖的数据发生变化时才进行更新。 该比较过程数据浅比较,因此对象属性或数组中元素并不适用于该特性。
// 假设父组件有 count 和 name 两个状态
// 子组件仅依赖父组件的 count
// 如果子组件继承的是 React.Component,那么父组件 name 值发生变更时,子组件依旧会重新 render
// 继承的是 React.PureComponent 时,则仅有父组件的 count 值变化时,子组件才会重新调用 render
class Child extends React.PureComponent {
render() {
return <div>{this.props.count}</div>
}
}
React 16.6.0 之后,使用 React.memo
让函数式的组件也有 PureComponent 的功能
const Child = React.memo(() => {
return <div>{this.props.count}</div>
})
2. 高阶组件是什么
2.1 高阶组件与一般组件有什么不同
高阶组件是 React 中重用组件逻辑的高级技术,它不是 React 的 api ,而是一种组件增强模式。高阶组件是一个函数,它返回另外一个组件,产生新的组件可以对被包装组件属性进行包装,也可以重写部分生命周期。
高阶组件可以为组件添加某一特殊功能,也可以多层嵌套,赋予被包装组件多个功能。比如打印日志功能、添加标题功能等。
// 包装后的组件具备日志打印功能
const withLog = Component => {
class newComponent extends React.Component {
componentDidMount() {
console.log(`${Date.now()}:组件已挂载`)
}
render() {
return <Component {...this.props} />
}
}
return newComponent
}
// 包装后的组件都带有一个标题
const withTitle = Component => {
const newComponent = props => {
return (<Fragment>
<h3>这是一个标题</h3>
<hr />
<Component {...props} />
</Fragment>)
}
return newComponent
}
2.2 高阶组件怎么使用
- 链式调用
高阶组件本质上就是一个函数,因此可以采用链式调用的形式,将待包装的组件作为参数传入,并 export 出去即可。同时也可以多个高阶组件嵌套,一层层包装单一组件。
export default withLog(withTitle(CommentList))
- 装饰者模式
ES7 中提供了装饰者模式的写法,可以使代码更加简洁,但需要进行相关配置:
暴露项目的所有配置项:
npm run eject
安装:
npm install -D @babel/plugin-proposal-decorators
-
配置
package.json
文件中 babel 配置项"babel": { "presets": [ "react-app" ], "plugins": [ ["@babel/plugin-proposal-decorators", {"legacy": true}] ] }
如此,上述链式调用可以修改为:
export default
@withLog
@withTitle
class CommentList extends React.Component {
...
}
3. 复合组件
复合组件可以让开发者以更便捷地创建组件的外观和行为,相比继承更加直观和安全。
// 容器不关心内容与逻辑
// 3. 容器中可以使用 children,但由于传入的是 vdom 数组,故而不能修改
function Dialog(props) {
return (<div style={{border: `1px solid ${props.color || '#ccc'}`}}>
{React.Children.map(props.children, child => child.type === 'p' ? child : null)}
{props.footer}
</div>)
}
// 通过复合提供内容
function HelloDialog(props) {
// 1. 参数可以使用 props 传入
// 2. 可以传入任何表达式
return (<Dialog color='blue' footer={<p>版权归 road 所有</p>}>
<h3>你好啊,{props.name}</h3>
<p>感谢访问本网站</p>
</Dialog>)
}
4. 组件间如何实现通信
4.1 父传子
通过 props 将参数传递给子组件,使用 class
关键字以类方式定义组件时,使用 this.props
即可以父组件传递的所有参数,函数方式定义时则需要在声明时添加 props 参数,或解构参数。
// 类方式定义
class Child extends React.Component {
render() {
return (<div>
子组件:{this.props.name}
</div>)
}
}
// 函数方式定义
function Child(props) {
return (<div>
子组件:{props.name}
</div>)
}
// 函数方式
function Child({name}) {
return (<div>
子组件:{name}
</div>)
}
父组件传参:
<Child name='road'></Child>
4.2 子传父
父组件中声明一个相关方法,并作为参数传递给子组件。子组件通过调用父组件传递过来的方法,修改父组件中的数据。
// 比如:父组件中有个计数值,子组件中的按钮点击之后计数值 +1
function Child({increase, step}) {
return (
<div>
<button onClick={() => increase(step)}>+{step}</button>
</div>
);
}
export class Parent extends Component {
state = {
count: 0
}
add(step) {
this.setState(state => ({count: state.count + step}))
}
render() {
return (
<div>
计数值为 {this.state.count}
{/* 注意方法传递过程中 this 的指向变更 */}
<Child increase={this.add.bind(this)} step={1}></Child>
<Child increase={this.add.bind(this)} step={2}></Child>
</div>
);
}
}
4.3 跨组件通信
跨组件通信有兄弟组件通信、父组件与孙组件的通信等,从上到下的数据传递可以通过 props 一层层传递,但从下到上的数据传递则十分麻烦。例如下图中【子组件1】相与【父组件B】通信时,就需要将信息一层层冒到祖先组件中,再通过祖先组件派发给【父组件B】。
因此如果项目较为庞大时,可以引入 redux 进行全局状态管理(可参考 redux 使用实例)。当项目量级较小时,则使用 React 中的 Context 来进行公共状态的管理,该模式包括两个角色:
Provider:外层提供数据的组件,内部组件都可以访问到来自 provider 的数据
Consumer :内层获取数据的组件,沿上追溯到最近的 provider,消费其数据。接收一个函数作为子节点,返回 react 节点。
function Display(props) {
// 6. props 重新赋值,组件更新
return (
<div>
<h2>{props.title}</h2>
<p>你的名字是:{props.name}</p>
<p>你的邮箱是:{props.email}</p>
</div>
)
}
class FormItem extends Component {
state = {
val: ''
}
render() {
const {keyName, label, type} = this.props
// 3. consumer 内部接收一个函数,参数 value 来源于最近的 provider
return (<SurveyContext.Consumer>
{(value, _this) => {
return (<div>
<label htmlFor={keyName}>{label}</label>
<input
id = {keyName}
type = {type}
placeholder={value[keyName]}
onChange = {e => {this.setState({val: e.target.value})}}
onKeyDown = {e => {
if( 13 === e.keyCode ) {
// 4. 调用操作方法,也即 Survey 组件中的 changeState 方法,修改 provider 中的数据
value.change(keyName, this.state.val)
}
}}
/>
</div>)
}}
</SurveyContext.Consumer>)
}
}
// 2. 中间组件不需要传递数据和方法
class Form extends Component {
render() {
return (
<div>
<FormItem keyName='name' label='名字' type='text'/>
<FormItem keyName='email' label='邮箱' type='text'/>
</div>
);
}
}
const SurveyContext = React.createContext()
export default class Survey extends Component {
state = {
name: 'abc',
email: '123@163.com'
}
changeState(key, val) {
this.setState({[key]: val})
}
// 5. setState 方法触发组件更新,重新 render
render() {
return (
<div>
{/* 1. provider 提供 value 给 consumer,可以将修改 state 的方法也作为 value 对象的方法传递*/}
<SurveyContext.Provider
value={{
...this.state,
change: this.changeState.bind(this)
}}
>
<Form></Form>
</SurveyContext.Provider>
<hr />
<Display title='问卷调查' name={this.state.name} email={this.state.email}></Display>
</div>
)
}
}
效果: