react hook系列一: 诞生

309 阅读7分钟

react-hook

本文着重讲react hook要解决的问题,以及与render propshoc两种方式的比较。关于react hook最佳实践另外写一篇文章进行。

react-hook的出现要解决什么问题

概括的来说就是为了代码复用这个目标,编程中出现的大部分解决方案其实都是两个核心诉求驱动分治复用。达到良好复用效果的途径有很多,或许分治是最有效的方式。

web从诞生开始,经历了众多开发方式的演进迭代,比如初期浑然一体的方式,后端为主的MVC方式,富客户端应用的前端MV*方式,前后端同构的方式等等。除了早期需求和技术太过于简单的时代而采用的简单方式外,其他的几种颇具代表性的方式都蕴含着分层模块化组件化分治 的思想和操作在其中,既然分开处理,那么必然昭示着各部分的职责和操作手法是不同的。

react的世界里,要想构建出精美实用的webApp,只有一种武器可用使用,那就是组件。那么问题就此诞生了。我们要做视觉呈现,用组件;要写业务逻辑,用组件;要写状态管理,也是组件(react默认状态提升, 当然也可以抽离使用reduxmobx等进行全局状态管理,对状态管理是另外一个比较大的话题,这里先不过多阐述)。

所以说我们会开始组件开始刻意划分约束,能够感知数据变化进行数据传递的smart component,使用数据进行视觉渲染的dumb component,使用这种划分来保障数据状态处理视觉呈现的分离,但由此也就诞生了数据状态管理如何划分如何管理的话题(所以这是先创造一个问题,再来解决这个问题啊。。)

另外单单这么划分还不够,比如我们经常有这样的需求场景:对同样的一些数据花样呈现,有列表展示、有饼图占比、有折线图趋势、有热力图说明频次等等,这些组件使用的是相同的一些数据和数据处理逻辑。

其中数据还好,我们可以抽离到公共的状态管理中进行统一维护,结合上面的两种组件划分方式进行数据一致性保障

数据处理逻辑呢,肯定是写一套复用之,统一管理统一维护嘛。目前有两种主要的操作方式,render propshoc,下面通过一个例子来详细阐述这两个技术手段的具体实现。

render props渲染属性函数

react中组件接收外界的数据使用props属性,render props也就是说接收一个用来做渲染的属性,进一步说,组件来提供数据逻辑的处理,然后数据处理好了,使用这些数据渲染出什么样的视觉效果,就由外界传递来的props属性自行决定。

所以这种模式,组件实际上承载的是逻辑处理功能,如何渲染逻辑处理的结果数据,由使用者来决定(传递渲染属性,一般而言都是一个函数,因为函数可以动态接收数据)

下面我们来看一个来自官网使用光标位置渲染的例子

import React, {Component} from 'react'
import {render} from 'react-dom'

class Mouse extends Component {
    constructor(props) {
        super(props)

        this.handleMouseMove = this.handleMouseMove.bind(this)

        this.state = {x: 0, y: 0}
	}

	handleMouseMove(e) {
        this.setState({
            x: e.clientX,
            y: e.clientY
        })
	}

    render() {
        const {x, y} = this.state

        // 此时Mouse组件已经拿到处理好的光标位置数据了
        // 渲染就交给使用者传递来的`render props函数`
        return {this.props.render(this.state)}
    }
}

class Tracker extends Component {
    _render(data) {
        return <div>x: {data.x}, y: {data.y}</div>
    }

    render() {
        return <Mouse render={this._render} />
    }
}

class Cat extends Component {
    _render(data) {
        return <img src='require('xxx.png')' style={{left: data.x; top: data.y}} />
    }

    render() {
        return <Mouse render={this._render} />
    }
}

class App extends Component {
    render() {
        return (
            <Tracker />
            <Cat />
        )
    }
}

render(
    <App />,
    document.getElementById('app')
)

从上面的示例可以看出,Mouse组件负责具体的光标逻辑数据处理,对应在业务代码中,一般是获取异步和更新数据操作。TrackerCat是消费数据做视觉呈现。

一般情况下我们使用render props方式还有另外一种常用的操作手法,借助组件的props.children属性来实现,children是表示组件的子节点的属性,和上面不同的是,这个属性的名字是固定的,内容随着需要的不同而不同。

