什么是生命周期?
在开发微信小程序中,常见的生命周期方法有onLaunch,onLoad,onShow等。生命周期函数就是让我们在一个组件的各个阶段都提供一些钩子函数来让开发者在合适的时间点可以介入并进行一些操作,比如加载时(onLoad)我们应该初始化组件相关的状态和变量。
React的生命周期
总的来说React组件的生命周期分为三个部分: 装载期间(Mounting) ,更新期间(Updating) 和卸载期间(Unmounting) ,React16新增了一个componentDidCatch() 用于捕捉错误。这些生命周期函数有一定的规律,比如在某件事情发生之前调用的会用xxxWillxxx,而在此之后发生的会用xxxDidxxx。
图1 React16生命周期
装载期间
组件被实例化并挂载在到DOM树这一过程称为装载,在装载期调用的生命周期函数依次为
- constructor()
- getDerivedStateFromProps()
- render()
- componentDidMount()
constructor(props)
构造函数,用于初始化这个组件的一些状态和操作,如果通过继承React.Component
子类来创建React的组件的,那么应当首先调用super(props)
来初始化父类。
在contructor函数中,可以初始化state,不要在构造函数中使用setState()函数,强行使用的话React会报错。
示例代码:
this.state = {
defaultValue1: 1,
defaultValue2: 2,
...
}
其次可以在构造函数中进行函数bind:
示例代码:
this.handleClick = this.handleClick.bind(this)
contructor的实现,示例代码:
constructor(props) {
super(props)
this.state = { color: '#fff' }
this.handleClick = this.handleClick.bind(this)
}
当不需要初始化状态也不需要绑定handle函数的this时,可以不实现constructor函数,由默认实现代替。
关于bind函数
js的this指向比较特殊,比如下例当button组件调用onClick的时候不会把组件类的上下文带过去。
handleClick() {
console.log('handleClick', this) // this的值为undefined
}
这种问题常用的三种解决方式,其核心均为将函数的this强制绑定到组件类上:
- 如上所示,在constructor函数中显示调用bind。
- 在onClick的时候进行bind:这种方式的劣势是每次调用的时候都需要进行bind,优势是方便传参,处理函数需要传参可以参考React的文档 Passing Arguments to Event Handlers。
- 声明函数时使用箭头匿名函数,箭头函数会自动设置this为当前类(简洁有效,通常使用这种方法)。
handleClick = () => {
console.log('handleClick', this) // Component
}
getDerivedStateFromProps()
这个函数会在render函数被调用之前调用,包括第一次的初始化组件以及后续的更新过程中,每次接收新的props之后都会返回一个对象作为新的state,返回null则说明不需要更新state。
该方法主要用来替代componentWillReceiveProps方法,willReceiveProps经常被误用,导致了一些问题,因此在新版本中被标记为unsafe。componentWillReceiveProps的常见用法如下,根据传进来的属性值判断是否要load新的数据。
示例代码:
class ExampleComponent extends React.Component {
state = {
isScrollingDown: false
}
componentWillReceiveProps(nextProps) {
if (this.props.currentRow !== nextProps.currentRow) {
// 检测到变化后更新状态、并请求数据
this.setState({
isScrollingDown: nextProps.currentRow > this.props.currentRow
})
this.loadAsyncData()
}
}
loadAsyncData() {
/* ... */
}
}
但这个方法的一个问题是外部组件多次频繁更新传入多次不同的 props,而该组件将这些更新 batch 后仅仅触发单次自己的更新,这种写法会导致不必要的异步请求,相比下来getDerivedStateFromProps配合componentDidUpdate的写法如下。
示例代码:
class ExampleComponent extends React.Component {
state = {
isScrollingDown: false, lastRow: null
}
static getDerivedStateFromProps(nextProps, prevState) {
// 不再提供 prevProps 的获取方式
if (nextProps.currentRow !== prevState.lastRow) {
return {
isScrollingDown: nextProps.currentRow > prevState.lastRow, lastRow: nextProps.currentRow
}
}
// 默认不改动 state return null
}
componentDidUpdate() {
// 仅在更新触发后请求数据
this.loadAsyncData()
}
loadAsyncData() {
/* ... */
}
}
这种方式只在更新触发后请求数据,相比下来更节省资源。
注意getDerivedStateFromProps是一个static方法,意味着拿不到实例的this
render()
该方法在一个React组件中是必须实现的,你可以看成是一个java interface的接口
这是React组件的核心方法,用于根据状态state和属性props渲染一个React组件。我们应该保持该方法的纯洁性,这会让我们的组件更易于理解,只要state和props不变,每次调用render返回的结果应当相同,所以不要在render方法中改变组件状态,或者和浏览器直接交互。
componentDidMount()
componentDidMount方法会在render方法之后立即被调用,该方法在整个React生命周期中只会被调用一次。React的组件树是一个树形结构,此时可以认为这个组件以及它所有的子组件都已经渲染完成,所以在这个方法中可以调用和真实DOM相关的操作了。
在第一次渲染后调用,只在客户端。之后组件已经生成了对应的DOM结构,可以通过this.getDOMNode()来进行访问。 如果你想和其他JavaScript框架一起使用,可以在这个方法中调用setTimeout, setInterval或者发送AJAX请求等操作(防止异步操作阻塞UI)。
有些组件的启动工作是依赖 DOM 的,例如动画的启动,而 componentWillMount
的时候组件还没挂载完成,所以没法进行这些启动工作,这时候就可以把这些操作放在 componentDidMount
当中。
可以在这个函数中发送异步请求,在回调函数中调用setState()设置state,等数据到达后触发重新渲染。尽量不要在这个函数中直接调用setState()设置状态,这会触发一次额外的重新渲染,可能造成性能问题。在componentDidMount加载数据并设置状态。
示例代码:
componentDidMount() {
console.log('componentDidMount')
fetch("https://api.github.com/search/repositories?q=language:java&sort=stars")
.then(res => res.json())
.then((result) => {
this.setState({
// 触发render
items: result.items
})
})
.catch((error) => { console.log(error)})
// this.setState({color: xxx}) // 不要这样做
}
更新期间
当组件的状态或属性变化时会触发更新,更新过程中会依次调用以下方法:
- getDerivedStateFromProps() 上文已描述
- componentWillUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
shouldComponentUpdate(nextProps, nextState)
这个方法用来告诉React是否要进行下一次render(),默认这个函数返回true,即每次更新状态和属性的时候都进行组件更新。
注意这个函数如果返回false,并不会导致子组件也不更新。
这个函数一般不需要实现,如果组件性能比较差或者渲染比较耗时,可以考虑使React.PureComponent重新实现该组件,PureComponent默认实现了一个版本的shouldComponentUpdate会进行state和props的比较。如有必要,可以自己实现比较nextProps和nextState是否发生了改变。
该函数通常是优化性能的紧急出口,是个大招,不要轻易用,如果要用可以参考Immutable 详解及 React 中实践 .
getSnapshotBeforeUpdate()
该方法的触发时间为update发生的时候,在render之后dom渲染之前返回一个值,作为componentDidUpdate的第三个参数。该函数与 componentDidUpdate 一起使用可以取代 componentWillUpdate 的所有功能,以下是官方的例子。
示例代码:
class ScrollingList extends React.Component {
constructor(props) {
super(props)
this.listRef = React.createRef()
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current
return list.scrollHeight - list.scrollTop
}
return null
}
componentDidUpdate(prevProps, prevState, snapshot) {
// If we have a snapshot value, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
// (snapshot here is the value returned from getSnapshotBeforeUpdate)
if (snapshot !== null) {
const list = this.listRef.current
list.scrollTop = list.scrollHeight - snapshot
}
}
render() {
return ( {/* ...contents... */} )
}
}
componentDidUpdate(prevProps, prevState, snapshot)
该方法会在更新完成后被立即调用,可以在这个方法中进行DOM操作,或者做一些异步调用。这个和首次装载过程后调用componentDidMount是类似的,不一样的是可能需要判断属性是否变化了再发起网络请求。
示例代码:
componentDidUpdate(prevProps) {
if(prevProps.myProps !== this.props.myProp) {
// this.props.myProp has a different value
// we can perform any operations that would
// need the new value and/or cause side-effects
// like AJAX calls with the new value - this.props.myProp
}
}
卸载期间
卸载期间是指组件被从DOM树中移除时,调用的相关方法为:
- componentWillUnmount()
componentWillUnmount()
该方法会在组件被卸载之前被调用,可以在这个函数中进行相关清理工作,比如删除定时器。
示例代码:
componentWillUnmount() {
console.log('componentWillUnmount')
// 清除timer
clearInterval(this.timerID1)
clearTimeout(this.timerID2)
// 关闭socket
this.myWebsocket.close()
// 取消消息订阅...
}
错误捕获
React16中新增了一个生命周期函数:
- componentDidCatch()
componentDidCatch(error, info)
在react组件中如果产生的错误没有被被捕获会被抛给上层组件,如果上层也不处理的话就会抛到顶层导致浏览器白屏错误,在React16中可以实现这个方法来捕获子组件产生的错误,然后在父组件中妥善处理,比如弹窗展示错误等。
在这个函数中通常只进行错误恢复相关的处理,不做其他流程控制方面的操作。
示例代码:
componentDidCatch(error, info) {
// 展示显示错误信息的组件
this.setState({
hasError: true
})
// 也可以调用处理错误的service logErrorToMyService(error, info)
}
React16中的生命周期函数变化
componentWillMount,componentWillUpdate, componentWillReceiveProps等生命周期方法在下个主版本中会被废弃。这些生命周期方法被认为是不安全的,在React16中被重命名为UNSAFE_componentWillMount,UNSAFE_componentWillUpdate,UNSAFE_componentWillReceiveProps,在下个大版本中会被废弃,详见 React 16.3版本发布公告。
总结
每个生命周期都有自己存在的意义,但在React使用过程中最常用到的生命周期函数是如下几个:
constructor: 初始化状态,进行函数绑定
componentWillMount 在渲染前调用,在客户端也在服务端。(废弃)
componentDidMount : 在第一次渲染后调用,只在客户端。之后组件已经生成了对应的DOM结构,可以通过this.getDOMNode()来进行访问。 如果你想和其他JavaScript框架一起使用,可以在这个方法中调用setTimeout, setInterval或者发送AJAX请求等操作(防止异步操作阻塞UI)。进行DOM操作,进行异步调用初始化页面。
componentWillReceiveProps 在组件接收到一个新的 prop (更新后)时被调用。这个方法在初始化render时不会被调用。根据props更新状态。(废弃)使用getDerivedStateFromProps替代
shouldComponentUpdate 返回一个布尔值。在组件接收到新的props或者state时被调用。在初始化时或者使用forceUpdate时不被调用。
可以在你确认不需要更新组件时使用。
componentWillUpdate在组件接收到新的props或者state但还没有render时被调用。在初始化时不会被调用。(废弃)getSnapshotBeforeUpdate 与 componentDidUpdate 一起使用可以取代 componentWillUpdate 的所有功能。
componentDidUpdate 在组件完成更新后立即调用。在初始化时不会被调用。
componentWillUnmount在组件从 DOM 中移除之前立刻被调用。清理组件定时器,网络请求或者相关订阅等。