在react 16.8版本中增加了 Hooks。
Hooks 是什么?
Hooks 直译 “钩子”,主要作用有如下几点:
- 对函数型组件进⾏增强
- 让函数型组件可以存储状态
- 可以拥有处理副作⽤的能⼒
让开发者在不使⽤类组件的情况下, 在函数组件中实现相同的功能。 React 官方是提倡使用函数组件,因此学习Hooks是很有必要性的
什么是副作用?只要不是把数据转换成视图的代码,它就是副作用。比如说:获取DOM元素,为DOM元素设置事件, 设置定时器, 发起ajax请求等等都属于副作用。在类组件里,经常通过生命周期函数来处理这些副作用
类组件的不足
- 缺少逻辑复用机制
为了复⽤逻辑增加⽆实际渲染效果的组件,增加了组件层级 显示⼗分臃 增加了调试的难度以及运⾏效率的降低
- 类组件经常会变得很复杂难以维护
将⼀组相⼲的业务逻辑拆分到了多个⽣命周期函数中,在⼀个⽣命周期函数内存在多个不相⼲的业务逻辑
- 类成员⽅法不能保证this指向的正确性
React Hooks 的使用
Hooks 意为钩⼦, React Hooks 就是⼀堆钩⼦函数, React 通过这些钩⼦函数对函数型组件进⾏增强, 不同的钩⼦函数提供了不同的功能.
- useState()
- useEffects()
- useReducer()
- useRef()
- useCallback()
- useContext()
- useMemo()
useState
它的使用以及和类组件的对比可访问官网有详细的说明和介绍,此处不做过多的介绍。
和类组件中使用方式对比, 使用函数组件的优点有:
- 代码更简洁,代码量更少了
- 可以不使用this
useState 的使用示例:
// 引入钩子函数 useState
import { useState } from "react";
function App() {
// 调用useState,为count添加初始值
// 返回 count的初始值,和 setCount 的函数用于更新
const [ count, setCount ] = useState(0)
return (
<div className="App">
<p> You clicked {count} times </p>
<button onClick = {() => setCount(count + 1)}>click me</button>
</div>
)
}
从上面的代码中我们可以得出以下两点:
-
接收唯⼀的参数即状态初始值. 初始值可以是任意数据类型.
-
返回值为数组. 数组中存储状态值和更改状态值的⽅法. ⽅法名称约定以set开头, 后⾯加上状态名称.
-
⽅法可以被调⽤多次. ⽤以保存不同状态值.
function App() {
// 调用useState,为count添加初始值
// 返回 count的初始值,和 setCount 的函数用于更新
const [ count, setCount ] = useState(0)
// 再次调用useState, 创建一个新的对象状态
const [ person, setPerson ] = useState({name: 'huan_zai', age: 18})
return (
<div className="App">
<p> You clicked {count} times </p>
<button onClick={() => setCount(count + 1)}>click me</button>
<p>UserName: {person.name} Age: {person.age}</p>
<button onClick={() => setPerson({name: 'linhuan', age: 18})} >change name</button>
</div>
)
}
- 参数可以是⼀个函数, 函数返回什么, 初始状态就是什么, 函数只会被调⽤⼀次, ⽤在初始值是动态值的情况.
const [ count, setCount ] = useState(() => 100)
获取动态值作为初始值
const propsCount = props.count || 0
const [ count, setCount ] = useState(propsCount)
虽然,上面的代码还是会实现效果,但是第一行代码,在每一次count 更新,组件重新渲染的时候,这一行代码都会被执行,这样做是没有意义的,因此我们可以给useState 传入一个函数作为参数,函数只会执行一次
const [ count, setCount ] = useState(() => {
return props.count || 0
})
关于设置状态值方法 setXXX 的两点细节:
- 设置状态值⽅法的参数可以是⼀个值也可以是⼀个函数
function handleCount() {
// 现在的setCount的参数是一函数
setCount(count => count + 1)
}
// 原先的setCount的参数是一个值
<button onClick={() => setCount(count + 1)}>click me</button>
// 现在的
<button onClick={handleCount}>click me</button>
- 设置状态值⽅法的⽅法本身是异步的
function handleCount() {
setCount(count => count + 1)
document.title = count
}
当点击更新count的值, 而 title的值还是上一次的旧的值,因此可见设置状态值⽅法的⽅法本身是异步的
优化一下功能,使title 和 count 同步
function handleCount() {
setCount(count => {
var newCount = count + 1
document.title = newCount
return newCount
})
}
手写 useState 的实现
根据上文中的useState 的使用,接下来一起来一步步实现它。
首先, 我们可以根据它的入参和返回值,来搭建一个基本的架子:
function useState(initialState) {
// 接收初始状态
let state = initialState
// 更新状态
let setState = newState => {
state = newState
// 重新渲染视图
render()
}
return [state, setState]
}
import ReactDOM from "react-dom";
function render() {
ReactDOM.render(<App />, document.getElementById('root'))
}
基本的架子搭好后,我们可以尝试的调用一下,看是否可行。
function App() {
const [count, setCount] = useState(0)
return (
<div className="App">
<p> You clicked {count} times </p>
<button onClick={() => setCount(count + 1)}>click me</button>
</div>
)
}
【结果】: 点击按钮, count 没有 增加还是保持0不变。
【分析】:我们从点击按钮开始的流程是, setCount -> setState -> render 引起组件的重新渲染 -> App() 重新调用 -> useState 会被重新调用 -> state 被重新赋值成 0
【解决】 将state的声明从原来useState内部提到外部去
let state = null
function useState(initialState) {
// 接收初始状态
state = state || initialState
// 更新状态
let setState = newState => {
state = newState
// 重新渲染视图
render()
}
return [state, setState]
}
到目前为止,只是实现了基本的单次的调用,那多次的调用,上面的代码就显得力不从心了,因此,接下来要对上面的代码进行完善,支持多次的调用。
我们对于useState 要被调用几次,根本是无法确定的,因此我们只能借助数组来存放所有的状态和对应的更新方法
// 存放状态
let state = []
// 存放状态对应的更新方法
let setters = []
// 通过索引将状态对应的更新方法对应起来
let stateIndex = 0
对索引创建一个产生闭包的的方法,保证了状态和其对应的更新方法能够准确地对应起来
function createSetter(index) {
return function (newState) {
state[index] = newState
render()
}
}
重新渲染后要将索引重置,否则索引一直累加会超出数组的范围
function render() {
// 重新渲染后要将索引重置,否则索引一直累加会超出数组的范围
stateIndex = 0
ReactDOM.render(<App />, document.getElementById('root'))
}
完善后的完整代码:
import './App.css';
import ReactDOM from "react-dom";
// 存放状态
let state = []
// 存放状态对应的更新方法
let setters = []
// 通过索引将状态对应的更新方法对应起来
let stateIndex = 0
function useState(initialState) {
state[stateIndex] = state[stateIndex] || initialState
setters.push(createSetter(stateIndex))
let value = state[stateIndex]
let setState = setters[stateIndex]
stateIndex++
return [value, setState]
}
function render() {
// 重新渲染后要将索引重置,否则索引一直累加会超出数组的范围
stateIndex = 0
// 清空数据,否则数组会重复变多
setters = []
ReactDOM.render(<App />, document.getElementById('root'))
}
function createSetter(index) {
return function (newState) {
state[index] = newState
render()
}
}
function App() {
const [count, setCount] = useState(0)
// const [person, setPerson] = useState({name: 'huan_zai', age: '18'})
const [name, setName] = useState('huan_zai')
return (
<div className="App">
<p> You clicked {count} times </p>
<button onClick={() => setCount(count + 1)}>click me</button>
<p>UserName: {name} </p>
<button onClick={() => setName('linhuan')} >change name</button>
</div>
)
}
export default App;
开始调试,于是打开界面
我们点击 按钮 change Name 会发现,点击一次按钮,文本userName 未改变,需要再次点击才会。尝试 点击按钮 chenge person 也是这样的。这是怎么回事呢?我通过控制台打印console.log(setters)和断点调试发现:
oh! my god! 这是我生平第一次见到这么奇怪的事儿! 打印结果的数组没有展开的时候是3项,展开后,里面竟然出现了 6项 !!!经过一番的调试,我就是发现App() 方法会被调用两次,出现这个的原因竟然是:
打开了严格模式, 严格模式下, App 方法会被调用两次,即渲染两次
useContext
作用: 在跨组件层级获取数据时简化获取数据的代码
useEffect
作用: 让函数型组件拥有处理副作用的能力, 类似类的生命周期函数
执行时机: 可以把useEffect 这个函数看作是componentDidMount, componentDidUpdate 和 componentWillUnmount这三个函数的组合
useEffect (() => {}) 类比 => componentDidMount, componentDidUpdate
useEffect (() => {}, []) 类比 => componentDidMount
useEffect (() => () => {}) 类比 => componentWillUnmount
案例:
import './App.css';
import { useEffect, useState } from "react";
import ReactDOM from "react-dom";
function App() {
function onscroll() {
console.log('页面发生滚动')
}
// 1. 为window对象添加滚动事件
// 挂载完成后添加事件,组件卸载前销毁事件
useEffect(() => {
window.addEventListener('scroll', onscroll )
return () => {
window.removeEventListener('scroll', onscroll)
}
}, [])
// 2. 设置一个定时器,让count数值每隔一秒加 1
const [count, setCount] = useState(0)
useEffect(() => {
const timeId = setInterval(() => {
setCount(count => {
document.title = count
return count + 1
})
}, 1000)
return () => {
clearInterval(timeId)
}
})
return (
<div className="App">
<span>{ count }</span>
<button onClick={() => ReactDOM.unmountComponentAtNode(document.getElementById('root'))}>卸载组件</button>
</div>
)
}
export default App;
useEffect解决的问题
-
按照用途将代码进行分类(将一组相干的逻辑归置到了同一个副作用函数中)
-
简化重复代码,使组件内部代码更加清晰。
useEffect (() => {}) 类比 => componentDidMount, componentDidUpdate, 原先的类组件要使用两个生命周期的API 才能完成挂载和更新, 在这里只要使用useEffect (() => {})
useEffect 的第二个参数
作用:
- 第二个参数是空数组, 表示副作用函数只会在挂载完成后执行一次, 此时不会监测更新
- 第二个参数指定了一个有值的数组, 表示只有指定数据发生变化时才会执行副作用函数
useEffect 结合异步函数
useEffect中的参数函数不能是异步函数, 因为useEffect函数要返回清理资源的函数, 如果是异步函数就变成了返回Promise
手写useEffect 实现
首先,来个基础版本的,主要实现功能:
- useEffect 只有一个参数(回调函数)的情况下, 即调用该回调函数
- useEffect 传入第二个参数时, 要监测其数据变化时才执行回调函数
// 保存上次的数据
let preArray = []
function useEffect(callback, depsArray) {
// callback 是否是函数
if(typeof callback !== 'function') {
throw new Error('first params must be function!!')
}
// 判断depsArray有没有被传递
if(typeof depsArray === 'undefined') {
callback()
} else {
// 判断是不是数组
if(Object.prototype.toString.call(depsArray) !== '[object Array]') throw new Error('second params must be Array!')
// 当前的依赖数组和上一次的进行对比,看是否发生变化
// 有变化调用回调函数
const hasChange = preArray.length === 0 || depsArray.every((dep, index) => dep === preArray[index]) === false
if(hasChange) {
callback()
}
// 同步依赖值
preArray = depsArray
}
}
测试一下
function App() {
const [count, setCount] = useState(0)
const [name, setName] = useState('huan_zai')
const [person, setPerson] = useState({name: 'huan_zai', age: '18'})
useEffect(() => {
console.log('hai')
}, [count])
return (
<div className="App">
<p> You clicked {count} times </p>
<button onClick={() => setCount(count + 1)}>click me</button>
<p>UserName: {name} </p>
<button onClick={() => setName('linhuan')} >change name</button>
<p>UserName: {person.name} Age: {person.age}</p>
<button onClick={() => setPerson({name: 'linhuan', age: '28'})} >change person</button>
</div>
)
}
结果, 在只有点击 click me 按钮时才会触发useState 的副作用函数, 符合预期结果。
但是,但我们想要对多个不同数据进行监测的时候, 上面的代码就无法实现
例如:
useEffect(() => {
console.log('hai')
}, [count])
useEffect(() => {
console.log('hai name')
}, [name])
结果:无论是哪个数据发生变化, useEffect 都会被执行, 这就有问题了,接下来我们需要优化下代码
优化后的代码:
// 保存上次的数据
let preDepsArray = []
let effectIndex = 0
function useEffect(callback, depsArray) {
// callback 是否是函数
if(typeof callback !== 'function') {
throw new Error('first params must be function!!')
}
// 判断depsArray有没有被传递
if(typeof depsArray === 'undefined') {
callback()
} else {
// 判断是不是数组
if(Object.prototype.toString.call(depsArray) !== '[object Array]') throw new Error('second params must be Array!')
// 当前的依赖数组和上一次的进行对比,看是否发生变化
// 有变化调用回调函数
const preDep = preDepsArray[effectIndex]
let hasChange = preDep ? depsArray.every((dep, index) => dep === preDep[index]) === false : false
if(hasChange) {
callback()
}
// 同步依赖值
preDepsArray[effectIndex] = depsArray
effectIndex++
}
}
手写useReducer 实现
function useReducer(reducer, initialState) {
const [ state, setState ] = useState(initialState)
function dispatch(action) {
const newState = reducer(state, action)
setState(newState)
}
return [state, dispatch]
}
function App() {
function reducer (state, action) {
switch (action.type) {
case 'increment':
return state + 1
case 'decrement':
return state - 1
default:
return state
}
}
const [count, dispatch] = useReducer(reducer, 0)
return (
<div className="App">
<p> {count} </p>
<button onClick={() => dispatch({type: 'increment'})}>increment</button>
<button onClick={() => dispatch({type: 'decrement'})}>decrement</button>
</div>
)
}