React 组件渲染结果不外乎 props state 和生命周期对 props state 的副作用。Class 组件已经在业界存在多年,一直以来都有下面几个问题
- state 和 state mutaion 的逻辑不容易复用 (redux 是一种方式, 但 redux 也它的问题 …)
- 生命周期太多导致组件异常臃肿, componentDidXXXX 地狱; 且生命周期逻辑不好复用
- class 为了实现复用不得不搞出很多 HOC, 这个会造成组件嵌套地狱 wrapper hell
Hooks 始于 2018 年,在经过了严密的 issues 论证后正式确定规范并在 React Conf 2018 [1]可见 youtube 上的相关视频 中首次曝光
# Hooks 描述 Props ↵
函数式组件的 Props 比类组件的简洁,且在形式上更能说明 props + state 决定 render 的 React 哲学:
00import React from 'react'; 01 02export interface AppProps {} 03 04export function App(props: AppProps) { 05 return <div>hello, world</div> 06}
# Hooks 描述 State ↵
利用 useState 我们可以快速做到:
- 声明式风格的状态 React.useState
- useState 解构可以一口气拿到
[state, setState]
这一对 pair - state 及其 mutation 可以被抽象成独立个体达到复用的目的
下面是一个计数器例子
00import React from 'react'; 01 02export function Counter() { 03 const [count, setCount] = React.useState(); 04 return ( 05 <div onClick={() => setCount(count + 1)}>{count}</div> 06 ) 07}
针对第三点, 状态及其 mutaion 可以进一步抽象:
00import React from 'react'; 01 02export function useCounter(initCount: number) { 03 const [count, setCount] = React.useState(initCount); 04 // 自加 mutation 05 const inc = () => setCount(count + 1); 06 return [count, inc, setCount] as const; 07} 08 09export function EcznCounter() { 10 const [count, inc] = useCounter(10) 11 return <div onClick={inc}>EcznCounter {count}</div> 12} 13export function AppCounter() { 14 const [count, inc] = useCounter(0) 15 return <div onClick={inc}>AppCounter {count}</div> 16}
React State Hooks 原语相比 class 来说,最强大的地方就在于 Hooks 可以以函数抽象的形式抽象成独立的状态及其 mutaion,这是 class 很难搞的一点 (class 搞起来比较麻烦, 很绕)
# Hooks 描述 LifeCycle ↵
React 组件还有个大问题,就是组件的生命周期,在 class 组件里这个概念跟 DOM 强相关,需要用很多 componentDidXXX 来描述组件的生命周期,很容易让组件变得非常臃肿
此外这次在 A 里面用过的生命周期逻辑不能直接复用在 B 里,要做一些处理,比如包一层… 或者复制粘贴。
didxxx 的作用在于让组件能感知到外部环境的变化,进而作出 props 或者 state 的 mutation 以改变组件行为,比方在 didMount 的时候绑定一个 window.addEventListener,最后需要在 unmount 的时候移除这个 eventListener,在这个过程中事件是副作用,它影响组件状态,而 componentDidXXX 只是 React 开的一个钩子函数让你好监听这些事件。
后来你也知道了,DOM 还是比较复杂的,React 不得不开了很多生命周期钩子给组件来感知外部变化… 逐渐臃肿复杂化。
那 hooks 风格下的组件如何感知到外部环境变化, 是这样吗 ?
00import React from 'react'; 01export function App() { 02 React.useDidMount(() => { 03 console.log('dom mounted'); 04 }); 05 React.useUnMount(() => { 06 console.log('dom will unmount'); 07 }) 08}
这么看起来还可以啊 ? 但仔细想想这个本质上跟类的处理手段一样,提供众多钩子让组件监听外部变化 … 显然不可取,但不这样我们能处理生命周期吗?
看过文档的你肯定知道用的是 useEffect 来解决,但 useEffect 是怎么抽象地表述钩子和副作用的关系的呢 ? 看例子:
00import React from 'react' 01 02export interface AppProps {/* 略 */} 03 04export function App(props: AppProps) { 05 React.useEffect(() => { 06 const fn = () => {/* state mutaion */} 07 window.addEventListener('resize', fn); 08 return () => { 09 window.removeEventListener('resize', fn); 10 } 11 }, []); 12}
useEffect 做了什么 ?
- useEffect 第一个入参称为 effect 其返回值称为 cancelEffect;
- React 会在组件渲染完成后调用 effect, 并在下一次渲染前调用 cancelEffect; React 保证了每次运行 effect 的同时,DOM 都已经更新完毕 (某些场景下需要考虑到这个特性)
- 函数里面包函数真的没有问题吗? 借助这个闭包, 副作用里可以直接访问到 state props 等状态无需引入其他 react api; 担心内存泄漏? 记住一定要在 cancelEffect 里吧所有的引用拿掉
- 每次渲染完都会执行吗? 也不尽然,useEffect 还有第二个参数 deps, 这个参数用于指定依赖,只有这个列表的里面的元素出现 mutation 的时候才会调 effect
didMount & unMount
useEffect(fn, []) 可以实现上述这对生命周期的描述
didReceiveNextProps
useEffect(fn, [props]) 可以在 props 变化的时候执行,但还需要处理 prev props 和 next props 这两个入参:
00function usePrevious<T>(value: T) { 01 const ref = React.useRef<T>(); 02 React.useEffect(() => { 03 ref.current = value; 04 }); 05 return ref.current; 06} 07 08function App(props: {/* 略 */}) { 09 const prevPropsRef = usePreviousRef(props); 10 11 React.useEffect(() => { 12 const prevProps = prevPropsRef.current; 13 if (!prevProps) return console.log('第一次渲染还么有 prevProps'); 14 console.log('prevProps', prevProps); 15 console.log('nowProps', props); 16 }, [props]); 17 18 return <div>app</div> 19}
componentDidXxx
总之我自己遇到的生命周期都可以用 useEffect 来描述 (偶尔遇到不会的可以 stackoverflow 看大神操作)
总之 useEffect 比 componentDidXxx 来描述生命周期高明多了
# 也就是说… ↵
忘了那个它, 只用 hooks 来描述组件吧
Example
- 可见 youtube 上的相关视频 ↩︎