一、错误边界(Error Boundaries)
错误边界是用来捕获子组件树内的Javascript异常,记录错误并展示一个回退的UI的React新特性。当在渲染期间发生错误的时候,就可以避免整棵组件数发生异常
不过,错误边界无法捕捉以下错误:
- 事件处理
- 异步代码(
setTimeout
或者requestAnimationFrame
回调函数) - 服务端渲染
- 错误边界自身抛出的错误
1、用法
如果一个类定义组件
定义了componentDidCatch(error, info)
方法,那么就成为了一个错误边界,如:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
componentDidCatch(error, info) {
// 展示回退UI
this.setState({ hasError: true })
// 还可以调用日志服务来记录错误信息
logErrorToMyService(error, info)
}
render() {
if (this.state.hasError) {
return <h1>发生了错误!</h1>
}
return this.props.children
}
}
然后,我们就可以这么使用:
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
如此,当<ErrorBoundary>
内的组件发生错误时,就能够捕获到错误,这就相当于为组件包裹了try-catch
块。但是错误边界仅可以捕获内部组件的错误,而不能捕获自身的错误。如果错误边界自身无法渲染错误信息,那么错误会向上冒泡至最近的错误边界
2、componentDidCatch参数
componentDidCatch
具有参数error
和参数info
,其中:
error
参数记录了被抛出的错误info
参数是个对象,含有componentStack
属性,包含了错误期间关于组件的堆栈信息
3、注意事项
1)自React16开始,任何未被错误边界捕获的错误,将会卸载整个React组件树
2)错误边界的使用范围为声明式组件,命令式代码、事件处理器中的错误处理则使用try-catch
二、高阶组件(HOC)
HOC是React中对组件逻辑进行重用的高级技术。其主要思想是通过一个函数,接收一个组件作为参数,然后返回一个新的组件,即:
const EnhancedComponent = higherOrderComponent(WrappedComponent)
1、使用HOC解决交叉问题
在React里,组件是代码复用的主要单元,然而在一些模式下,传统的组件并不适用。如有两个组件,里面都有相同的行为:
class CommentList extends React.Component {
constructor() {
super()
this.handleChange = this.handleChange.bind(this)
this.state = {
comments: DataSource.getComments()
}
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange)
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange)
}
handleChange() {
this.setState({
comments: DataSource.getComments()
})
}
render() {
return (
<div>
{this.state.comments.map(comment => (
<Comment comment={comment} key={comment.id} />
))}
</div>
)
}
}
class BlogPost extends React.Component {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
this.state = {
blogPost: DataSource.getBlogPost(props.id)
}
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange)
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange)
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
})
}
render() {
return <TextBlock text={this.state.blogPost} />
}
}
虽然CommentList
和BlogPost
组件并不相同,但是他们的大部分实现逻辑却是相似的,这便是两个组件之间的交叉
,其交叉部分如下:
- 挂载组件时,向
DataSource
添加一个监听函数 - 监听函数内,每当数据源发生变化,都调用
setState
函数设置新数据 - 卸载组件时,移除监听函数
因此,基于上述交叉部分,我们可以写出withSubscription
函数,如下:
function withSubscription(WrappedComponent, selectData) {
return class extends React.Component {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
this.state = {
data: selectData(DataSource, props)
}
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange)
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange)
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
})
}
render() {
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}
如此一来,CommentList
和BlogPost
里就可以移除交叉的那部分代码,改写如下:
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource, props) => DataSource.getComments()
)
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
)
2、注意点
1)不要在高阶组件内部修改(或以其他方式修改)原组件的原型属性
2)应该将不相关的props属性,悉数传给包裹组件,即:
render() {
return (
<WrappedComponent injectedProp={injectProp} {...passThroughProps} />
)
}
3)除非需要动态的高阶组件,不然不要在render()
函数中调用高阶函数,应该在render()
外调用,否则每次重新渲染时,就都会重新加载一个组件,不仅造成性能问题,还会导致原有组件的所有状态和子组件丢失
4)需要将包裹组件的静态方法做拷贝,如:
WrappedComponent.staticMethod = function() { /* ... */ }
const EnhancedComponent = enhance(WrappedComponent)
这种情况下,EnhancedComponent
是不会得到静态方法的,所以EnhancedComponent.staticMethod
是undefined
,所以我们需要这么处理:
function enhance(WrappedComponent) {
class Enhance extends React.Component { /* ... */ }
Enhance.staticMethod = WrappedComponent.staticMethod
return Enhance
}
也可以使用hoist-non-react-statics
来帮我们自动处理拷贝所有非React的静态方法:
import hoistNonReactStatic from 'hoist-non-react-statics'
function enhance(WrappedComponent) {
class Enhance extends React.Component { /* */ }
hoistNonReactStatic(Enhance, WrappedComponent)
return Enhance
}
5)不要在高阶组件中传递ref
,因为ref
是指向最外层容器组件实例的,而不是包裹组件
三、Render Props
Render Props
是一种在React组件之间使用一个值为函数的prop
在React组件间共享代码的技术,带有Render Prop
的组件带有一个返回React元素
的函数,如:
<DataProvider render={data => (
<h1>Hello {data.target}</h1>
)} />
这种情况下,渲染的具体内容,便由属性值给出
注意事项:
1)render
只是一个名称,并不要求只能使用render
作为属性名称,Render Prop
技术的核心在于prop
值是一个函数,且返回值是React元素
,所以以下都是Render Prop
技术的应用:
<Mouse children={mouse => (
<p>鼠标位置为:{mouse.x},{mouse.y}</p>
)} />
<Mouse>
{mouse => (
<p>鼠标位置为:{mouse.x},{mouse.y}</p>
)}
</Mouse>
2)由于属性值传入的是一个匿名函数,所以每次render()
函数渲染时,都会创建一个新的函数,因此prop的比较结果总为false,所以会抵消React.PureComponent
带来的优势,而解决这一问题的方法也很容易,如下:
class MouseTracker extends React.Component {
constructor(props) {
super(props)
this.renderTheCat = this.renderTheCat.bind(this)
}
renderTheCat(mouse) {
return <Cat mouse={mouse} />
}
render() {
return (
<div>
<Mouse render={this.renderTheCat} />
</div>
)
}
}