组件生命周期
React 是如何渲染组件的?
一般来讲,我们都会先定义一个组件,我们想要在页面当中看到这个组件渲染的结果的话,需要用 JSX 的形式把组件传入到 ReactDOM.render 方法中作为第一个参数。JSX 通过 React 转化其实是一个 react.createElement 的方法。render 方法获取到 react 元素中会将它实例化,根据 React 实例的元素创建出真实DOM。再更具第二个参数的指向,讲创建好的元素插入到目标DOM容器当中。
新版本的React当中,React的底层被重写,换上新的引擎,React Fiber。它将 React 更新应用界面分成了 2 个主要的部分调度过程与执行过程。
在调度过程中,有四个生命周期会被触发:
- componentWillMount
- conponentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
在执行过程中,有三个生命周期会被触发:
- componentDidMount
- componentDidUpdate
- componentWillUnmount
React 为了方便我们控制自己的引用提供了许多预知的生命周期方法,就想以上的生命周期方法,会在组件挂载流程、更新流程、卸载流程中触发。React初次渲染时,属于挂载流程,后续更新时的都是更新流程,最后我们还可以通过 ReactDOM.unmountComponentAtNode 将组件卸载,其中进行的就是卸载流程。
举个栗子 。这里我们定义2个组件,父组件会传递 state 到子组件当中,顺便 state 的变化和 props 的传递流程我们也可以从这个 中看到。子组件每一个生命周期函数中,都在控制台输出相关的信息。
import React from 'react'
import ReactDOM from 'react-dom'
class Number extends React.Component {
constructor(props) {
super(props)
console.log('constructor 子组件已构造')
}
componentWillMount() {
console.group("React 挂载")
console.log('componentWillMount 组件即将挂载')
}
componentDidMount() {
console.log('componentDidMount 组件已挂载')
console.groupEnd();
}
componentWillReceiveProps(newProps) {
console.group("React 更新")
console.log('componentWillReceiveProps 组件即将接收props')
console.log('newProps', newProps.counter)
console.log('this.props', this.props.counter)
}
shouldComponentUpdate(newProps, newState) {
const result = true
console.info('shouldComponentUpdate 返回判断是否要更新组件')
if (!result) console.groupEnd()
return result;
}
componentWillUpdate(nextProps, nextState) {
console.log('componentWillUpdate 组件即将更新')
console.log('nextProps', nextProps.counter)
console.log('this.props', this.props.counter)
}
componentDidUpdate(prevProps, prevState) {
console.log('componentDidUpdate 组件已更新')
console.log('prevProps', prevProps.counter)
console.log('this.props', this.props.counter)
console.groupEnd();
}
componentWillUnmount() {
console.group("React 卸载")
console.log('componentWillUnmount 组件即将卸载')
console.groupEnd();
}
render() {
console.log('组件渲染中...')
console.log('this.props', this.props.counter)
return <p>{ this.props.counter }</p>
}
}
class Counter extends React.Component {
constructor(props) {
super(props)
console.log('constructor 父组件已构造')
this.state = {
counter: 0
}
}
componentWillMount() {
console.log('this.state', this.state.counter)
}
shouldComponentUpdate(newProps, newState) {
const result = true
console.info('shouldComponentUpdate 返回判断是否要更新父组件')
return result;
}
addOne() {
console.log('addOne() 事件处理函数触发')
console.log('prevState', this.state.counter)
this.setState((prevState) => ({
counter: prevState.counter + 1
}))
}
unMount() {
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}
update() {
this.forceUpdate()
}
render() {
console.log('render 父组件渲染中...')
console.log('nextState', this.state.counter)
return (
<div>
<Number counter={this.state.counter} />
<button onClick={() => this.addOne()}>增加</button>
<button onClick={() => this.update()}>强制更新</button>
<button onClick={() => this.unMount()}>卸载</button>
</div>
)
}
}
ReactDOM.render(
<Counter />,
document.getElementById('root')
)
首先是组件初次渲染的挂载流程,我们通过一个按钮,绑定一个渲染函数来主动触发组件的渲染。初次挂载组件时,我们构建的组件类会被挂载声明,在渲染之前会触发
componentWillMount
,在渲染之后会触发componentDidMount
这2个生命周期函数。componentWillMount
会在渲染之前被触发,并且只会被组件被渲染之前触发一次。如果使用ES6的class声明组件的话,时机和作用几乎和constructor
相同。在componentWillMount
对state的定义或者改变,是不会触发新的渲染的。componentDidMount
会在渲染完成之后被触发,同样自会触发一次。在这个方法中,已经可以访问到渲染到页面的DOM元素了,所以官方推荐在这个方法中可以进行一些类似于AJAX请求的操作,是我们最经常使用的生命周期函数。父组件会向子组件更新props的数值,这时候会触发
componentWillReceiveProps
。组件在运行这一流程,props还没有被改变。然后就会就会触发shouldComponentUpdate
,默认返回true,如果返回false的话,更新流程会被跳过,渲染也不会继续被触发。假如要提升应用的性能,可以在这个函数中进行自定义的判断来跳过不需要被更新的组件。组件在初次渲染时,和forceUpdate时,是不会触发这个函数的。componentWillUpdate
和componentDidUpdate
会在更新渲染的前后触发。在componentWillUpdate
里无法调用this.setState,可以理解为更新流程到这一步想要更改state已经晚了,如果有需要可以在componentWillReceiveProps
中更新state,React会把改变合并到同一个更新流程里面执行。而componentDidUpdate
是另一个适合我们发起AJAX请求的地方,在这里我们还可以比较前后的props变化,再决定是否发起网络请求。比较实用的场景就是保存用户的输入到服务器,用户可能会来来回回修改内容,如果我们判断修改前后数据最终没有被改变,那就没有必要发起不必要的网络请求了。通过
ReactDOM.unmountComponentAtNode
来卸载已经挂载了的React组件。在卸载流程的componentWillUnmount
是我们解绑事件最合适的位置。
>> constructor 父组件已构造
>> this.state 0
>> render 父组件渲染中...
>> nextState 0
>>constructor 子组件已构造
>>componentWillMount 组件即将挂载
>>render 组件渲染中...
>>this.props 0
>>componentDidMount 组件已挂载
>>addOne 事件处理函数触发
>>prevState 0
>>shouldComponentUpdate 返回判断是否要更新父组件
>>render 父组件渲染中...
>>nextState 1
>>componentWillReceiveProps 组件即将接受Props
>>nextProps 1
>>this.props 0
>>shouldComponentUpdate 返回判断是否要更新组件
>>componentWillUpdate 组件即将更新
>>nextProps 1
>>this.props 0
>>render 组件渲染中...
>>this.props 1
>>componentDidUpdate 组件已更新
>>preProps 0
>>this.props 1
>> componentWillUnmount 组件即将卸载
例子:React异步获取文章
通过 const 的关键字声明,使用 arrowFunction 定义一个简单的 RedditList 展示组件。一般组件包含多层嵌套的JSX的时候,都会加上一个小括号,因为浏览器在某些情况下可能会对换行的代码加上分号。 JSX当中时可以使用js表达式的,一般在渲染列表中都会拿到一个数组数据,通过map的方法渲染出列表中的每一项。JSX在渲染列表项的时候,每一项都必须带有key属性,作为识别列表中某一项的标识,React在渲染列表的时候会通过key来判断每一个的内容。
使用类定义的方法来定义一个 RedditFetch 容器组件。首先把posts初始化为数组,定义一个从服务器获取数据的方法 fetchFromApi,在请求成功之后重新设置 state,获取到帖子的数据。在 componentDidMount 中调取 fetchFromApi,在 componentWillUnmount 中解除向服务器发起的请求。
const RedditList = props => (
<div>
<h1>{`/r/${props.subreddit}`}</h1>
<ul>
{props.posts.map(post =>
<li key={post.id}>
<a href={post.url}>{post.title}</a>
</li>
)}
</ul>
</div>
)
class RedditFetch extends React.Component {
constructor(props) {
super(props)
this.state = {
posts: []
}
}
fetchFromApi() {
this.serverRequest = axios.get(`https://www.reddit.com/r/${this.props.subreddit}.json`)
.then(res => {
const posts = res.data.data.children.map(obj => obj.data)
this.setState({
posts
})
})
}
componentDidMount() {
this.fetchFromApi()
}
componentWillUnmount() {
this.serverRequest.abort()
}
render() {
return <RedditList subreddit={this.props.subreddit} posts={this.state.posts} />
}
}
ReactDOM.render(<RedditFetch subreddit="reactjs" />,document.getElementById('root'))
小结
- React 组件渲染包括三个流程:挂载流程、更新流程、卸载流程。
- 各个生命周期函数会在特定的时候触发并适用于不同的使用场景。
- 通过使用生命周期函数我们可以对应用进行更精准的控制。
- 如果你需要发起网络请求,将其安排在适合的生命函数周期中。