React hooks详细总结

473 阅读12分钟

1.Hooks定义

Hooks:(原译)钩子、钓钩、钩住。 react中的hooks是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

作用: 为函数组件提供状态、生命周期等原本 在Class 组件中才提供的功能

  • Hooks 只能在函数组件中使用

  • 可以理解为通过 Hooks 为函数组件钩入 class 组件的特性

2.hooks解决类组件的问题

先说一下类组件的一些缺点

1.复杂且不容易理解的this,更多react事件及this解决方案请参考: react事件绑定以及this指向问题

例如事件绑定处理函数,都需要考虑正确的this指向才可以正确执行。
例如想获取某些自定义属性,都需要使用this.state.xx或this.props.xx,写起来比较繁琐

Hooks解决方式:函数组件和普通JS函数非常相似,在普通JS函数中定义的变量、方法都可以不使用“this.”,而直接使用该变量或函数,因此你不再需要去关心“this”了。 2.状态和逻辑难以复用

在生命周期函数中混杂不相干的逻辑(如:在 componentDidMount 中注册事件以及其他的逻辑,在 componentWillUnmount 中卸载事件,这样分散不集中的写法,很容易写出 bug )
类组件中到处都是对状态的访问和处理,导致组件难以拆分成更小的组件

Hooks解决方式:
1.通过自定义Hook,可以数据状态逻辑从组件中抽离出去,这样同一个Hook可以被多个组件使用,解决组件数据状态逻辑并不能重用的问题。
2.通过React内置的useEffect()函数,将componentDidMount、componentDidUpdate、componentWillUncount 个生命周期函数通过Hook(钩子)关联成1个处理函数,解决事件订阅分散在多个生命周期函数的问题

3.class 组件并没有发挥它最重要的功能

组件之间很少继承
组件之间很少相互访问

hooks解决问题:函数本身比较简单,更好的胜任根据状态来渲染UI这件事

3.hooks各钩子使用方式

1.useState

作用

为函数组件提供状态(state)
useState(value)函数会返回一个数组,该数组包含2个元素:第1个元素为我们定义的变量,第2个元素为修改该变量对应的函数名称。

使用步骤

  1. 导入 useState 函数
  2. 调用 useState 函数,传入初始值,返回状态修改状态的函数
  3. 使用
    1. 在 JSX 中展示状态
    2. 特定的时机调用修改状态的函数来改状态

示例

// 1.导入useState
import React, { useState } from 'react';
import ReactDom from 'react-dom'

export default  function Component() {

// 2.调用useState()传入初始值为0,得到count和修改count的函数setCount组成的数组
// 注意:在一个组件中,可以不限次数使用useState()
  const [count, setCount] = useState(0)
  const [age,setAge] = useState(20)
  
// click事件回调,每次点击count+1
  function clickHandler(){
    setCount(count+1);
    console.log(count)        //点击一次,这里输出得到 0
  }

  return <div onClick={clickHandler}>
 
    {count}    // 结构中借助jsx语法直接使用count
  </div>
}
ReactDom.render(<App />, document.getElementById('root'))

注意点!!!:

上述代码中,如果在事件回调中输出count , 得到的结果依然是0 , 这看起来是一个异步操作...

解释:
setState本身并不是一个异步(setTime, setInterval, ajax,Promise.then.....)方法,其之所以会表现出一种异步的形式,是因为react框架本身的性能优化机制

在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用 setState 不会同步更新 state,除此之外的setState调用会同步执行。class组件中也是如此 .... 简单一点说:

  1. 经过React 处理(事件回调,钩子函数)中的setState是异步更新
  2. 没有经过React处理(如通过 addEventListener || setTimeout/setInterval)中的setState是同步更新。 所以此时如果需要在事件回调中多次重复修改count,那么得到的最终结果是count只被修改了一次:
// click事件回调,每次点击count+1
  function clickHandler(){
    setCount(count+1)
    setCount(count+1)
    setCount(count+1)
    console.log(count)        //点击一次,这里输出得到 0  页面显示1 并不是3
  }

解决方法:

将setCount()参数写成函数形式

// click事件回调,每次点击count+1
  function clickHandler(){
   setCount( count => count + 1)
   setCount( count => count + 1)
   setCount( count => count + 1)
   console.log(count)        //点击一次,这里输出得到 0  页面显示3
  }

2.useEffect副作用

理解:

  • 主作用:就是根据数据(state/props)渲染 UI
  • 副作用:数据(Ajax)请求、手动修改 DOM、开启定时器,清空定时器,添加事件监听,删除事件, 对于react组件来说,除了渲染UI之外的其他操作,都可以称之为副作用。localStorage 操作等

useEffect作用:

在函数组件中模拟出类组件中的几个生命周期

componentDidMount(组件被挂载完成后)
componentDidUpdate(组件重新渲染完成后)
componentWillUnmount(组件即将被卸载前)

使用步骤

// 1. 导入useEffect
import { useEffect } from 'react'

// 2. 使用useEffect  在一个函数组件中useEffect可以使用多次
useEffect(() => {
	console.log('useEffect 1 执行了,可以做副作用')
})
useEffect(() => {
	console.log('useEffect 2 执行了,可以做副作用')
})

