React Hooks浅谈

468 阅读7分钟

前言

  • 你还在为该使用无状态组件(Function)还是有状态组件(Class)而左右为难吗?——拥有React Hooks,你在也不用写Class了,所有的组件都是Function

  • 你还在为搞不清楚React生命周期函数而彻夜难眠吗?——拥有React Hooks,你再也不用懂生命周期了

  • 你还在为组件中的this指针晕头转向吗?——拥有React Hooks,连Class都抛弃了,还有哪来的this

What React Hooks?

Hook是React 16.8.0新增的一个特性。它可以让你不编写class的情况下使用state以及其他的react特性。Hook单词的字面意思就是“钩子”。它的作用就是在函数组件内部钩入React State 及生命周期等特性。React本身自带很多钩子,当然开发者可以去自定义钩子。

Why React Hooks?

  • 想要复用一个有状态的组件有时候真的太TM麻烦了

    React的核心思想是组件,将页面拆成一个个独立的、可复用的组件,通过自上而下的单向数据流的形式将这些组件串联起来。组件化开发给前端带来前所未有的体验,我们可以像玩乐高一样将组件堆积拼接起来,组成完整的UI界面。带来的好处就是提高代码的复用度,加快开发速度。但随着业务功能复杂度的提高,业务代码不得不和不同的函数糅合在一起。这样很多重复的复杂的业务逻辑代码很难被抽离出来,有时候为了加快开发速度不得不采用复制/黏贴。当业务逻辑发生变化的时候,我们又不得不同时去修改很多地方。极大影响开发效率和可维护性。为了解决业务逻辑复用问题,React官方也做了很多努力,先后推出了mixin、高级组件(Higher-Order Components)和属性渲染(Render Props)。
    现在假设有个需求是在页面上实时显示鼠标移动的坐标。采用传统的方法来实现,代码如下:
    import React from 'react'
    class Detail extends React.Component {
      constructor (props, context) {
        super(props, context)
        this.state = {
          x: 0,
          y: 0
        }
      }
    
      handleMouseMove = (e) => {
        this.setState({
          x: e.clientX, 
          y: e.clientY
        })
      }
    
      render () {
        const { x, y = 0 } = this.state
        return (
          <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
            <h1>当前鼠标的位置是({x}, {y})</h1>
          </div>
        )
      }
    }
    export default Detail
    

    现在假设有一个其他页面也需要实现这个功能,但是页面显示的文字不太一样,那要怎么办呢?
    传统的做法有两种:
    1、直接ctrl+c/ctrl+v。那作为开发人员,一旦你使用ctrl+c/ctrl+v那你就开始在挖坑了。
    2、在组件中传入一个title的props,用于控制页面的显示内容。这种做法看上去显然比第一种更高级一些,但是它显然是违背了OOD的设计原则,破坏类的内部结构。
    接下来我们来看看HOC和RenderProps是如何实现逻辑功能复用的
    高阶组件:HOC本质上是一个函数,这个函数接收一个组件,经过封装,返回一个新的组件。

    import React from 'react'
    const withMouse = (Component) => {
      return class extends React.Component {
        state = { x: 0, y: 0 }
    
        handleMouseMove = (event) => {
          this.setState({
            x: event.clientX,
            y: event.clientY
          })
        }
    
        render() {
          return (
            <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
              <Component {...this.props} mouse={this.state}/>
            </div>
          )
        }
      }
    }
    export {withMouse}
    

    这就是一个简单的高阶组件,它的使用方法如下。该高阶组件实现的功能就是鼠标移动,在页面上实时显示鼠标的坐标。

    import React from 'react'
    import { withMouse } from './HocUtils'
    class Test extends React.Component {
      render () {
        const {x, y} = this.props.mouse || {}
        return (
          <h1>通过高阶组件的方式获取当前鼠标的位置是({x}, {y})</h1>
        )
      }
    }
    export default withMouse(Test)
    

    RenderProps:在组件中调用父组件的方法来渲染。
    下面来介绍一下,要用RenderProps来实现上面高阶组件的需求,首先定义一个Mouse组件

    import React from 'react'
    class Mouse extends React.Component {
      constructor (props, context) {
        super(props, context)
        this.state = {
          x: 0,
          y: 0
        }
      }
    
      handleMouseMove = (e) => {
        this.setState({
          x: e.clientX, y: e.clientY
        })
      }
      
      render () {
        return (
          <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
            {this.props.render(this.state)}
          </div>
        )
      }
    }
    export default Mouse
    

    在Mouse组件的render方法中,调用了一个父组件的方法进行渲染,这个是RenderProps的核心。Mouse组件的使用方法如下:

    import React from 'react'
    import Mouse from './components/Mouse'
    export default class RenderProps extends React.Component {
      render () {
        return (
          <Mouse render={({x, y}) => {
            return (
              <h1>通过Render Props方法,获取当前鼠标的位置是({x}, {y})</h1>
            )
          }}/>
        )
      }
    }
    
  • 面向生命周期编程

    在一个组件中,生命周期函数随处可见。每个生命周期里都承担着一个或多个业务逻辑的一部分。换句话说某个业务逻辑分散在各个组件生命周期中。

