React在v16.8的版本中推出了React Hooks新特性。在我看来,使用React hooks相比于从前的类组件有一下几点好处
- 代码可读性更强,原本同一块功能的代码逻辑被拆分在不同的生命周期函数中,容易使开发者不利于维护和迭代,通过React Hooks可以将功能代码聚合,方便阅读维护
- 组件树层级变浅,在原本的代码中,我们经常使用HOC/render props等方式来复用组件的状态,增强功能等,无疑增加了组件树层级及渲染,而在React Hooks中,这些功能都可以通过强大的自定义的Hooks来实现
# State Hook
基础用法
function state() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> </div> ) }更新
更新分为以下两种方式,即直接更新和函数式更新,其应用场景的区分点在于:
- 直接更新不依赖于旧state的值
- 函数式更新依赖于旧state的值
// 直接更新 setState(newCount); // 函数式更新 setState(prevCount => prevCount - 1)实现合并
与class组件中的setState方法不同,useState不会自动合并更新对象,而是直接替换它。我们可以用函数式的setState结合展开运算符来达到合并更新对象的效果
setState(prevState => { // 也可以使用Object.assign return {...prevState, ...updatedValues} })惰性初始化state
initialState参数只会在组件的初始渲染中起作用,后续渲染会被忽略。其应用场景在于:创建初始state很昂贵时,例如需要通过复杂计算获取得;那么则可以传入一个函数,在函数中计算并返回初始的state,此函数只在初始渲染时被调用;
const [state, useState] = useState(() => { const initialState = someExpensiveComputation(props); return initialState; })一些重点
- 不像class中this.setState,Hooks更新state变量总是替换它而不是合并它
- 推荐使用多个state变量,而不是单个state变量,因为state的替换逻辑而不是合并逻辑,并且利于后续的相关state逻辑抽离
- 调用State Hook的更新函数并传入当前的state时,React将跳过子组件的渲染及effect的执行(React使用Object.is比较算法 (opens new window))
# Effect Hook
基础用法
function Effect() { const [count, setCount] = useState(0); useEffect(() => { console.log(`You clicked ${count} times`); }) return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ) }清除操作
为防止内存泄露,清除函数会在组件卸载前执行;如果组件多次渲染(通常如此),则在执行下一个effect之前,上一个effect就已被清除,即先执行上一个effect中return的函数,然后在执行本effect中非return的函数
useEffect(() => { const subscription = props.source.subscribe(); return () => { // 清楚订阅 subscription.unsubscribe(); } })执行时期
与componentDidMount与componentDidUpdate不同,使用useEffect调用的effect不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快;(componentDidMount或componentDidUpdate会阻塞浏览器更新屏幕)
性能优化
默认情况下,React会每次等待浏览器完成画面渲染之后延迟调用effect;但是如果某些特定值在两次重渲染之间没有发生变化,你可以通知React跳过对effect的调用,只需要传递数组作为useEffect的第二个可选参数即可:如下所示,如果count值两次渲染之间没有发生变化,那么第二次渲染后就会跳过effect的调用
useEffect(() => { document.title = `You clicked ${count} times` }, [count])模拟componentDidMount
如果想只运行一次的effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数,如下所示,原理跟第4点性能优化讲述的一样
useEffect(() => { // .... }, [])最佳实践
要记住effect外部的函数使用了哪些props和state很难,这也是为什么通常你会想在effect内部去声明它所需要的函数
// bad 不推荐 function Example(someProp) { function doSomething() { consolel.log(someProp) } useEffect(() => { doSomething(); }, []) // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`) } // good,推荐 function Example({ someProp }) { useEffect(() => { function doSomething() { console.log(someProp); } doSomething(); }, [someProp]); // ✅ 安全(我们的 effect 仅用到了 `someProp`) }如果处于某些原因你无法把一个函数移动到effect内部,还有一些其他办法
- 你可以把那个函数移动到你的组件之外。这样一来,这个函数肯定不会依赖任何props或state,并且也不用出现在依赖列表中了
- 万不得已情况下,你可以 把函数加入effect的依赖 把它的定义包裹进 useCallback Hook。这就确保它不随渲染而改变,除非它自身的依赖发生了变化
推荐启动eslint-plugin-react-hooks (opens new window)中exhaustive-deps (opens new window)规则,此规则会在添加错误依赖时发出警告并给出修复建议
// 1、安装插件 npm i eslint-plugin-react-hooks --save-dev // 2、eslint 配置 { "plugins": [ // ... "react-hooks" ], "rules": { // ... "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn" } }一些重点
- 可以把useEffect Hook看做componentDidMount,componentDidUpdate和componentWillUnmount这三个函数的组合
- 在React的class组件中,render函数是不应该有任何副作用的;一般来说,在这里执行操作太早了,我们基本上都希望在React更新DOM之后才执行我们的操作