最近一年几乎都在使用 TypeScript + Hooks 编写函数式组件,这一篇是我使用 hooks 的一些总结。
开始之前,看一个经典的计数器例子:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>点击了 {count} 次</p>
<button onClick={() => setCount(count + 1)}>
Click
</button>
</div>
);
}
任意一次渲染中的
count
常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的count
值独立于其他渲染。 —— Dan Abramov
在React组件中,通过改变状态来触发组件的重新 render,每次渲染都可以理解为一帧。在每一帧中,状态只是一个普通的变量,render的时候,它都是独立不变的。
也就是说,在每次渲染中,所有的 state、props 以及 effects 在组件的任意位置都是固定的,我们无法直接获取过去或者未来渲染周期的状态。
state 变化,引发了视图的更新,从直觉上看来,这里是不是使用了数据绑定或者,观察者之类的高级技巧,实际上不是的,它只是函数的重复调用而已,count 是每次调用都独立的局部变量。
在react中,state或者props的改变,都会触发重新渲染。函数式组件以参数的形式接受props,props变化,整个组件都会重新渲染。useState
在函数式组件内部创建了状态,并提供了一个改变状态的方法。
const [count, setCount] = useState(0);
几个值得注意的点:useState的初始值可以是一个简单类型,也可以是复杂类型。同时它还可以接收一个函数,将函数的返回值作为该state的初始值。
const a = 1;
const b = 2;
const [count, setCount] = useState(() => a + b);
既然每一帧的渲染中,state 都是独立的,其实就会有一个问题,当我们执行完 setCount 之后,并不会立即拿到最新的 count 的值:
const [count, setCount] = useState(0);
setCount(count + 1);
console.log(count); // 0
也就是说,count 的值在本次渲染周期内是固定不变的,直到下一次渲染,count 才会更新为 1.这也是为什么感觉 state 的改变是异步的原因。
如果想要获取到最新的state值,则可以通过给setCount方法传入一个函数来执行。
还有一种方法就是使用 useRef
,它是一个所有帧共享的变量,你可以在任何时间改变它,然后在它未来的帧中访问它。也就是说,useRef可以为渲染视图的特定一帧打一个快照进行保存。
在实际使用中,一个组件可能会出现大量的 useState
定义,这个时候,我们需要回头反思,如此多的 state 定义是否有必要?
我们知道,react 状态的变化会引发视图的更新,所以将一个变量定义为 state 的标准是:它的改变需要直接引发视图的更新?如果答案是否定的,那就完全不必定义一个 state 出来,而是通过一般的变量将其缓存起来。或者说,使用 useRef
是一种不错的选择。
对于一些需要全局使用的状态,如果需要在多层组件之间传递更新数据,这很容易造成逻辑混乱且不可追踪,则可以通过 useContext 来简化这一过程,以避免显示地在每一层组件之间传递props,子组件可以在任何地方访问到该 context 的值。在下面的例子中,我们将终端的平台和版本通过context注入:
const client = {
mobile: {
system: 'android',
version: '8.0.0'
},
mac: {
system: 'MacOS',
version: '11.0.1'
}
}
const ClientContext = React.CreateContext({});
const App = () => {
return (
<ClientContext.Provider value={client.mac}>
<MyComponent />
</ClientContext.Provider>
)
}
const MyComponent = () => {
const client = useContext(ClientContext);
return <>当前系统为{client.system},系统版本为{client.version}</>
}
在某一个节点注入 context,该组件及其所有下属组件都会共享这个 context。当该 context 的值发生变化时,其下的所有组件都会重新 render.
useReducer
,是改变 state 的另一种方式。顾名思义,就是 reducer 的 hooks 用法。reducer 接受一个改变 state 的方法,以及触发方法的 action,计算之后,返回新的 state.类似于这样 (state, action) => newState
.useReducer
在某些复杂场景下比 useState
更实用,例如一个操作会引发N多个 state 的更新,或者说,state 本身嵌套很多层,更新的逻辑易遗漏,维护起来一片凌乱等等场景。
reducer 是一个纯函数,也就是说,它不包含任何 UI 和副作用操作。也就是说,只要输入的值不变,其输出的值也不会改变。
同样地,reducer 中的数据是 immutable 的,不要直接改变输入的 state,而是应该返回一个新的改变后的 state.
action 是一个用 type 标识的动作,例如对计数器的 increase、decrease等,在 reducer中,可以根据 action type 的不同,采用不同的数据处理。同时,它可以接收第二个参数 payload,传入执行该 action 需要的额外数据。
const [state, dispatch] = useReducer(reducer, initialArg, init);
useReducer
接收一个 reducer 函数,以及一个初始的 state 值,暴露出计算之后的新 state,以及一个 dispatch 方法,它接收一个 action 为参数,用来触发相应的 reducer. 下面使用 useReducer
重构计数器的例子:
const initialCount = {
count: 0
};
const countReducer = (state, action) => {
switch (action.type) {
case "increase":
return { count: state.count + 1 };
case "decrease":
return { count: state.count - 1 };
case "reset":
return { count: action.payload };
default:
return initialCount;
}
};
export default function App() {
const [state, dispatch] = useReducer(countReducer, initialCount);
return (
<div className="App">
<h1>{state.count}</h1>
<button onClick={() => dispatch({ type: "increase" })}>+1</button>
<button onClick={() => dispatch({ type: "decrease" })}>-1</button>
<button
onClick={() => dispatch({ type: "reset", payload: initialCount.count })}
>
reset
</button>
</div>
);
}
在某些复杂的场景中,reducer 的引入实际上将复杂的 state 更新行为剥离出来,单独在 reducer 之中维护,而组件的核心交互逻辑我们只需要关照 dispatch 了哪个 action,这样使得代码可读性大大提高,组件的核心逻辑也会清晰明了。同时,对于不涉及多层组件交互的状态,并不适合使用 reducer 来维护,这样,反而增加了维护的复杂度。
在一些复杂场景下,结合 useContext
和useReducer
可以发挥出十分强大的威力。一般的做法是,可以把 state 和 dispatch 方法通过 context 注入,这样,很方便地实现了状态的集中管理和更新。这种方法最好不要滥用,因为集中管理、处处可以变更的方式虽然看起来方便很多,但在 context 的作用范围处处都可以通过 dispatch 来更新 state,这样很容易造成 state 的更新不可追踪。
一般情况下,这种模式适合多层组件状态交互十分密集,且数据具有较好的完整性和共享需要,整个 state 描述的是同一件事,而不是把任意的数据都塞进去维护,这样写起来一时爽,维护起来火葬场~
useEffect
和useLayoutEffect
都是用来执行视图之外的副作用。前者在每次状态更新且视图也渲染完毕之后执行。后者则是在DOM更新完毕,但页面渲染之前执行,所以会阻塞页面渲染。
如前所述,在每一帧的渲染中,useEffect
中使用的 state 和 props 也是独立不变的。
可以通过给它传入第二个参数,一个依赖数组,来跳过不必要的副作用操作,React 通过对比依赖数组的值是否发生变化来决定是否执行副作用函数。当第二个参数为一个空数组的时候,意味着这个 Effects 只会执行一次。
对于依赖数组,使用不当经常会遇到各种各样的重复渲染的情况。不要添加不必要的依赖在数组中,因为依赖项越多,意味着该 Effects 被多次执行的概览越大。例如,在下面的场景中,如果需要在 Effects 中更新 state,不必将该 state 传入依赖数组,而是通过给 setCount 传入回调的方式去获得当前 state:
useEffect(() => {
// setCount(count + 1); 这种方式会引入一个 state 的依赖项。
setCount(count => count + 1);
}, [])
在React官方的文档中,还提到了两种需要避免重复渲染的情况及处理方式:
useCallback
来包裹函数避免函数反复被创建;useMemo
来缓存处理它。如上所述,合理地使用 useMemo
和useCallback
能够避免不必要的渲染。当对象或者数组作为 props 传入的时候,可以使用 useMemo
来缓存对象,使其在必要的时候更新:
const data = useMemo(() => ({ id}), [id]);
<ComponentA data={data} />
只要 id不变,Component 就不会重新渲染。
useMemo
同样可以用来缓存组件内部的部分 React Element ,以避免重复渲染并没有变化的部分。
使用 useMemo
或者 useCallback
并不是绝对会提升性能。如果你缓存的数据永远不会改变或者说,每一次都会改变,那大可不必使用这两个 hooks,毕竟它们需要额外的计算成本以及存储空间,有的时候得不偿失。
最后,在React哲学一文中,官方给出了一种使用 React 来构建应用的思路,我觉得十分赞。这篇文章中提到,开始的时候,找出应用所需的最小集合,其他数组均有它们计算而出。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有