组件的生命周期是指组件从被创建到挂载到页面中,再到组件卸载的过程。在React中,只有类组件才有生命周期,函数组件是不存在生命周期的。组件生命周期大体分为三个阶段:创建阶段、更新阶段、卸载阶段。在组件生命周期的每一个阶段,React 都给我们提供了一些方法,让我们可以在对应阶段执行某些任务,这些方法就是生命周期的钩子函数。
生命周期的创建阶段常用的钩子函数有三个,执行顺序为:constructor() --> render() --> componentDidMount
:
钩子函数 | 触发时机 | 作用 |
---|---|---|
constructor() | 创建组件时,最先执行 | ① 初始化state ② 为事件处理程序绑定this |
render() | 每次组件渲染都会触发 | 渲染页面UI(可以访问 props 和 state,但是不能在该函数中,调用setState() 方法) |
componentDidMount() | 每次组件挂载(完成DOM渲染)后 | 可以在此阶段发送网络请求和进行DOM操作 |
之所以不能在render()方法内调用 setState()
方法,是因为 setState()
方法会改变组件的状态(数据),然后引起页面重新渲染,再次触发 render(),再次调用 setState()
方法…形成死循环。
生命周期的更新阶段常用的钩子函数有两个,执行顺序为:render() --> componentDidUpdate()
。当组件调用 setState() / forceUpdate() / 接收新的props,这三种情况任意一种,就会触发组件的更新,重新渲染,进入更新阶段。
钩子函数 | 触发时机 | 作用 |
---|---|---|
render() | 每次组件渲染都会触发 | 渲染页面UI(可以访问 props 和 state,但是不能在该函数中,调用setState() 方法) |
componentDidUpdate() | 组件更新(完成DOM渲染) 后 | 可以在此阶段发送网络请求和进行DOM操作(如果调用setState() 必须放到一个if语句中) |
在 componentDidUpdate()
阶段调用 setState() 要加if条件判断也是为了避免死循环的出现。
生命周期的卸载阶段常用的钩子函数只有一个:componentWillUnmount()
:
钩子函数 | 触发时机 | 作用 |
---|---|---|
componentWillUnmount() | 当组件卸载,从页面消失时触发 | 执行清理工作(定时器、绑定的事件等) |
如果两个组件的主要部分的功能相似或相同,为了减少代码量,我们可以考虑组件复用,类似于函数封装,将相同的功能模块,封装到一个组件中去。在React中组件复用,复用的是组件的 state 和 操作state 的方法。
React 中实现组件复用的方式有两种:① render props 模式 ② 高阶组件(HOC) 。这两种方式并不是React提供的API,而是利用React的语法特点,利用技巧形成的固定写法。
该模式的思路是将想要复用的state和操作state的方法封装到一个组件中,然后在使用组件时,给组件标签添加一个回调函数,该函数的参数就是组件内部向外传递的复用数据。在组件标签中,将该函数的返回值设置为要渲染的页面UI样式,然后在组件内部的render() 方法中,通调用该函数,获取想要渲染的页面UI样式。
① 创建组件,并在组件中写好要复用的状态逻辑代码(state 和 操作state 的方法)
② 假设回调函数名为renderProps,将要复用的状态(state) 作为 props.renderProps() 方法的参数,暴露到组件外部。
③ 在组件标签中,通过renderProps()方法的参数获取想要的数据,并通过返回值设置想要渲染的页面UI结构。
④ 在组件中 把props.renderProps() 方法的返回值,作为要渲染的页面UI结构,放到 render() 方法的return中。
// 封装的复用组件 Mouse
export default class Mouse extends Component {
// 想要复用的状态
state = {
x: 0,
y: 0
}
// 在组件挂载时绑定事件
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
// 在组件卸载时解绑事件
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
// 状态操作程序
handleMouseMove = e => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
render() {
const { x, y } = this.state
return (
{this.props.renderProps({ x, y })}
)
}
}
// 使用复用组件
import Mouse from './Mouse'
const Position = () => (
(
鼠标当前位置:(x: {x}, y: {y})
) }>
)
render props 模式除了可以通过属性回调函数的方式实现,还可以通过chidren 来实现,而且我更推荐这种实现方式:
// 封装的复用组件 Mouse
export default class Mouse extends Component {
// 省略相同代码
render() {
const { x, y } = this.state
return (
{/* 推荐使用 children */}
{this.props.children({ x, y })}
)
}
}
// 使用复用组件
import Mouse from './Mouse'
const Position = () => (
{({ x, y }) => (
鼠标当前位置:(x: {x}, y: {y})
)}
)
为了确保用户能够正确使用封装的复用组件,我们还可以给复用组件加上 props 校验:
// 用户必须传递children 并必须是函数
Mouse.protoTypes = {
children: PropTypes.func.isRequired
}
高阶组件(HOC,Higher-Order Component) 是实现组件复用的另一种方式,高阶组件本身就是一个函数,通过参数接收要包装的组件,返回增强后的组件。高阶组件内部会创建一个类组件,在这个类组件内提供要复用的状态和操作状态的方法,在render() 里面将传过来的参数组件,作为要渲染的页面UI结构。并通过prop将要复用的状态传递给参数组件。
① 创建一个高阶组件函数,一般约定名称以 with 开头,例如:withMouse() {}。
② 指定函数的参数,参数表示要渲染的组件,该组件决定要渲染的页面UI结构,参数名称要首字母大写,因为后面要作为组件渲染。
③ 在高阶组件函数内部创建一个类组件,提供要服用的状态逻辑代码,并return 这个类组件。
④ 在上一步创建的类组件的render()中,渲染参数组件,并将状态state通过prop传递进参数组件内。
⑤ 调用该高阶组件,传入要增强的组件,通过返回值获得增强后的组件,并将其渲染到页面中。
// 创建高阶组件
function withMouse(WrappedComponent) {
// 该组件提供复用的状态逻辑
class Mouse extends React.Component {
// 鼠标状态
state = {
x: 0,
y: 0
}
// 操作状态的方法
handleMouseMove = e => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
// 绑定事件 控制鼠标状态的逻辑
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
// 在组件卸载时解绑事件
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
// 渲染参数组件 并将状态传入组件内
render() {
return
}
}
// 返回创建的类组件
return Mouse
}
// 要增强的组件
const Position = props => (
鼠标当前位置:(x: {props.x}, y: {props.y})
)
// 获取增强后的组件:
const MousePosition = withMouse(Position)
class App extends React.Component {
render() {
return (
高阶组件
{/* 渲染增强后的组件 */}
)
}
}
ReactDOM.render( , document.getElementById('root'))
如果用同一个高阶组件处理多个组件,会出现这多个组件在React的调试工具中的名字(displayName)是一样的,都是高阶组件中创建的那个类组件名字的情况。这是因为在默认情况下,React会使用这个类组件的名称作为 displayName 。为了方便我们调试和区分,我们可以在高阶组件中,通过参数组件来设置这个displayName:
// 在高阶组件内部
// 调用方法设置displayName
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
// 高阶组件外面 定义好处理方法
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
在使用高阶组件增强后的组件时,通过prop向组件内部传入一个参数,但我们却无法在要增强的组件中访问到这个参数,因为在高阶组件中只向下传递了本身的状态,并没有向下传递props中的参数。所以我们需要在高阶组件中将props也向下传递:
// 修改上面案例中高阶组件中类组件的render()
render() {
return
}