愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

React学习笔记(五):高阶知识(下)

一、错误边界(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} />
    }
}

虽然CommentListBlogPost组件并不相同,但是他们的大部分实现逻辑却是相似的,这便是两个组件之间的交叉,其交叉部分如下:

  • 挂载组件时,向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} />
        }
    }
}

如此一来,CommentListBlogPost里就可以移除交叉的那部分代码,改写如下:

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.staticMethodundefined,所以我们需要这么处理:

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>
        )
    }
}