Hooks的基本使用
Hooks解决的问题
- 函数组件不能拥有自己的状态(state)。在hooks之前的函数组件是无状态的,都是通过props的来获取父组件的状态,但是hooks提供了useState来维护函数组件内部的状态。
- 函数组件中不能监听组件的生命周期,useEffect聚合了多个生命周期函数。
- class组件生命周期较为复杂(在15版本到16版本的变化大)
- class组件逻辑难以复用(HOC render props)
hooks对比class的好处
- 写法更加的简洁
class 组件
class ExampleOfClass extends Component {
constructor(props) {
super(props)
this.state = {
count: 1
}
}
handleClick = () => {
let { count } = this.state
this.setState({
count: count+1
})
}
render() {
const { count } = this.state
return (
<div>
<p>you click { count }</p>
<button onClick={this.handleClick}>点击</button>
</div>
)
}
}
hooks
function ExampleOfHooks() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<p>you click { count }</p>
<button onClick={handleClick}>点击</button>
</div>
)
}
- 业务代码更加聚合
使用class组件经常会出现一个功能出现在两个生命周期函数内的情况,这样分开写有时候可能会忘记。比如:
let timer = null
componentDidMount() {
timer = setInterval(() => {
// ...
}, 1000)
}
// ...
componentWillUnmount() {
if (timer) clearInterval(timer)
}
由于添加定时器和清除器是在两个不同的生命周期函数,中间可能会有很多其他的业务代码,所以可能清除定时器,如果在组件卸载时没有添加清除定时器的函数就可能会造成内存泄漏,网络一直在请求。
但是使用hooks可以让代码更加方便,方便我们管理,也不容易忘记。
useEffect(() => {
let timer = setInterval(() => {
// ...
}, 1000)
return () => {
if (timer) clearInterval(timer)
}
}, [//...])
- 逻辑复用方便
class组件在逻辑复用通常render props以及HOC两种方式.react hooks提供了自定义来复用逻辑。
下面以获取鼠标在页面的位置的逻辑复用为例。
class组件render props方式复用
import React, { Component } from 'react'
class MousePosition extends Component {
constructor(props) {
super(props)
this.state = {
x: 0,
y: 0
}
}
handleMouseMove = (e) => {
const { clientX, clientY } = e
this.setState({
x: clientX,
y: clientY
})
}
componentDidMount() {
document.addEventListener('mousemove', this.handleMouseMove)
}
componentWillUnmount() {
document.removeEventListener('mousemove', this.handleMouseMove)
}
render() {
const { children } = this.props
const { x, y } = this.state
return(
<div>
{
children({x, y})
}
</div>
)
}
}
// 使用
class Index extends Component {
constructor(props) {
super(props)
}
render() {
return (
<MousePosition>
{
({x, y}) => {
return (
<div>
<p>x:{x}, y: {y}</p>
</div>
)
}
}
</MousePosition>
)
}
}
export default Index
自定义hooks复用
import React, { useEffect, useState } from 'react'
function usePosition() {
const [x, setX] = useState(0)
const [y, setY] = useState(0)
const handleMouseMove = (e) => {
const { clientX, clientY } = e
setX(clientX)
setY(clientY)
}
useEffect(() => {
document.addEventListener('mousemove', handleMouseMove)
return () => {
document.removeEventListener('mousemove', handleMouseMove)
}
})
return [
{x, y}
]
}
// 使用
function Index() {
const [position] = usePosition()
return(
<div>
<p>x:{position.x},y:{position.y}</p>
</div>
)
}
export default Index
可以很明显的看出使用hooks对逻辑复用更加的方便,使用的时候逻辑也更加清晰。
hooks常见的一些API使用
- useState
语法
const [value, setValue] = useState(0)
这种语法方式是ES6的数据结构,数组的第一个值声明的状态,第二个值是状态改变函数。
每一帧都有独立的状态
个人理解针对每一帧独立状态是采取了闭包的方法来实现的。
function Example() {
const [val, setVal] = useState(0)
const timeoutFn = () => {
setTimeout(() => {
// 取得的值是点击按钮的状态,不是最新的状态
console.log(val)
}, 1000)
}
return (
<>
<p>{val}</p>
<button onClick={()=>setVal(val+1)}>+</button>
<button onClick={timeoutFn}>alertNumber</button>
</>
)
}
当组件的状态或者props更新是,该函数组件会被重新调用渲染,并且每一次的渲染是独立的都有自己独立的props以及state,不会影响其他的渲染。
useState初步实现
let lastState
function useState(initialState) {
lastState = lastState || initialState
function setState(newState) {
lastState = newState
render()
}
return [lastState, setState]
}
// 每次 render 的时候讲 state 的index 重置为 0
function render () {
index = 0
ReactDOM.render(<App />, document.getElementById('root'))
}
通过扇面的代码可以看出,state和index是强关联关系,因此,不能再if,while等判断条件下使用setSate,不能会导致state更新错乱。
- useEffect
语法
useEffect(() => {
//handler function...
return () => {
// clean side effect
}
}, [//dep...])
useEffect接收一个回调函数以及依赖项,当依赖项发生变化时才会执行里面的回调函数。useEffect类似于class组件的didMount、didUpdate、willUnmount的生命周期函数。
注意点
- useEffect是异步的在组件渲染完成后才会执行
- useEffect的回调数只能返回一个清除副作用的处理函数或者不返回
- 如果useEffect传入的依赖项是空数组那么useEffect内部的函数只会执行一次
实现useEffect
let lastDependencies
function useEffect(callback, dependencies) {
if (lastDependencies) {
let changed = !dependencies.every((item, index) => item == lastDependencies[index])
if (changed) {
callback()
lastDependencies = dependencies
}
} else {
// 首次渲染
callback()
lastDependencies = dependencies
}
}
- useMemo,useCallback
useMemo和useCallback主要用于减少组件的更新次数、优化组件性能的。
- useMemo接收一个回调函数以及依赖项,只有依赖项变化时才会重新执行回调函数。
- useCallback接收一个回调函数以及依赖项,并且返回该函数的memorize版本,只有在依赖项重新发生变化的时才会重新计算新的meorize版本
语法
const memoDate = useMemo(() => data, [//dep...])
const memoCb = useCallback(() => {//...}, [//dep...])
在优化组件性能是针对class组件我们一般使用React.PureComponent,PureComponent会在shouldUpdate进行一次比较,判断时候需要更新;针对函数组件我们一般使用React.memo。但是在使用react hooks时由于每一次渲染更新都是独立的(生成了新的状态),即使使用了React.memo,页还是重新渲染。
比如下面这种场景,改变子组件的name值后由于父组件更新后每次都会生成新值(addAge函数会改变),所以子组件也会重新渲染。
function Parent() {
const [name, setName] = useState('cc')
const [age, setAge] = useState(22)
const addAge = () => {
setAge(age + 1)
}
return (
<>
<p>父组件</p>
<input value={name} onChange={(e) => setName(e.target.value)} />
<p>age: {age}</p>
<p>-------------------------</p>
<Child addAge={addAge} />
</>
)
}
const Child = memo((props) => {
const { addAge } = props
console.log('child component update')
return (
<>
<p>子组件</p>
<button onClick={addAge}>click</button>
</>
)
})
使用useCallback优化
function Parent() {
const [name, setName] = useState('cc')
const [age, setAge] = useState(22)
const addAge = useCallback(() => {
setAge(age + 1)
}, [age])
return (
<>
<p>父组件</p>
<input value={name} onChange={(e) => setName(e.target.value)} />
<p>age: {age}</p>
<p>-------------------------</p>
<Child addAge={addAge} />
</>
)
}
const Child = memo((props) => {
const { addAge } = props
console.log('child component update')
return (
<>
<p>子组件</p>
<button onClick={addAge}>click</button>
</>
)
})
只有useCallback的依赖发生变化时,才会重新生成memorize函数。所以当改变name的状态是addAge不会变化。
实现useMemo,ueCallBack
主要功能:缓存变量值,因此返回的是一个函数的返回值
let lastMemo
let lastMemoDependencies
function useMemo(callback, dependencies) {
if (lastMemoDependencies) {
// 更新时渲染
// 判断依赖是否改变
let changed = !dependencies.every((item, index) => item == lastMemoDependencies[index])
if (changed) {
lastMemo = callback()
lastMemoDependencies = dependencies
}
} else {
// 初始化
lastMemo = callback()
lastMemoDependencies = dependencies
}
return lastMemo
}
实现useCallBack
主要功能: 缓存函数
let lastCallback
let lastCallbackDependencies
function useCallback(callback, dependencies) {
if (lastCallbackDependencies) {
// 更新时渲染
// 判断依赖是否改变
let changed = !dependencies.every((item, index) => item == lastCallbackDependencies[index])
if (changed) {
lastCallback = callback
lastCallbackDependencies = dependencies
}
} else {
// 初始化
lastCallback = callback
lastCallbackDependencies = dependencies
}
return lastCallback
}
- useRef
useRef类似于react.createRef。
const node = useRef(initRef)
useRef返回一个可变的ref对象器current属性被初始化为传入的参数(initRef)
作用在DOM上
const node = useRef(null)
<input ref={node}/>
这样的可以通过node.current属性访问到该DOM元素
需要注意的是useRef创建的对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的ref对象都是每一个(使用React.createRef,每次重新渲染组件都会重新创建ref)
useRef的实现
let lastRef
function useRef(initialRef) {
lastRef = lastRef || initialRef
return {
current: lastRef
}
}
- useReducer
useReducer类似于redux中的reducer
语法
const [state, dispatch] = useReducer(reducer, initState)
useReducer传入一个计算函数和初始化state,类似于redux,通过返回的state我们可以访问状态,通过dispatch可以针对状态作修改。
const initstate = 0;
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {number: state.number + 1};
case 'decrement':
return {number: state.number - 1};
default:
throw new Error();
}
}
function Counter(){
const [state, dispatch] = useReducer(reducer, initstate);
return (
<>
Count: {state.number}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
)
}
自定义useReducer
let lastState
function useReducer(reducer, initialState) {
lastState = lastState || initialState
function dispatch(action) {
lastState = reducer(lastState, action)
render()
}
return [lastState, dispatch]
}
- useContext
通过useContext我们可以更加方便的获取上层组件提供的context
父组件
import React, { createContext, Children } from 'react'
import Child from './child'
export const MyContext = createContext()
export default function Parent() {
return (
<div>
<p>Parent</p>
<MyContext.Provider value={{name: 'cc', age: 21}}>
<Child />
</MyContext.Provider>
</div>
)
}
子组件
import React, { useContext } from 'react'
import { MyContext } from './parent'
export default function Parent() {
const data = useContext(MyContext) // 获取父组件提供的context
console.log(data)
return (
<div>
<p>Child</p>
</div>
)
}
使用步骤
- 父组件创建并且导出
context:export const MyContext = createContext() - 父组件使用
provider和value提供值:<MyContext.provide value={{name: 'cc', age:'22'}}/> - 子组件导入父组件的
context: import {MyContext} from './parent' - 获取父组件提供的值:
const data = useContext(MyContext)
不过在多数情况下我们都不建议使用context,因为会增加组件的耦合性
hooks让函数组件拥有了内部状态,生命周期,使用hook让代码更加简洁,自定义hooks方便了对逻辑的复用,并且摆脱了class组件的this问题,但是在使用hooks时会产生一些闭包问题,需要仔细使用。