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个元素为修改该变量对应的函数名称。
使用步骤
- 导入
useState
函数 - 调用
useState
函数,传入初始值,返回状态
和修改状态的函数
- 使用
- 在 JSX 中展示
状态
- 特定的时机调用修改状态的函数来改状态
- 在 JSX 中展示
示例
// 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组件中也是如此 .... 简单一点说:
- 经过React 处理(事件回调,钩子函数)中的setState是
异步更新
- 没有经过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钩子的作用定义:
“勾住”某些组件挂载完成或重新渲染完成后才拥有的某些对象,并返回该对象的引用。该引用在组件整个生命周期中都固定不变,该引用并不会随着组件重新渲染而失效
怎么好像还是看不懂啊...
我们试着拆解开来理解这段话:
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'))
以上代码看起来没有问题,但当我们点击停止按钮的时候发现定时器并没有停止
原因分析:
每次定时器启动会修改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'))
使用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 ,后面有时间再更新 ....有不对的地方请指出,虚心请教...