生命周期与异常边界
React 组件的生命周期与异常边界
生命周期
组件的生命周期分成三个状态:
- Mounting:已插入真实 DOM,即 Initial Render
- Updating:正在被重新渲染,即 Props 与 State 改变
- Unmounting:已移出真实 DOM,即 Component Unmount
React 为每个状态都提供了两种处理函数,will 函数在进入状态之前调用,did 函数在进入状态之后调用,三种状态共计五种处理函数。
- componentWillMount()
- componentDidMount()
- componentWillUpdate(object nextProps, object nextState)
- componentDidUpdate(object prevProps, object prevState)
- componentWillUnmount()
此外,React 还提供两种特殊状态的处理函数。
- componentWillReceiveProps(object nextProps):已加载组件收到新的参数时调用
- shouldComponentUpdate(object nextProps, object nextState):组件判断是否重新渲染时调用
Initial Render
Props Change
State Change
这里可以看出,Props 比 State 的改变会有多出一个componentWillReceiveProps
的回调方法。在 React 中有一种被称为PureRenderMixin的 Mixin 模式,它可以用来对新的属性和之前的属性进行对比,如果是数据没有发生变化,就不再重新渲染。在内部实现上,它也是基于 shouldComponentUpdate 方法的。
这听起来很赞,但遗憾的是,PureRenderMixin 并不能很好的进行对象的比较。它只会检查对象引用的相等性(===),也就是说,对于有相同数据的不同对象而言它会返回 false。
`boolean shouldComponentUpdate(object nextProps, object nextState)`;
如果 shouldComponentUpdate 返回的是 false 的话,render 函数便会跳过,直到状态再次发生改变。(此外,componentWillUpdate 和 componentDidUpdate 也会被跳过)。对于上面所说的问题,我们可以简单的举个例子来说明,有代码如下:
`var``a = { foo: ``'bar'``}; ``var``b = { foo: ``'bar'``};``a === b; ``// false`;
可以看到,数据是相同的,但它们隶属于不同对象的引用,因此返回的是 false,也因此组件仍然会进行重新渲染,显然这没有达到我们的目的。如果我们想要达成设想的效果(即对于相同数据而言,组件不再重新渲染),我们就需要在原始的对象上进行数据的修改:
`var``a = { foo: ``'bar'``}; ``var``b = a; ``b.foo = ``'baz'``; ``a === b; ``// true`;
虽然实现一个能够进行深度对象比较的 mixin 来代替引用检查并不困难,但是,考虑到 React 调用 shouldComponentUpdate 方法非常频繁,并且对象的 深度检查代价较高,所以 React 选择了这种对象引用比较的方案。
Component Unmount
如果需要判断某个组件是否挂载,可以 isMounted()方法进行判断,可以用该方法来确保异步调用中的 setState 与 forceUpdate 方法不会被误用。不过该方法在 ES6 的类中已经被移除了,在未来的版本中也会被逐步移除。
总结而言,一个完整的 React Component 的写法应该如下:
/**
* @jsx React.DOM
*/
var React = require("react"),
MyReactComponent = React.createClass({
// The object returned by this method sets the initial value of this.state
getInitialState: function() {
return {};
}, // The object returned by this method sets the initial value of this.props // If a complex object is returned, it is shared among all component instances
getDefaultProps: function() {
return {};
}, // Returns the jsx markup for a component // Inspects this.state and this.props create the markup // Should never update this.state or this.props
render: function() {
return <div />;
}, // An array of objects each of which can augment the lifecycle methods
mixins: [], // Functions that can be invoked on the component without creating instances
statics: {
aStaticFunction: function() {}
}, // -- Lifecycle Methods -- // Invoked once before first render
componentWillMount: function() {
// Calling setState here does not cause a re-render
}, // Invoked once after the first render
componentDidMount: function() {
// You now have access to this.getDOMNode()
}, // Invoked whenever there is a prop change // Called BEFORE render
componentWillReceiveProps: function(nextProps) {
// Not called for the initial render
// Previous props can be accessed by this.props
// Calling setState here does not trigger an an additional re-render
}, // Determines if the render method should run in the subsequent step // Called BEFORE a render // Not called for the initial render
shouldComponentUpdate: function(nextProps, nextState) {
// If you want the render method to execute in the next step
// return true, else return false
return true;
}, // Called IMMEDIATELY BEFORE a render
componentWillUpdate: function(nextProps, nextState) {
// You cannot use this.setState() in this method
}, // Called IMMEDIATELY AFTER a render
componentDidUpdate: function(prevProps, prevState) {}, // Called IMMEDIATELY before a component is unmounted
componentWillUnmount: function() {}
});
module.exports = MyReactComponent;
/**
* ------------------ The Life-Cycle of a Composite Component ------------------
*
* - constructor: Initialization of state. The instance is now retained.
* - componentWillMount
* - render
*
- [children's constructors]
*
- [children's componentWillMount and render]
*
- [children's componentDidMount]
* - componentDidMount
*
* Update Phases:
* - componentWillReceiveProps (only called if parent updated)
* - shouldComponentUpdate
* - componentWillUpdate
* - render
*
- [children's constructors or receive props phases]
* - componentDidUpdate
*
* - componentWillUnmount
*
- [children's componentWillUnmount]
*
- [children destroyed]
* - (destroyed): The instance is now blank, released by React and ready for GC.
*
* -----------------------------------------------------------------------------
*/
实例化
存在期
销毁期
函数式组件的生命周期
异常处理
在 React 15.x 及之前的版本中,组件内的异常有可能会影响到 React 的内部状态,进而导致下一轮渲染时出现未知错误。这些组件内的异常往往也是由应用代码本身抛出,在之前版本的 React 更多的是交托给了开发者处理,而没有提供较好地组件内优雅处理这些异常的方式。在 React 16.x 版本中,引入了所谓 Error Boundary 的概念,从而保证了发生在 UI 层的错误不会连锁导致整个应用程序崩溃;未被任何异常边界捕获的异常可能会导致整个 React 组件树被卸载。所谓的异常边界即指某个能够捕获它的子元素(包括嵌套子元素等)抛出的异常,并且根据用户配置进行优雅降级地显示而不是导致整个组件树崩溃。异常边界能够捕获渲染函数、生命周期回调以及整个组件树的构造函数中抛出的异常。我们可以通过为某个组件添加新的 componentDidCatch(error, info)
生命周期回调来使其变为异常边界:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
然后我们就可以如常使用该组件:
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
componentDidCatch()
方法就好像针对组件的 catch {}
代码块;不过 JavaScript 中的 try/catch
模式更多的是面向命令式代码,而 React 组件本身是声明式模式,因此更适合采用指定渲染对象的模式。需要注意的是仅有类组件可以成为异常边界,在真实的应与开发中我们往往会声明单个异常边界然后在所有可能抛出异常的组件中使用它。另外值得一提的是异常边界并不能捕获其本身的异常,如果异常边界组件本身抛出了异常,那么会冒泡传递到上一层最近的异常边界中。在真实地应用开发中有的开发者也会将崩坏的界面直接展示给开发者,不过譬如在某个聊天界面中,如果在出现异常的情况下仍然直接将界面展示给用户,就有可能导致用户将信息发送给错误的接受者;或者在某些支付应用中导致用户金额显示错误。因此如果我们将应用升级到 React 16.x,我们需要将原本应用中没有被处理地异常统一包裹进异常边界中。譬如某个应用中可能会分为侧边栏、信息面板、会话界面、信息输入等几个不同的模块,我们可以将这些模块包裹进不同的错误边界中;这样如果某个组件发生崩溃,会被其直属的异常边界捕获,从而保证剩余的部分依然处于可用状态。同样的我们也可以在异常边界中添加错误反馈等服务接口以及时反馈生产环境下的异常并且修复他们。完整的应用代码如下所示:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null, errorInfo: null };
}
componentDidCatch(error, errorInfo) {
// Catch errors in any components below and re-render with error message
this.setState({
error: error,
errorInfo: errorInfo
}); // You can also log error messages to an error reporting service here
}
render() {
if (this.state.errorInfo) {
// Error path
return (
<div>
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: "pre-wrap" }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo.componentStack}
</details>
</div>
);
} // Normally, just render children
return this.props.children;
}
}
class BuggyCounter extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(({ counter }) => ({
counter: counter + 1
}));
}
render() {
if (this.state.counter === 5) {
// Simulate a JS error
throw new Error("I crashed!");
}
return <h1 onClick={this.handleClick}>{this.state.counter}</h1>;
}
}
function App() {
return (
<div>
<p>
<b>
This is an example of error boundaries in React 16. <br />
<br />
Click on the numbers to increase the counters. <br />
The counter is programmed to throw when it reaches 5. This simulates a
JavaScript error in a component.
</b>
</p>
<hr />
<ErrorBoundary>
<p>
These two counters are inside the same error boundary. If one crashes,
the error boundary will replace both of them.
</p>
<BuggyCounter />
<BuggyCounter />
</ErrorBoundary>
<hr />
<p>
These two counters are each inside of their own error boundary. So if
one crashes, the other is not affected.
</p>
<ErrorBoundary>
<BuggyCounter />
</ErrorBoundary> <ErrorBoundary>
<BuggyCounter />
</ErrorBoundary>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));