最近一年几乎都在使用 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 删除。