首次渲染执行useState,获取初始值,之后重新渲染,获取的是新值,不是初始值(hooks有缓存);
这也是Hooks顺序不能变化,不能使用if语句包裹的原因;Hooks内部维护一个有单向链表,保证调用顺序,简单地移动一下指针,就可以调用下一个hook;
每个函数组件都有一个隐式的Hooks调用栈。
React 内部通过一个固定的调用顺序来管理 state 和其他副作用。如果 Hooks 可以在条件语句中使用,那么每次渲染时根据条件的不同,Hooks 的调用顺序可能会变化,这将导致 React 无法正确地追踪状态。
Hooks不能写在条件语句中的原因
为什么不维护一个键值对呢?而要维护单向链表;
遇到自定义hooks,一个组件里面多次调用自定义Hooks;此时通过 key 寻找 Hook state 的方式就会发生冲突。
1 useState
const user = { name: 'aa', age: 14 }
[user, setUser] = useState(user)
user.name = 'lisi';
setUser(user}
setUser(user}
无法更改数据,更新前后的user对象,原值和新值的引用相同,react认为值没有变化,不会重新渲染;
解决方案:setUser({...user})
,setUser(Object.assign({}, user))
- 强制重新渲染:利用引用不同
console.log({} === {}); // false
const [, forceUpdate] = useState({});
const onRefresh = () => forceUpdate({}); // 放置一个新的引用
<button onClick={onRefresh}>组件强制重新渲染</button>
2 useRef
解决问题:
- 获取DOM元素或子组件的实例对象;
- 存储渲染周期之间共享的数据;(比如:上次渲染的数据和本次渲染的数据)
注意:修改ref不会重新渲染,useEffect也不会监听到ref变化
const time = useRef(Date.now());
time.current = Date.now(); // 修改ref不会重新渲染;
console.log(time.current) // 但是打印结果会更新
useEffect(() => {
console.log('time值发生了变化', time.current)
}, [time.current]); // useEffect也不会监听到ref变化
const iptRef = useRef<HTMLInputElement>(null);
const getFocus = () => {
iptRef.current?.focus();
}
<input type="text" ref={iptRef} />
<button onClick={getFocus}>点击获取焦点</button>
组件第一次渲染会调用useRef、之后渲染不会调用useRef
const { count, setCount } = useState(0);
const preCountRef = useRef();
const add = () => {
setCount(pre => pre + 1);
preCountRef.current = count;
}
<h1>新值是{count},旧值是{preCountRef.current}</h1>
更改ref(time.curernt = ‘’),组件不会重新渲染;
useEffect无法监听到ref的变化,所以不会重新渲染;
useEffect(() => {
console.log('time的值发生变化');
}, [time.current]);
函数组件使用ref,没有办法获取到组件实例,可以利用 Reat.forwardRef;
3 useEffect
注意事项:
- 不在useEffect中改变依赖项的值,会造成死循环;
- 多个不同功能的副作用尽量分开声明,不要写到一个useEffect中;
useEffect(fn, 参数)
- 不加参数,每次渲染都会执行;
- 参数为空数组[],仅在首次渲染执行;(didMount)
- 参数为[count],仅在count更改时执行;(didUpdate)
- useEffect第一个参数fn中return的东西,是组件销毁时要执行的(一般在return中取消请求,取消定时器、延时器)
useEffect(() => {
const controller = new AbortController();
fetch('xxx', { signal: controller.signal })
.then(res => res.json())
.then(res => {
console.log('==>res', res);
});
return () => controller.abort(); // 取消请求
}, []);
4 自定义Hooks(抽离状态和状态更改方法)
自定义Hooks需要以Use开头将需要的数据和方法返回,也就是暴露出来;
比如设置一个计数器;自定义Hooks写好初始计数值、计数更改方法;
return { count, increment, decrement, reset };
export default useCounter;
在其他组件引入
import useCounter from './useCounter';
function Counter() {
const { count, increment, reset } = useCounter(10); // 初始值设为10
return (
<div>
<p>当前计数: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={reset}>重置</button>
</div>
);
}
export default Counter;
也可以设置一个倒计时组件;在自定义Hooks中的useEffect中进行倒计时,更改一次,调用一次setTime;这是其他组件接收到的time也会发生变化;
5 useLayoutEffect
- useLayoutEffect在浏览器重新绘制屏幕前触发;同步执行,阻塞浏览器重新绘制;
- useEffect在浏览器重新绘制屏幕之后触发;异步执行,不阻塞浏览器绘制;
useLayoutEffect可以解决 闪烁 问题;
useLayoutEffect(() => {
if (num === 0) {
setNum(Math.random() * 200);
}
});
<h1>num:{num}</h1>
<button onClick={() => setNum(0)}>重置为0</button>
使用useLayoutEffect,页面只出现一次变化;
使用useEffect,页面出现两次变化,一次是展示0;之后展示随机数;
6 useReducer(状态管理)
const [state, dispatch] = useReducer(reducer, initState, initAction?);
reducer
是一个函数 (prevState, action) => newState;prevState表示旧状态,action表示行为变更;state不可直接更改
,不会触发重新渲染
,只能通过reducer
修改;刚进入组件reducer函数不会执行(因为没有dispatch)initState对象
表示初始状态,也就是默认值;initAction函数
是进行状态初始化时的处理,将initState传递给initAction函数进行处理;- 返回值
state
是状态值,dispatch
是更新state的方法;
useReducer只需要调用dispatch(action),即可更新state(dispatch会触发reducer重新执行)
const initAction = (initState) => {
return { ...initState, age: Math.round(Math.abs(initState.age)) || 18 };
}
const reducer = (prevState, action) => {
switch (action.type) {
case 'UPDATE_NAME':
return { ...prevState, name: action.payload };
default:
return prevState;
}
}
dispatch({ type: 'UPDATE_NAME', payload: 'xxx' });
注意:
如果子组件、孙子组件都想使用dispatch
来实现状态变更;需要通过props将dispatch函数
传递下去;当然也可以使用useContext
来共享;
7 UseContext
const MyContext = React.createContext({});
<MyContext.Provider value={共享数据}>
<二级组件></二级组件>
</MyContext.Provider>
二级组件及其后代组件都可以使用MyContext;传递的value数据可以是状态(count
),也可以是状态变更函数(setCount
),也可以是dispatch
;
const ctx = useContext(MyContext);
这里可以简单做个封装
const AppContext = React.createContext({});
export const AppContextWrapper = (props) => {
const [count, setCount] = useState(0);
return <AppContextWrapper.Provider value={{count, setCount}}>
{props.children}
</AppContextWrapper.Provider>
}
<AppContextWrapper>
<子组件></子组件>
</AppContextWrapper>
8 React.memo
React.memo包裹函数组件,被包裹的组件,只有props变化了,才会被重新渲染;
9 useMemo
某个数据只依赖某个值的变化而变化,可以使用useMemo来进行优化;避免重新计算;结果返回的是值,不是函数。
const memoValue = useMemo(() => {
return 计算得到的值;
}, [value]);
// 不传数组,每次渲染都重新计算;
// 空数组,只计算一次;
// 依赖对应的值,对应的值发生变化时会重新执行
10 useCallback
为了防止每次重新render时,反复创建相同的函数,能够节省内存开销;
如果子组件接收的props里面有函数,则不会重新渲染。