前言
为什么出现hooks
组件之间复用状态逻辑难
React没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。 rReact提供的解决此类问题的方案,比如 render props和高阶组件需要重新组织组件结构,使代码难以理解。 在React DevTools中观察会发现由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。
使用Hook从组件中提取状态逻辑,使这些逻辑可以单独测试并复用。可以在无需修改组件结构的情况下复用状态逻辑,在组件间或社区内共享Hook更便捷。
复杂组件难以理解
组件维护过程中逐渐被状态逻辑副作用充斥。
例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
Hook将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
API
-
useLayoutEffect 作用基本与
useEffect相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染 推荐先用useEffect,只有当它出问题的时候再尝试使用useLayoutEffect -
useReducer
管理包含多个子值得state对象 使用场景:
- state逻辑复杂 且包含多个子值
- 下一个state依赖其他的state
-
useCallback 函数只有在依赖项发生变化时才会更新 使用引用相等性避免渲染 useCallback(fn,[deps]) == useMemo(()=>fn ,[deps])
-
useMemo 「记住」上一次计算结果的方式在多次渲染的之间缓存计算结果
-
useImperativeHandle 让你在使用
ref时自定义暴露给父组件的实例值
useContext
返回一个memoized值。
接收context对象(React.createContext 的返回值)并返回该 context 的当前值
useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>。
const themes = {
light: { background: "#eeeeee"},
dark: { background: "#222222"}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
const theme = useContext(ThemeContext);
return (
<div>
<button style={{ background: theme.background }}>
I am styled by theme context!
</button>
</div>
);
}
调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,可以使用 memoization 来优化
useState
在函数调用时保存变量的方式,与 class 里面的 this.state 提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
1: import React, { useState } from 'react';
2:
3: function Example() {
4: const [count, setCount] = useState(0);
5:
6: return (
7: <div>
8: <p>You clicked {count} times</p>
9: <button onClick={() => setCount(count + 1)}>
10: Click me
11: </button>
12: </div>
13: );
14: }
如果更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过
函数式更新
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prev => prev + 1)}>+</button>
</>
);
}
惰性初始化
//传入函数 返回初始state,此函数只会在初始渲染时被调用
const [state, setState] = useState(() => {
...
...
return initialState;
});
useState 不会自动合并更新对象。 如何合并更新对象?
- 可以用函数式的
setState结合展开运算符来达到合并更新对象的效果。
setState(prevState => {
// 也可以使用 Object.assign
return {...prevState, ...updatedValues};
});
- useReducer管理包含多个子值得state对象
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
惰性初始化
function init(initialCount) { return {count: initialCount};}
function reducer(state, action) {
switch (action.type) {
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
useEffect
在函数组件中执行副作用,与class中的生命周期函数极为类似。 数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。
useEffect可看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
const [count,setcount]=useState(0)
useEffect(() => {
//
//
const subscription = props.source.subscribe();
return () => {
// 消除副作用(比如取消订阅)
// 清除函数执行时机 组件卸载前执行
subscription.unsubscribe();
};
},[依赖]);
函数主体
- 告诉组件在渲染后执行哪些操作,Dom更新后执行
- 改变 DOM、添加订阅、设置定时器、记录日志
- 在组件渲染到屏幕/某些值改变后执行
执行时机
- 默认
每次渲染/执行更新之后都会执行,不考虑“挂载”还是“更新”,effect发生在渲染之后。React 保证了每次运行effect的同时,DOM都已经更新完毕。 - 浏览器完成布局与绘制之后 新的渲染执行之前
- 如何控制执行时机
只有依赖数组内的值发生变化才会执行
effect中可获取最新的count的值,每次重新渲染都会生成新的effect,替换掉之前的。某种意义上讲,effect更像是渲染结果的一部分 —— 每个effect“属于”一次特定的渲染。
为什么每次更新都需要更新Effect 为什么effect的清除阶段在每次重渲染都会执行
清除函数
目的:消除副作用
- 上一次的effect会在重新渲染后被清除
- 在函数卸载前执行(防止内存泄漏)
- 组件多次渲染,则在执行下一个effect之前,上一个effect就已被清除
无需清除的副作用: 网络请求,手动变更DOM,记录日志
useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。
大多数情况下,effect 不需要同步地执行。
需要同步执行的情况下(例如测量布局),可使用useLayoutEffect
规范:
- 使用多个Effect实现关注点分离 将不同逻辑按用途分离
- 正确使用依赖 通过跳过effect实现性能优化--依赖项发生改变 effect才会执行
- 在effect内部声明所需要的函数(容易看出effect依赖了组件作用域中哪些值)
为什么
useLayoutEffect
useCallback
把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useMemo
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。
- 传入
useMemo的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作
useRef
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
useRef()和自建一个{current: ...}对象的唯一区别是,useRef会在每次渲染时返回同一个 ref 对象。- 当 ref 对象内容发生变化时,
useRef并不会通知你。- 变更
.current属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,需要使用回调 ref来实现。
获取 DOM 节点的位置或是大小的基本方式是使用callback ref,每当 ref 被附加到一个另一个节点,React 就会调用 callback
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
没有选择使用 useRef,因为当 ref 是一个对象时它并不会把当前 ref 的值的 变化 通知到我们。使用 callback ref 可以确保 即便子组件延迟显示被测量的节点 (比如为了响应一次点击),依然能够在父组件接收到相关的信息,以便更新测量结果。
useRef 与ref的区别
useImperativeHandle
让你在使用 ref 时自定义暴露给父组件的实例值
useImperativeHandle(ref, createHandle, [deps])