useEffect的参数

情况1
不带第二个参数。
执行时机:挂载完成后和每次更新之后都要执行
相当于生命周期componentDidMount(组件被挂载完成后) + componentDidUpdate(组件重新渲染完成后)

import React, { useEffect, useState } from 'react'
import ReactDOM from 'react-dom'
export default function App () {
  const [num, setNum] = useState(0)
  
  // 相当于类组件中使用compentDidMount 和compentDidUpdate
  useEffect(() => {
    console.log('给我一个div')  // 页面打开执行,之后每次num的值改变也会执行
  })
  
  return (
    <div>
      {num}
      <button onClick={() => setNum(num + 1)}>点击加1</button>
    </div>
  )
}
ReactDOM.render(<App />, document.getElementById('root'))

情况2
带第二个参数,参数是空数组。

执行时机:只在页面挂载完毕后执行第一次 , 相当于生命周期componentDidMount(组件被挂载完成后),

适用场景: ajax请求 , 事件绑定

  // 相当于类组件中使用compentDidMount
  
  useEffect(() => {
  
    console.log('给我一个div')  // 页面打开执行一次,之后每次num的值改变不会打印结果
    
  },[]) //此处第2个参数为[],告知React以后该组件任何更新引发的重新渲染都与此useEffect无关

情况3
带第二个参数(数组格式),并指定了依赖项。执行时机:
(1)初始执行一次
(2)任意一个依赖项的值发生改变都会执行

