前言
-
你还在为该使用无状态组件(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使用
-
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和schoolDataimport { 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]
}