一、概念
Hooks 是 React v16.8 的新特性,可以在不使用类组件的情况下,使用 state 以及其他的 React 特性;
Hooks 是完全可选的,无需重写任何已有代码就可以在一些组件中尝试 Hook 于React v16.8发布,100%向后兼容,Hooks 不包含任何破坏性改动. React 也没有计划移除 class 类组件,而且 Hooks 不会影响对 React 的理解,它为已知的 React 概念提供了更直接的 API
二、Hooks解决的问题
- 函数式组件不能使用
state :函数式组件比类组件更简洁好用,而 Hooks 让它更加丰富强大; - 副作用问题:诸如数据获取、订阅、定时执行任务、手动修改
ReactDOM 这些行为都可以称为副作用;而 Hooks 的出现可以使用 useEffect 来处理这些副作用; - 有状态的逻辑重用组件
- 复杂的状态管理:之前通常使用
redux、dva、mobx 这些第三方状态管理器来管理复杂的状态,而 Hooks 可以使用 useReducer、useContext 配合实现复杂的状态管理; - 开发效率和质量问题:函数式组件比类组件简洁,效率高,性能也好。
useState:组件状态管理的钩子
import { useState } from 'react'
const [state, setState] = useState(initState)
import { useState } from 'react'
export default function App() {
const [count, setCount] = useState(0)
return (
<div>
<div>点击了{ count }次</div>
<button onClick={()=>setCount(count+1)}>点击</button>
</div>
)
}
useState 让函数式组件具备了管理状态的能力,不再是一个无状态组件;与 class 的 setState 类似,当向 useState 更新状态的方法 setCount 传递一个函数时,此函数会接收到上一次的状态:setCount(prevCount => prevCount + 1) 但与 class 的 setState 不同的是,如果状态值是一个对象,useState 更新状态的方法是不会合并更新对象的,可以结合展开运算符来达到合并更新对象的效果:
setState(prevState => {
return { ...prevState, ...updateValues };
});
useState 当然可以多次调用,从而定义多个状态值;但不管调用多少次,相互之间都是独立的,不会相互污染。这就是为什么 React 否掉了 Mixins ,因为 Mixins 机制让多个 Mixins 共享一个对象的数据空间,这样就很难保证不同 Mixins 依赖的状态不发生冲突;
function App() {
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}
React 又是如何保证多个useState 的相互独立呢,定义时并没有告诉React 这些值的key ,React 如何保证这三个useState 能准确找到它对应的state 呢 —> 根据useState 出现的顺序! 第一次渲染
useState(42);
useState('banana');
useState([{ text: 'Learn Hooks' }]);
第二次渲染
useState(42);
useState('banana');
useState([{ text: 'Learn Hooks' }]);
加入条件控制语句
let showFruit = true;
function App() {
const [age, setAge] = useState(42);
if(showFruit) {
const [fruit, setFruit] = useState('banana');
showFruit = false;
}
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}
这样一来,首次渲染的初始化过程是不变的,但第二次渲染就有所不同了 第二次渲染
useState(42);
useState([{ text: 'Learn Hooks' }]);
鉴于此,React 规定: Hooks 必须写在函数的最外层,不能在循环语句、条件判断、子函数中调用; 只能在 React 的函数式组件、自定义Hooks 中调用Hooks ,不能在其他JavaScript 函数中调用。
useEffect
useEffect (callback, array):副作用处理的钩子;它也是componentDidMount()、componentDidUpdate()、componentWillUnmount() 、这几个生命周期方法的统一,一个顶三个!React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect ,而生命周期钩子是同步执行的
callback 回调函数,作用是处理副作用的逻辑,可以返回一个函数,用作清理副作用;
import { useEffect } from 'react'
useEffect(() => {
......
return () => {
......
}
}, [])
为防止内存泄漏,清除函数会在组件卸载前执行;如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除; array 可选数组,用于控制useEffect 的执行; 省略时,每次渲染都会执行; 空数组时,只会在组件挂载/卸载时执行一次,类似componentDidMount 、componentWillUnmount ; 非空数组时,会在数组元素发生改变后执行,且这个变化的比较是浅比较。 提供第二个参数时,相当于告诉React 该组件不依赖于state/props 的变化,只依赖第二个参数的变化。
function App() {
const [count, setCount] = useState(0)
useEffect(()=>{
console.log('1')
return () => {
console.log('2')
}
}, [count])
return (<div>
<div>点击了{ count }次</div>
<button onClick={() => setCount(preCount => preCount+1)}>点击</button>
</div>)
}
父组件也可以通过 props 控制子组件是否执行 useEffect
useEffect(()=>{
const subscription = props.source.subscribe();
return ()=>{
subscription.unsubscribe();
}
}, [props.source]);
如前文所述,Hooks 可以反复多次使用,相互独立。所以合理的做法是,给每一个副作用加一个单独的useEffect 钩子。这样一来,这些副作用不再全堆在class 组件的生命周期钩子里,代码变得更加清晰;
useEffect 中定义的副作用函数在执行时不会阻碍浏览器更新视图,即这些函数是异步执行的,而class 组件中的生命周期钩子都是同步执行的;异步设计对大多数副作用是合理的,但也有特例,比如有时候需要先根据DOM 计算出某个元素的尺寸,然后再去渲染,此时则希望二次渲染是同步发生的,也就是在浏览器真的去绘制界面前发生; 为此,React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect 。它和 useEffect 的结构相同,区别只是调用时机不同 – useLayoutEffect 是同步的。
useContext
useContext() :同一个父组件的后台组件之间的全局数据共享; useContext() 接收React.createContext() 的返回值作为参数,即context 对象,并返回最近的context 。当最近的context 更新时,使用该context 的 Hooks 将会重新渲染;
创建一个Context 文件 InfoContext.js
const InfoContext = React.createContext({ name: 'Jerry', age: 18 })
export default InfoContext
父组件
import InfoContext from './context/InfoContext'
const Person = () => {
const ctx = useContext(InfoContext)
return(<InfoContext.Provider value={{ username: 'superman' }}>
<div>
<AgeCompt></AgeCompt>
</div>
</InfoContext.Provider>)
}
export default Person
某一层的后代组件
import { useContext } from 'react'
import InfoContext from './context/InfoContext'
const AgeCompt = () => {
const { username } = useContext(InfoContext)
return <p>{username}</p>
}
export default AgeCompt
useReducer
useReducer() :useState 的一个增强体,用于处理复杂的状态管理,灵感来源于Redux 的reducer useState 内部就是基于 useReducer 实现的,只是对于简单的状态管理,useState() 比较好用;
const [state, setState] = useState(initState)
const [state, dispatch] = useReducer(reducer, initState, initAction)
reducer – 一个函数,根据action 状态处理并更新state initState --初始化state initAction -- useReducer() 初次执行时被处理的action ,会把第二个参数initState 当作参数执行 state – 状态值 dispatch – 更新state 的方法,接收action 作为参数,当它被调用时,reducer 函数也会被调用,同时根据action 去更新state ,action 是一个描述操作的对象,如
dispatch({type: 'add'})
import { useReducer } from 'react';
const initState = { count: 0 };
const initAction = initState => { count: initState.count + 2 };
const reducer = (state, action) => {
switch(action.type) {
case 'ADD':
return { count: state.count+1 }
case 'DEL':
return { count: state.count-1 }
case 'RESET':
return initState
default:
return state
}
}
export default function UserCompt() {
const [state, dispatch] = useReducer(reducer, initState)
return (<div>
<p>{state.count}</p>
<div>
<button onClick={() => dispatch({ type: 'ADD', })}>增加</button>
<button onClick={() => dispatch({ type: 'DEL' })}>减少</button>
<button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
</div>
</div>)
}
更多Hooks
useMemo(callback, array):性能优化,利用了闭包的特性,通过记忆值来避免在每个渲染上都执行高开销的计算(计算缓存);适用于复杂的计算场景,如复杂的列表渲染,对象深拷贝… callback - 用于处理逻辑的函数 array - 依赖项,控制useMemo()重新执行的数组,array元素改变时才会重新执行useMemo() 返回值是一个记忆值,也是callback的返回值
import React, { useMemo } from 'react';
export default function UserCompt() {
const obj1 = { name: 'Tom', age: 15 }
const obj2 = { name: 'Jerry', age: 18, sex: '男' }
const memoValue = useMemo(() => Object.assign(obj1, obj2), [obj1, obj2])
return (<div>
<p>{ memoValue.name }</p>
<p>{ memoValue.age }</p>
</div>)
}
注意:不能在 useMemo() 中处理副作用逻辑,而是把副作用处理逻辑放在useEffect() useCallback(callback, array):也是用于性能优化,与useMemo()不同的是,返回值是callback本身;
const backValue = useCallback(() => Object.assign(obj1, obj2), [obj1, obj2])
<div>{ backValue().name } --- { backValue().age }</div>
当依赖项变化时,返回一个新的函数体callback。
useRef():创建ref,方便访问操作DOM
const RefCompt = () => {
const inputRef = useRef();
const getValue = () => {
const inpt = inputRef.current;
inpt.focus();
console.log(inpt.value);
};
return (<div>
<input ref={ inputRef } type="text" />
<button onClick={ getValue }>获取值</button>
</div>);
}
自定义Hooks
Hooks本质上就是封装好的勾子函数,在自定义Hooks时,最需要关心的就是性能、重复渲染这些问题; 自定义一个Effect Hooks,把可以复用的逻辑抽离出来,变成一个个可插拔的插销; 当标题变化时,则修改标题、否则不执行的Hooks
import { useEffect } from 'react'
const useChangeTitle = (title) => {
useEffect(() => {
document.title = title
}, [title])
}
export default (props) => {
useChangeTitle("自定义修改标题Hooks")
return <div>测试</div>
}
一个用来判断某个 id 是否在线的Hooks
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(()=>{
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
在组件中使用此Hooks
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
Hooks的使用规则
只在顶层调用Hooks Hooks的调用尽量只在顶层作用域 不要在循环、条件或嵌套函数中调用Hook,否则可能会无法确保每次组件渲染时都以相同的顺序调用Hook 只在函数组件调用Hooks:目前只支持函数式组件,未来版本Hooks会扩展到class类组件; React Hooks的应用场景:函数式组件、自定义Hooks
|