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介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
 
                     
     
        
       
        
       
        
       
        
       
    
发表评论
还没有评论,快来抢沙发吧!