1.为什么使用Hooks
副作用问题:诸如数据获取、订阅、定时执行任务、手动修改ReactDOM这些行为都可以称为副作用;而Hooks的出现可以使用useEffect来处理这些副作用
复杂的状态管理:之前通常使用redux、mobx这些第三方状态管理器来管理复杂的状态,而Hooks可以使用useReducer、useContext配合实现复杂的状态管理;
开发效率和质量问题:函数式组件比类组件简洁,效率高,性能也好。
2.useState
const [state, setState] = useState(initState)
2.1每次渲染都是独立的闭包
- 每次渲染都有自己的props和state
- 每次渲染都有自己的事件处理函数
- 组件函数每次渲染都会被调用,每一次调用中number的值都是常量,并且并赋予了当前渲染中的状态值
- 在单次渲染中,props和state始终不变
function Counter1() {
let [info, setInfo] = useState({ number: 0 });
const alertNumber = () => {
setTimeout(() => {
alert(info.number);
}, 3000);
}
return (
{info.number}
)
}
2.2函数式更新
如果新的state需要使用先前的state计算得出,可以将函数传递给setState, 该函数将接收先前的state值,并返回一个更新后的值
function Counter2() {
let [info, setInfo] = useState({ number: 0 });
function lazy() {
setTimeout(() => {
setInfo({ number: info.number + 1 })
}, 2000);
}
function lazyFunction() {
setTimeout(() => {
setInfo(info => ({ number: info.number + 1 }));
setInfo(info => ({ number: info.number + 1 }));
}, 3000);
}
return (
{info.number}
)
}
2.3惰性初始化
- initState参数只会在初始渲染中起作用,后续渲染会被忽略
- 如果初始state需要通过复杂计算获得,则可以传入一个函数,在函数中计算返回初始state, 此函数只在初始渲染时被调用
- 与class的setState方法不同,useState不会自动合并更新对象,可以用函数式的setState结合展开运算符达到合并对象的效果
function Counter3() {
let [info, setInfo] = useState(function () {
console.log('初始状态-----')
return { number: 0, name: "计数器" };
});
console.log("Counter5 render");
return (
{info.name}:{info.number}
);
}
3.useEffect
useEffect(callback, array)
:副作用处理的钩子;它也是componentDidMount()、componentDidUpdate()、componentWillUnmount()、
这几个生命周期方法的统一,一个顶三个!
- 第二个参数如果不写,只要状态改变都会执行。
- 第二个参数是个空数组时,不管哪个状态改变都不执行,只在组件初始时执行一次。
- 当第一个回调函数中有return返回值时,表示componentWIllUnmount时执行
4.性能优化
- 把回调函数和依赖项数组作为参数传入 useCallback, 将返回该回调函数的memoized版本,该回调函数仅在某个依赖项改变时才会更新。 (控制组件什么时候更新)
- 把创建函数和依赖项数组作为参数传入 useMemo 仅会在某个依赖项改变时才重新计算memoized值 这种优化有助于避免在每次渲染时都进行高开销的计算。 (控制组件是否更新)
let lastAddClick;
let lastChangeName;
function Counter4() {
let [number, setNumber] = useState(0);
let [name, setName] = useState('zhufeng');
//会在每次渲染的时候都 会生成一个新的函数
//只有在依赖的变量发生变化的时候才会重新生成
const addClick = useCallback(() => setNumber(number + 1), [number]);
console.log(lastAddClick === addClick);
lastAddClick = addClick;
const changeName = useCallback(() => setName(Date.now()), [name]);
console.log(lastChangeName === changeName);
lastChangeName = changeName;
return (
{name}:{number}
)
}
5.Hooks规则
自定义 Hook 必须以 “**use**
” 开头吗?必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。
- 只能在react的函数组件使用调用hook
- 需要在组件顶层调用,不能在判断啊,循环里调用,因为hook是按顺序执行的,加入放在判断里,第一个调用了,第二次没调用,后面的hook调用提前执行,会导致bug。
每一个 Hook 都有的两个相关函数:mountXxx()
和 updateXxx()
,它们分别是 Hook 在 Mount 阶段(即组件的挂载、或者说初始化阶段、又或者说是第一次执行 useXxx()的时候)和 Update阶段(即组件的更新、或者说组件重新渲染阶段)的逻辑。为了方便管理和调用,react 的工程师把 Hook 在 Mount 阶段的逻辑存到 (HooksDispatcherOnMount
) 对象中,把 Update 阶段的逻辑存到 (HooksDispatcherOnUpdate
) 对象中
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useResponder: createDeprecatedResponderListener,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useResponder: createDeprecatedResponderListener,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
};
Hook 在 Mount 阶段干了啥?
用 useState 为例。useState 在 Mount 阶段的逻辑写在 mountState()
方法中:
- 获取当前 Hook 节点,同时将当前 Hook 添加到 Hook 链表中
- 初始化 Hook 的状态,即读取初始 state 值
- 创建一个新的链表作为更新队列,用来存放更新操作(setXxx())
- 创建一个 dispatch 方法(即 useState 返回的数组的第二个参数:setXxx()),该方法的用途是用来修改 state,并将此更新操作添加到更新队列中,另外还会将该更新和当前正在渲染的 fiber 绑定起来
- 返回当前 state 和 修改 state 的方法(dispatch)
function mountState(
initialState: (() => S) | S,
): [S, Dispatch>] {
// 获取当前 Hook 节点,同时将当前 Hook 添加到 Hook 链表中
const hook = mountWorkInProgressHook();
// 初始化 Hook 的状态,即读取初始 state 值
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// 创建一个新的链表作为更新队列,用来存放更新(setXxx())
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
// 创建一个 dispatch 方法(即 useState 返回的数组的第二个参数:setXxx()),
// 该方法的作用是用来修改 state,并将此更新添加到更新队列中,另外还会将改更新和当前正在渲染的 fiber 绑定起来
const dispatch: Dispatch<
BasicStateAction,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
// 返回当前 state 和 修改 state 的方法
return [hook.memoizedState, dispatch];
}
存储 Hook 的数据结构——链表
一个函数组件中的所有 Hook 是以 链表 的形式存储的。链表中的每个节点就是一个 Hook
export type Hook = {
memoizedState: any, // Hook 自身维护的状态
...
queue: UpdateQueue | null, // Hook 自身维护的更新队列
next: Hook | null, // next 指向下一个 Hook
};
const [firstName, setFirstName] = useState('尼古拉斯');
const [lastName, setLastName] = useState('赵四');
useEffect(() => {})
useState 如何处理 state 更新
更新队列链表 queue
,用来存放更新操作,链表中的每一个节点就是一次更新 state 的操作(就是调用了一次 setXxx()),以便后面 Update 阶段可以拿到最新的 state。
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
pending
:最近一个等待执行的更新
dispatch
:更新 state 的方法(setXxx)
lastRenderedReducer
: 组件最近一次渲染时用的 reducer (useState 实际上是一个简化版的 useReducer,之所以用户在使用 useState 时不需要传入 reducer,是因为 useState 默认使用 react 官方写好的 reducer:basicStateReducer
)
lastRenderedState
:组件最近一次渲染的 state