React Hooks系列(一):常用Api的使用方法
前言
Hook 是 React 16.8 的新增特性。它解决了函数组件只能做视图渲染的限制,可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
优点
- 可以避开 class 组件中的 this
- 使用自定义hook,逻辑的复用更加简单
- 代码更清爽,可以减少 class 组件中使用高阶组件额外创建的多层 dom 嵌套关系
缺点
- 由于 hooks 大多都基于闭包保存状态,常常在一些异步代码或回调中拿不到最新的状态
- effect 的依赖项交叉改变时会导致意想不到的错误渲染,需要花更多的时间来解决
API
1. useState
目前前端框架一大特点就是数据驱动视图,所以定义数据项,修改数据项,就是基础中的基础,这里分别用类组件和函数组件分别完成一个小功能,每点击 “+1” 按钮一次,页面上数字加一
import React, { Component } from 'react'
import styles from './index.module.css'
export default class ComA extends Component {
state = {
count: 0
}
onClick = () => {
const { count } = this.state
this.setState({ count: count + 1 })
}
render () {
const { count } = this.state
return (
<div className={styles.main}>
<div className={styles.count}>{count}</div>
<button className={styles.btn} onClick={this.onClick}>
+1
</button>
</div>
)
}
}
// useState,就是为了解决函数组件中没有状态
import { useState } from 'react'
import styles from './index.module.css'
export default function ComB () {
// useState函数接收的参数是状态的默认值,其返回值是一个数组,第一个值是该状态的数据项,第二个值就是改变这个状态的方法
const [count, setCount] = useState(0)
const onClick = () => {
setCount(count + 1)
}
return (
<div className={styles.main}>
<div className={styles.count}>{count}</div>
<button className={styles.btn} onClick={onClick}>
+1
</button>
</div>
)
}
useState小结
可以看到,有了 hooks 的函数组件代码行数是要更少,而且摆脱了 this,如果想增加状态继续使用 useState 创建就好
2. useEffect
有了数据项,那自然需要生命周期在不同时机对数据项修改,useEffect 函数就是函数组件的生命周期,还是刚刚的例子,我们添加一个简单的 useEffect 看看效果
import { useState, useEffect } from 'react'
import styles from './index.module.css'
export default function ComB () {
const [count, setCount] = useState(0)
const onClick = () => {
setCount(count + 1)
}
// useEffect 有两个参数第一个参数是函数必填,而且返回值可以没有,如果有必须返回一个函数,第二个参数选填是一个数组
// 这里先写一个最简单的 effect ,其他参数后面会说到
useEffect(()=>{
console.log('111')
})
console.log('渲染视图')
return (
<div className={styles.main}>
<div className={styles.count}>{count}</div>
<button className={styles.btn} onClick={onClick}>
+1
</button>
</div>
)
}
刷新页面,发现 “111” 打印出来了,并且 “渲染页面” 在 “111” 之前打印,这不就是妥妥的 componentDidMount()
我们继续操作,点击几次 “+1” 按钮看看,一共点击三次,打印了三次,看来 componentDidUpdate() 时 也会触发useEffect
我们删掉无关代码,给effect的第一个参数加一个返回值(必须是函数),打印当前的 count 值
import { useState, useEffect } from 'react'
import styles from './index.module.css'
export default function ComB () {
const [count, setCount] = useState(0)
const onClick = () => {
setCount(count + 1)
}
useEffect(() => {
console.log('111')
return () => {
console.log(count)
}
})
return (
<div className={styles.main}>
<div className={styles.count}>{count}</div>
<button className={styles.btn} onClick={onClick}>
+1
</button>
</div>
)
}
刷新页面,抛开首次加载打印的第一个 “111”,每点击一次按钮都会打印上一次的 count 值,然后打印 “111”,这意味着每一次触发effect都会执行上一次effect的返回值,这是类组件所没有的
这时我们移除函数组件(切换至类组件),发现 3 被打印出来了,这个返回值(函数)看来还有 componentWillUnmount() 的作用
现在给effect添加第二个参数,需要传一个数组,先传一个空数组
import { useState, useEffect } from 'react'
import styles from './index.module.css'
export default function ComB (props) {
const [count, setCount] = useState(0)
const onClick = () => {
setCount(count + 1)
}
useEffect(() => {
console.log('111')
return () => {
console.log('222')
}
}, [])
return (
<div className={styles.main}>
<div className={styles.count}>{count}</div>
<button className={styles.btn} onClick={onClick}>
+1
</button>
</div>
)
}
刷新页面首次进入,打印 “111” ,之后无论我们怎么点击 “+1”按钮,都没了反应
之后我们移除函数组件(切换到类组件),“222” 也在移除组件前打印了,看来传入空数组,effect并不会受到数据项变化而触发 ,创建组件移除组件依旧会触发
接下来给空数组加一项 “count” ,发现所有表现,和 effect 不传第二个参数的表现一模一样
import { useState, useEffect } from 'react'
import styles from './index.module.css'
export default function ComB (props) {
const [count, setCount] = useState(0)
const onClick = () => {
setCount(count + 1)
}
useEffect(() => {
console.log('111')
return () => {
console.log('222')
}
}, [count]) // 第二个参数不传,相当于传了一个所有数据项所集合的数组
return (
<div className={styles.main}>
<div className={styles.count}>{count}</div>
<button className={styles.btn} onClick={onClick}>
+1
</button>
</div>
)
}
至此,我们知道了,useEffect可以通过给第一个参数添加返回值,添加第二个参数,代替传统类组件的 componentDidMount()、componentWillUnmount()、componentDidUpdate(),我们模拟一个接口请求,做一个列表渲染试试,体会一下
import { useState, useEffect } from 'react'
import styles from './index.module.css'
// 模拟接口
const getListApi = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{ id: 1, type: 10, text: 'List1' },
{ id: 2, type: 10, text: 'List2' },
{ id: 3, type: 20, text: 'List3' },
{ id: 4, type: 20, text: 'List4' },
{ id: 5, type: 10, text: 'List5' }
])
}, 100)
})
}
export default function ComC () {
const [list, setList] = useState([])
// 模拟接口请求渲染数据
useEffect(async () => {
const res = await getListApi()
setList(res)
}, []) // 接口只进入页面时请求,第二个参数只需要传空数组
return (
<div className={styles.main}>
<div className={styles.list}>
{list.length &&
list.map(val => (
<div className={styles.line} key={val.id}>
{val.text}
</div>
))}
</div>
</div>
)
}
看起来似乎没问题,列表正常渲染了,可是代码似乎有一些不太对,async声明过的函数其返回值是一个promise,而useEffect第一个参数的返回值是需要一个函数
打开控制台,发现有警告,建议我们再包一层不要让useEffect的第一个参数的返回值有冲突
改动一下代码,保证页面不会出现问题,这样一个简单的模拟请求的页面就完成了
import { useState, useEffect } from 'react'
import styles from './index.module.css'
// 模拟接口
const getListApi = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{ id: 1, type: 10, text: 'List1' },
{ id: 2, type: 10, text: 'List2' },
{ id: 3, type: 20, text: 'List3' },
{ id: 4, type: 20, text: 'List4' },
{ id: 5, type: 10, text: 'List5' }
])
}, 100)
})
}
export default function ComC () {
const [list, setList] = useState([])
useEffect(() => {
getList()
}, []) // 接口只进入页面时请求,第二个参数只需要传空数组
// 抽出 async 函数,避免冲突
const getList = async () => {
const res = await getListApi()
setList(res)
}
return (
<div className={styles.main}>
<div className={styles.list}>
{list.length &&
list.map(val => (
<div className={styles.line} key={val.id}>
{val.text}
</div>
))}
</div>
</div>
)
}
useEffect小结
- useEffect第一个参数(函数)会在创建组件开始执行(第二个参数所绑定的数据项更新也会触发执行),其返回值(函数)会在移除组件前触发(第二个参数所绑定的数据项在下一次变化前也会执行),给函数组件带来了生命周期
- useEffect 第一个参数(函数)的返回值一定要是函数,避免 hooks 出现错误
- useEffect也可以写多个(顺序执行)
- 要避免在已经监听某状态的 effect 中修改这个状态,这样将会使 effect 死循环
3. useRef
useRef就是获取dom元素的方法
// 类组件,this.a 就可以获取这个dom
import React from 'react'
class A extends React.Component{
a = React.createRef()
render() { return<div ref={a}></div> }
}
// useRef,b.current
import React, { useRef } from 'react'
function B () {
const b = useRef(null)
return <div ref={b}><div>
}
useRef的使用并不难,绑定之后只需要 .current 就可以获取dom对象,这里想说的是useRef更有用的地方,保存一个任意地方都可以取到的可变值,还记得刚刚的列表渲染的案例吗,现在想加一个功能,点击tab标签根据类别进行筛选展示
import { useState, useEffect } from 'react'
import styles from './index.module.css'
// tab
const TITLE = [
{ code: 0, text: '全部' },
{ code: 10, text: '类型一' },
{ code: 20, text: '类型二' }
]
// 模拟接口
const getListApi = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{ id: 1, type: 10, text: 'List1' },
{ id: 2, type: 10, text: 'List2' },
{ id: 3, type: 20, text: 'List3' },
{ id: 4, type: 20, text: 'List4' },
{ id: 5, type: 10, text: 'List5' }
])
}, 100)
})
}
export default function ComC () {
// tab 选中的索引
const [checkedIdx, setCheckIdx] = useState(0)
// 页面展示的 list
const [list, setList] = useState([])
// 接口返回的所有列表数据
const [AllList, setAllList] = useState([])
useEffect(() => {
getList()
}, [])
// 接口请求,渲染列表
const getList = async () => {
const res = await getListApi()
setList(res)
setAllList(res)
}
// tab切换,试图更新
const onClick = idx => {
setCheckIdx(idx)
if (idx === 0) return setList(AllList)
const newList = AllList.filter(val => val.type === TITLE[idx].code)
setList(newList)
}
return (
<div className={styles.main}>
<div className={styles.tab}>
{TITLE.map((val, idx) => (
<div
onClick={() => {
onClick(idx)
}}
key={val.code}
className={idx === checkedIdx ? styles.checked : ''}
>
{val.text}
</div>
))}
</div>
<div className={styles.list}>
{list.length &&
list.map(val => (
<div className={styles.line} key={val.id}>
{val.text}
</div>
))}
</div>
</div>
)
}
完成这个功能,代码中并没有用到 useRef ,而是增加了一个 AllList 的状态,可是仔细想一下,AllList并没有参与渲染页面,真正渲染页面的是 list ,AllList仅仅是一个提供数据的数据源,完全没有必要单独给它创建一个状态,浪费性能,使用useRef改造一下
import { useState, useEffect, useRef } from 'react'
import styles from './index.module.css'
const TITLE = [
{ code: 0, text: '全部' },
{ code: 10, text: '类型一' },
{ code: 20, text: '类型二' }
]
// 模拟接口
const getListApi = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{ id: 1, type: 10, text: 'List1' },
{ id: 2, type: 10, text: 'List2' },
{ id: 3, type: 20, text: 'List3' },
{ id: 4, type: 20, text: 'List4' },
{ id: 5, type: 10, text: 'List5' }
])
}, 100)
})
}
export default function ComC () {
// tab 选中的索引
const [checkedIdx, setCheckIdx] = useState(0)
// 页面展示的 list
const [list, setList] = useState([])
// 接口返回的所有列表数据
const AllList = useRef([]) // 修改
useEffect(() => {
getList()
}, [])
// 接口请求,渲染列表
const getList = async () => {
const res = await getListApi()
setList(res)
AllList.current = res // 修改
}
// tab切换,试图更新
const onClick = idx => {
setCheckIdx(idx)
if (idx === 0) return setList(AllList.current) // 修改
const newList = AllList.current.filter(val => val.type === TITLE[idx].code) // 修改
setList(newList)
}
return (
<div className={styles.main}>
<div className={styles.tab}>
{TITLE.map((val, idx) => (
<div
onClick={() => {
onClick(idx)
}}
key={val.code}
className={idx === checkedIdx ? styles.checked : ''}
>
{val.text}
</div>
))}
</div>
<div className={styles.list}>
{list.length &&
list.map(val => (
<div className={styles.line} key={val.id}>
{val.text}
</div>
))}
</div>
</div>
)
}
useRef 小结
useRef 可以用来获取dom对象,其实 useRef 真正的的作用是保存可变值,其类似于在 class 中使用实例字段的方式,减少性能浪费
4. useMemo、useCallback
说起性能,合理使用 useMemo、useCallback 可以提高效率,减少性能浪费,先说 useMemo,我们给之前的点击自增1的组件加一个显示时间的功能,初始时间格式为 xx/xx/xx,显示的格式为 xx-xx-xx
import { useEffect, useState } from 'react'
import styles from './index.module.css'
export default function ComB (props) {
const [count, setCount] = useState(0)
const [date, setDate] = useState('')
useEffect(() => {
setDate('2021/03/31')
}, [])
function onClick () {
setCount(count + 1)
}
function formatDate (date) {
console.log('处理数据')
return date.replace(/\//g, '-')
}
return (
<div className={styles.main}>
<div className={styles.count}>{formatDate(date)}</div>
<div className={styles.count}>{count}</div>
<button className={styles.btn} onClick={onClick}>
+1
</button>
</div>
)
}
看起来没有问题,打印两次 ‘处理数据’ 一次是组件初始化,一次是首次 effect 副作用,这时候我们点击按钮看看会发生什么
我们发现每次点击 +1 按钮视图更新的时候同样也打印了 ‘处理数据’,其实我们并不需要每次更新视图的时候都处理一次时间,显然这样重复对同一个值多次计算很浪费,这时候就可以使用 useMemo 改造一下
import { useEffect, useState, useMemo } from 'react'
import styles from './index.module.css'
export default function ComB (props) {
const [count, setCount] = useState(0)
const [date, setDate] = useState('')
// useMemo 同样接收两个参数,第一个参数是一个函数 useMemo 的返回值就是这个回调函数的返回值,这个值会被缓存起来
// 第二个参数是一个数组,数组中每一项变化都会重新执行回调函数缓,没变化则不会多次计算
const formatDateMemo = useMemo(() => formatDate(date), [date])
useEffect(() => {
setDate('2021/03/31')
}, [])
function onClick () {
setCount(count + 1)
}
function formatDate (date) {
console.log('处理数据')
return date.replace(/\//g, '-')
}
return (
<div className={styles.main}>
<div className={styles.count}>{formatDateMemo}</div>
<div className={styles.count}>{count}</div>
<button className={styles.btn} onClick={onClick}>
+1
</button>
</div>
)
}
因为这个返回值被缓存起来了,所有这时候我们多次点击按钮就不会计算多次了,是不是和 vue2 中的计算属性(computed)很像
同样是这个功能,使用 useCallback 一样可以避免多次渲染
import React, { useEffect, useState, useCallback } from 'react'
import styles from './index.module.css'
export default function ComB (props) {
const [count, setCount] = useState(0)
const [date, setDate] = useState('')
// useCallback 接收两个参数,第一个参数是一个回调函数,useCallback 的返回值就是被缓存起来的这个回调函数
// 第二个参数同样是个数组,只有数组中某一项变化才会重新缓存
const formatDateCallback = useCallback(formatDate, [date])
useEffect(() => {
setDate('2021/03/31')
}, [])
function onClick () {
setCount(count + 1)
}
function formatDate () {
return date.replace(/\//g, '-')
}
return (
<div className={styles.main}>
<MyDate formatDate={formatDateCallback}></MyDate>
<div className={styles.count}>{count}</div>
<button className={styles.btn} onClick={onClick}>
+1
</button>
</div>
)
}
class MyDate extends React.PureComponent{
render () {
console.log('组件渲染')
return <div className={styles.count}>{this.props.formatDate()}</div>
}
}
同样多次点击更新视图时,由于传给纯组件 MyDate 的 formatDateCallback 函数是被缓存起来的,纯组件没有进行重新渲染
useMemo、useCallback 小结
- useMemo 缓存的是一个值,第一个参数(回调函数)的返回值就是这个值,第二个参数是个数组里面的每一项变化都会重新执行回调进行缓存
- useCallBack 缓存的是一个函数,第一个参数就是要缓存的函数,第二个参数是个数组,里面的每一项变化都会重新缓存这个函数
- useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
- 当我们代码中出现了逻辑复杂的计算,每一次执行成本都很昂贵,我们完全可以使用 useMemo 缓存下来计算结果,当所依赖的数据项变化时再重新计算,减少重复计算
- 这里建议不要频繁使用 useMemo、useCallBack ,可以在功能完成后使用它们进行优化,因为他们本身都是基于闭包实现的,同样占用性能, useState、useEffect、useRef 同样都有缓存的能力,在逻辑实现使用他们足够,只有当你真正需要 useMemo、useCallback 他们时,再斟酌使用
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!