而React Hooks的出现可以将这种面相生命周期函数编程直接变成面向业务逻辑编程。

常见的React Hooks使用

  • State Hook

    useStates是React提供的一个Hook,它的功能类似于类组件中的state。useState接收一个参数,返回一个数组。返回的数组中第一个元素为当前的state的name,第二个元素为改变这个state的方法,接收的参数为这个state的初始值。

    import React, { useState } from 'react'
    import { Button } from 'antd'
    const CustomButton = (props) => {
      const NAMES = ['kobe', 'TD', 'manu', 'parker']
      const [count, setCount] = useState(0)
      const [name, setName] = useState(NAMES[0])
      const handleCountBtnClick = () => {
        setCount(count + 1)
      }
      const handleNameBthClick = () => {
        let index = NAMES.indexOf(name)
        index = index === 3 ? -1 : index
        setName(NAMES[index + 1])
      }
      return (
        <div>
          <Button type='primary' onClick={handleCountBtnClick}>改变次数,当前次数{count}</Button>
          <Button type='primary' onClick={handleNameBthClick}>改变名字,当前名字{name}</Button>
        </div>
      )
    }
    export default CustomButton
    
  • Effect Hook

    Effect Hook允许你在函数组件中执行副作用操作。React将旧版本生命周期函数componentDidMount、componentDidUpdate、componentWillUnmount三个函数合并成一个API即useEffect()。
    useEffect接收两个参数:
    第一个参数是一个函数,该函数在初始化的时候执行一次,之后在render之后会重新执行一次。该函数可以返回一个新函数,新函数在组件卸载之前调用,相当于类组件中componentWillUnmount。
    第二个参数是一个数组,数组中的元素为需要触发执行函数的state的name。

    import React, { useState, useEffect } from 'react'
    import { Button, Table } from 'antd'
    import axios from 'axios'
    const CustomButton = (props) => {
      const NAMES = ['kobe', 'TD', 'manu', 'parker']
      const COLUMNS = [
        {title: '部门名称', dataIndex: 'bmmc', key: 'bmmc'},
        {title: '所属校区', dataIndex: 'ssxq_mc', key: 'ssxq_mc'}
      ]
      const [name, setName] = useState(NAMES[0])
      const [data, setData] = useState([])
      const [schoolId, setSchoolId] = useState('58850bfcbe5e40d79d1f6c85db394266')
      useEffect(() => { // 设置title
        document.title = name
        console.log('title change')
      })
    
      const onComponentWillUnmount = () => {
        console.log('组件准备卸载啦...')
      }
    
      useEffect(() => { // 加载数据
        axios.get(`http://esp-edu-nation-base-dms.debug.web.nd/v1.0/xx/${schoolId}/xxbm?ssyx=&bmlx=&bmmc=`)
        .then(res => {
          setData(res.data || [])
        })
        return onComponentWillUnmount
      }, [schoolId])
    
      const handleSchoolChange = () => {
        setSchoolId('3fb2d6d3a042417a8a26606a2724bc7f')
      }
    
      return (
        <div>
          <Button type='primary' onClick={handleSchoolChange}>切换学校</Button>
          <div style={{marginTop: '1rem'}}>
            <Table columns={COLUMNS} dataSource={data || []} total={data.length} bordered/>
          </div>
        </div>
      )
    }
    export default CustomButton
    
    

    在上述例子中,
    1、"设置title"的useEffect会每次render之后都调用;
    2、"加载数据"的useEffect,除了初始化的时候执行一次,只有在schoolId发生变化之后才会在执行;
    3、onComponentWillUnmount会在组件卸载之前调用;

  • Custom Hook

    在前面我们分析了React Hook可以用来解决业务逻辑复用的问题。常用的手段就是自定义Hook。在上述Effect Hook的例子中,加载数据的逻辑会在很多页面用到,我们就可以通过自定义一个useSchool的Hook来实现业务逻辑的复用。操作步骤如下:
    1、定义一个useSchool的Hook,useSchool接收一个schoolId,返回loading和schoolData

    import { useState, useEffect } from 'react'
    import axios from 'axios'
    
    const useSchool = (schoolId) => {
      const [loading, setFetching] = useState(true)
      const [schoolData, setSchoolData] = useState([])
      useEffect(() => {
        console.log('*******customHook加载数据*****')
        setFetching(true)
        axios.get(`http://esp-edu-nation-base-dms.debug.web.nd/v1.0/xx/${schoolId}/xxbm?ssyx=&bmlx=&bmmc=`)
        .then(res => {
          setSchoolData(res.data || [])
          setFetching(false)
        })
      }, [schoolId])
      return [loading, schoolData]
    }
    export default useSchool
    

    2、在需要使用这个业务逻辑的页面,使用useSchool

    import React, { useState } from 'react'
    import { Button, Table } from 'antd'
    import useSchool from '../../hooks/useSchool'
    const CustomButton = (props) => {
      const COLUMNS = [
        {title: '部门名称', dataIndex: 'bmmc', key: 'bmmc'},
        {title: '所属校区', dataIndex: 'ssxq_mc', key: 'ssxq_mc'}
      ]
      const [schoolId, setSchoolId] = useState('58850bfcbe5e40d79d1f6c85db394266')
      const [loading, data] = useSchool(schoolId) // 调用自定义hook获取学校数据
    
      const handleSchoolChange = () => {
        setSchoolId('3fb2d6d3a042417a8a26606a2724bc7f')
      }
    
      return (
        <div>
          <Button type='primary' onClick={handleSchoolChange} style={{marginLeft: '1rem'}}>切换学校</Button>
          <div style={{marginTop: '1rem'}}>
            <Table columns={COLUMNS} dataSource={data || []} total={data.length} bordered/>
          </div>
        </div>
      )
    }
    
    export default CustomButton
    
    

Reack Hooks 简单原理

在React社区里关于React Hooks有一句很经典的话"React Hooks, not magic, just arrays"。React Hooks并没有多神奇,它只是数组的应用。
useState的简单原理:

    let state = []
    let setters = []
    let cursor = 0
    
    function createSetter(cursor) {
      return function setStateWithCursor(newVal) {
        state[cursor] = newVal
      }
    }
    
    export function useState(initVal) {
      state.push(initVal)
      setters.push(createSetter(cursor))
      const value = state[cursor]
      const setter = setters[cursor]
      cursor++
      return [value, setter]
    }

Hook 简介

精读《React Hooks》

30分钟精通React今年最劲爆的新特性——React Hooks

React Hooks 入门教程