useEffect(() => { // 副作用函数的内容 }, [依赖项1,依赖项2,....])

import React, { useEffect, useState } from 'react'
import ReactDOM from 'react-dom'
export default function App () {

  const [num, setNum] = useState(0)
  const [num2, setNum2] = useState(0)
  
  // 依赖项num1和num2任意一个改变都会执行 console.log('给我一个div')
  useEffect(() => {
    console.log('给我一个div')  // 初始执行一次 ,依赖项该变执行
  }, [num, num2])

  return (
    <div>
      {num}{num2}
      <button onClick={() => setNum(num + 1) }>num加1</button>
      <button onClick={() => setNum2(num2 + 2) }>num2加2</button>

    </div>
  )
}
ReactDOM.render(<App />, document.getElementById('root'))

情况4

1.带第二个参数,参数是空数组。副作用处理函数返回一个函数,这个返回的函数可以称为副作用清理函数 , 用来清理副作用

2.执行机制:副作用清理函数会在当前组件销毁之前执行 , 相当于生命周期 componentWillUnmount(组件即将被卸载前)

3.适用场景:在当前组件离开前清理事件绑定以及定时器等操作

useEffect(() => {
  // 副作用函数的内容
  return ()=>{ /* 做清理工作*/ } // 清理函数
}, [])

4.示例: 事件绑定

import React, { useEffect, useState } from 'react'
import ReactDom from 'react-dom'

// Com1组件
function Com1 () {
  useEffect(() => {
  
  // mousemove事件回调
    const fn = (e) => {
      console.log(e.pageX, e.pageY)
    }
    window.addEventListener('mousemove', fn)

 // 副作用清理函数 解除事件绑定
    return () => {
      window.removeEventListener('mousemove', fn)
      console.log('组件删除了')
    }
  }, [])
  return <div>子组件</div>
}

// App组件
export default function App () {
  const [isShow, setIsShow] = useState(true)
  return (
    <div>
      {isShow && <Com1 />}
      
      // 点击按钮卸载Com1组件
      <button onClick={() => setIsShow(!isShow)}>切换</button>
    </div>
  )
}
ReactDom.render(<App />, document.getElementById('root'))

以上示例代码: Com1组件中如果没有清理副作用(清除mousemove事件) , 那么在Com1组件被销毁之后,事件仍可以执行,控制台依旧打印pageX, pageY

3.useRef

作用

“勾住”某些组件挂载完成或重新渲染完成后才拥有的某些对象,并返回该对象的引用。该引用在组件整个生命周期中都固定不变,该引用并不会随着组件重新渲染而失效。

上面的一段话不太容用理解 我们可以先简单理解成下面的形式:
简单理解:

返回一个带有 current 属性的可变对象,通过该对象就可以进行 DOM 操作了(也可以对引用组件操作)。也就是说通过useRef我们可以获取到真实DOM元素,对dom元素进行各种原生js的操作

使用步骤

//  1. 导入 useRef
import React, { useRef } from 'react'
import ReactDom from 'react-dom'

// Com1组件 
class Com1 extends React.Component {
  state = {
    a: 100
  }

  render () {
    return <div>com1, {this.state.a}</div>
  }
}

// App组件
export default function App () {

  // 2. 调用useRef(初值),得到引用对象,初始值一般设置为null
  
  // 3. 把引用对象设置ref 给任意的组件/元素
  
  // 4. 通过引用对象.current 获取 组件/元素
  
  const refTxt = useRef(null)
  const refCom1 = useRef(null)
  console.log(refTxt)

  const click = () => {
    console.log(refTxt, refCom1)  // 得到当前dom  {current: input}  {current: button}
    console.log(refCom1.current.state.a)  // 100
    
    // console.log(refTxt.current.value)
    // refTxt.current.style.backgroundColor = 'red'
  }
  return (
    <div>
      <input ref={refTxt} />{' '}
      <button onClick={click}>点击,获取input中的值</button>
      <br />
      <Com1 ref={refCom1} />
    </div>
  )
}
ReactDom.render(<App />, document.getElementById('root'))

通过以上代码应该就能基本理解useRef的作用了,他是用来获得真实DOM的引用,现在我们再来回到上面对useRef钩子的作用定义:

“勾住”某些组件挂载完成或重新渲染完成后才拥有的某些对象,并返回该对象的引用。该引用在组件整个生命周期中都固定不变,该引用并不会随着组件重新渲染而失效

怎么好像还是看不懂啊...

0BA0796A.png
我们试着拆解开来理解这段话:

1."勾住”某些组件挂载完成或重新渲染完成后才拥有的某些对象:

首先我们应该知道以下几点:
1、我们在JSX中所写的标签看似和原生html标签一样,但是并不是真的原生HTML标签,它们依然是react内置组件。经过编译之后才会转换成真实的DOM
2、那什么时候可访问真实dom?组件挂载完成或重新渲染完成后,我们就可以访问真实的DOM了

所以上面这段拆解所说的对象的引用就是挂载完毕之后的真实DOM

2.该引用在组件整个生命周期中都固定不变,该引用并不会随着组件重新渲染而失效
理解这句话我们先来看一个需求:
在页面挂载完成后开启一个定时器,每隔一秒让数字加1,点击按钮停止当前定时器

import React, { useEffect, useState } from 'react'
import ReactDom from 'react-dom'
export default function App () {
  const [count, setCount] = useState(0)
  
  let timeId = null
  
  // 页面挂载完毕开启定时器 每过一秒让count加1
  useEffect(() => {
    timeId = setInterval(() => {
      setCount((count) => count + 1)
    }, 1000)
  }, [])

// 点击按钮清除定时器
  const hClick = () => {
    clearInterval(timeId)
  }

  return (
    <div>
      count:{count}
      <button onClick={hClick}>点击停止定时器</button>
    </div>
  )
}
ReactDom.render(<App />, document.getElementById('root'))

以上代码看起来没有问题,但当我们点击停止按钮的时候发现定时器并没有停止

未命名-副本.gif
原因分析:

每次定时器启动会修改count的值,setCount会导致组件重新渲染,而重新渲染时,会重复执行如下代码
let timeId = null

所以点击按钮的时候并没有清除掉定时器,定时器在不停的被重置

用useRef在多次渲染时共享数据:

使用useRef钩子存储定时器

import React, { useEffect, useState, useRef } from 'react'
import ReactDom from 'react-dom'
export default function App () {

  const [count, setCount] = useState(0)
  const ref = useRef(null)
  
  useEffect(() => {
  //  使用useRef钩子来存储定时器
    ref.current = setInterval(() => {
      setCount((count) => count + 1)
    }, 1000)
  }, [])

 // 点击按钮清除定时器
  const hClick = () => {
    clearInterval(ref.current)
  }

  return (
    <div>
      count:{count}
      <button onClick={hClick}>点击停止定时器</button>
    </div>
  )
}
ReactDom.render(<App />, document.getElementById('root'))

未命名-副本(3).gif

使用useRef钩子来存储定时器,点击停止按钮,清除的是原来useRef钩子中存储的定时器,它没有随着页面的刷新而重置,这时应该就能理解这句话了:
该引用在组件整个生命周期中都固定不变,该引用并不会随着组件重新渲染而失效

额外的:useState钩子也有跟这个类似的特性:

const [count, setCount] = useState(0)
当count改变的时候页面重新渲染, useState并没有将count重置为0

备注:我也不知道这个动图为什么这么小而且不清晰,看到这篇博客的老铁有好的GIF制作软件可以推荐一下

4.useContext全局状态

作用:

我们知道,原本不同级别的组件之间传递属性值,必须逐层传递,即使中间层的组件不需要这些数据。 注意:这里说的组件指React所有组件,包含类组件和函数组件。

数据层层传递增加了组件的复杂性,降低了可复用性。为了解决这个问题,我们可以使用useContext全局状态钩子 使用步骤:

1.导入并调用createContext方法,得到Context对象,按需导出

import { createContext } from 'react'
export const Context = createContext()

2.使用 Provider 组件包裹根组件,并通过 value 属性提供要共享的数据

return (
  <Context.Provider value={ 这里放要传递的数据 }>
  	<根组件的内容/>
  </Provider>
)

3.在任意后代组件中,如果希望获取公共数据:

导入useContext;调用useContext(第一步中导出的context) 得到value的值

import React, { useContext } from 'react'
import { Context } from './index'    // 注意这里要按需导入

const 函数组件 = () => {
    const res = useContext(Context)   // res就是根组件传递的value
    return ( 函数组件的内容 )
}

暂时写这么几个hook ,后面有时间再更新 ....有不对的地方请指出,虚心请教...