我们可以把上面的render属性去掉,改成使用children属性,关键代码如下:

class Mouse extends Component {
	// ...
    render() {
        const {x, y} = this.state

        // 此时Mouse组件已经拿到处理好的光标位置数据了
        // 渲染就交给使用者传递来的`render props函数`
        return {this.props.children(this.state)}
    }
}

class Tracker extends Component {
    render() {
        return (
            <Mouse>
                {data => (
                    <div>x: {data.x}, y: {data.y}</div>
                )}
            </Mouse>
        )
    }
}

class Cat extends Component {
    render() {
        return <Mouse> {data => <img src='require('xxx.png')' style={{left: data.x; top: data.y}} />} </Mouse>
    }
}

使用自定义的render属性还是组件默认的children属性,看个人的偏好而定,没有明显的优劣差异。

hoc高阶组件

高阶组件的适用场景有很多,这里的示例是用在属性代理场景中,高阶组件用来开发业务逻辑处理,然后向其中注入使用这些业务逻辑处理所得的数据进行视觉渲染。

或者可以说高阶组件就是负责通用功能的组件,高阶组件接收渲染组件参数,返回接收通用组件的数据的渲染组件

import React, {Component} from 'react'

class Tracker extends Component {
    render() {
        return '鼠标位置x: ' + this.props.x + ', y: ' + this.props.y
    }
}

import React, {Component} from 'react'

class Tracker extends Component {
    render() {
        return '鼠标位置x: ' + this.props.x + ', y: ' + this.props.y
    }
}

class Cat extends Component {
    render() {
        return <img src={require('@assets/logo.png')} style={{position: 'absolute', top: this.props.y, left: this.props.x}} />
    }
}

function withMouse(WrappedComponent) {
    return class Common extends Component {
        constructor(props) {
            super(props)

            this.state = {
                x: 0,
                y: 0
            }

            this.handler = this.handler.bind(this)
        }

        componentDidMount() {
            document.addEventListener('mousemove', this.handler, false)
        }

        handler(ev) {
            this.setState({
                x: ev.clientX,
                y: ev.clientY
            })
        }

        componentWillUnmount() {
            document.removeEventListener('mousemove', this.handler, false)
        }

        render() {
            return <WrappedComponent {...this.state} />
        }
    }
}

class App extends Component {
    render() {
        // 将高阶组件先进行返回,返回值是一个组件,然后再使用
        // 否则会报警告,因为高阶组件是一个返回组件的函数
        const Tracker1 = withMouse(Tracker)
        const Cat1 = withMouse(Cat)

        return (
            <div>
                <Tracker1 />
                <Cat1 />
            </div>
        )
    }
}

render(
    <App />,
    document.getElementById('app')
)

hook

好了,现在到了本文的主角hook,我们首先来看一下使用hook来实现同样逻辑

import React, {Component} from 'react'

function usePosition() {
    const [position, setPosition] = useState({x:0, y: 0})

    function handler(ev) {
        setPosition({x: ev.pageX, y: ev.pageY})
    }

    useEffect(() => {
        document.addEventListener('mousemove', handler, false)

        return () => {
            document.removeEventListener('mousemove', handler, false)
        }
    })

    return position
}

function App() {
    const position = usePosition()

    return (
        <div>
            <p>'鼠标位置x: ' + position.x + ', y: ' + position.y</p>
            <img src={require('@assets/logo.png')} style={{position: 'absolute', top: position.y, left: position.x}} />
        </div>
    )
}

从上面的代码中可以看到,hook封装了对业务逻辑的处理,对组件而言,拿到数组使用即可。当然这个例子比较简单纯粹,我们会说组件肯定会和业务逻辑有交互,必然是这样的,用户的交互行为要反馈到数据中,hook则负责封装数据如何处理的逻辑,非常类似在react中提供了一层数据状态管理,基于此,可以方便的在组件间进行业务逻辑的复用

同时必须明确指出,hook并非是替代redux mobx等数据状态管理工具的方案,hook的推出是为了解决同层次组件间的代码复用问题的(代码横切问题),同时hook也并不能完全替代renderPropsHOC,后面我们详细的阐述下面的几个问题

  • react提供了哪些hook,分别适用于哪些场景
  • hook和hoc及renderprops适用的场景
  • hook的最佳实践
  • hook的生态