前言
目前团队中使用的都是ReactHooks写法,对于从来没接触过ReactHooks实战项目的小凌来说的确是不小的挑战。为了让更多新手能够快速入门ReactHooks,特此来写一篇ReactHooks的学习心路。
为什么要学ReactHooks?
Hooks的出现起初是为了让函数式组件(Function)也能支持状态的管理,但在学习过程中我认识到了很多Hooks的优点。
Function和Class的纠结: 函数式组件(Function)更高效。类组件(Class)有状态管理,方便后期拓展。学了Hooks之后我们不必在纠结。
繁琐的生命周期: 在刚学React的时候我们总需要清楚的记住一个组件的生命周期和执行时机。在学了Hooks之后我们可以完全抛开生命周期了。
this指向问题: React的this指向问题总是令人头疼,网上虽然了解决方案,但是额外的代码总让人觉得麻烦。在学了Hooks之后,我们再也不用考虑这个问题了。
更简洁的代码: 相比传统方法而言,实现一个功能Hooks方法所用的代码量将更少。
与传统实现对比(Example)
原始方法实现一个计数器
import React, { Component } from 'react';class Example extends Component {constructor(props) {super(props);this.state = {count:0}}render() { return ( <div> <div>点击我{this.state.count} 了次</div> <button onClick={()=>{this.addCount() }} >点击我</button> {/* <button onClick={this.addCount.bind(this)} >点击我</button> */} </div> );}// 计数器 addCount(){ this.setState({count: this.state.count + 1 }) }}export default Example;
使用Hooks实现一个计数器
import React, { useState } from 'react'function Example(){// 声明一个值 [ 数值, 设置函数 ] 0为初始值 (声明不能存在于条件判断中)const [ count, setCount ] = useState(0); return ( <div> <div>useState点击我{count} 了次</div> <button onClick={()=>{setCount( count + 1 )}} >点击我</button> </div> )}export default Example;
相比于传统方法Hooks的实现直接减少了三分之一的代码量。在代码上更简洁,也更容易维护。
接下来我们来介绍一下ReactHooks的主要模块以及用法。
useState(状态管理)
useState
使普通函数组件也有了状态管理的能力。其实我们在上方的代码里已经使用了useState,在这一模块里将详细介绍它的用法。我们从声明一个state开始:
左侧数组部分为ES6的解构赋值,在后续的讲解中我们将大量用到此类语法。
数组第1个参数为状态值
(用于控制页面的展示和逻辑处理),第2个参数是更改值的方法
。useState方法需要一个参数,这个参数就是状态值
的默认值。
注意点
不要在条件语句中声明hooks
✖错误案例
const [ name, setName ] = useState('小凌');const isShowAge = false;if(isShowAge){ const [ age, setAge ] = useState(26);}const [ hoppy, setHobby ] = useState('做菜');
控制台报错
Hooks渲染是从上到下依次执行,在if语句内使用的话,由于第一次未执行到useState,后面render时却又突然检测到了,就会导致控制台报错(可以理解成渲染时突然发现一个未知的useState,老版本不会报错,但是新版本将这个问题修复了所以控制台会报错)。
为什么我更改了对象state视图却没刷新?
当我们设置状态为数组或对象且只想改变其中一项属性时。
✖错误案例
const [ info, setInfo ] = useState({name:'小凌', age:26});const changeAge = () => { let setInfo = info; setInfo.age = 18 setInfo(setInfo)}return ( <div> <div>姓名:{info.name} 年龄:{info.age}</div> <button onClick={changeAge} >设置年龄</button> </div>)
发现视图并没有刷新,因为info的指针指向未变。
正确方法是使用解构赋值或是深浅拷贝的形式。
let setInfo = info;setInfo.age = 18setInfo({...setInfo})
useEffect(副作用)
useEffect主要用于监听状态值变化、在构建组件时进行监听、在销毁组件时进行监听。像极了类组件的
componentDidUpdate
、componentDidMount
、componentWillUnmount
生命周期函数钩子。
之前我也会用类组件的生命周期来类比Hooks的执行时机。后来发现这并不是一个很好的方法。首先这个方法根本没有办法准确类比,其次React18中新增了API,OffScreen可以对Hooks的执行时机造成影响。
Function Component 仅描述 UI 状态,React 会将其同步到 DOM,仅此而已。
关于参数
useEffect方法有两个参数,第1个参数是要执行的函数,第2个参数是一个依赖项数组(根据具体需要监听什么状态值来决定数组内要填写什么)。
参数详解
1、不传递
useEffect不传递第二个参数会导致每次渲染都会运行useEffect。
useEffect(() => { console.log('使用useEffect') // 所有更新都执行})
2、传递空数组
仅在挂载和卸载的时候执行,如果是多个就是其中某个更改时调用
useEffect(()=>{ console.log('使用useEffect')},[]) // 仅在挂载和卸载的时候执行
3、传递非空数组
在监听值更新时才会触发
useEffect(()=>{ console.log(count)},[count]) // count更新时执行
3、返回函数
在组件销毁时调用函数
const time = null;useEffect(() => { const time = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(time); // 这个方法在组件销毁的时候会被调用}, []);
useContext(跨组件通信)
useContext主要用于父子组件之间状态的跨级传递,实现了状态的共享(类似于Vue的Vuex)。
在认识useContext之前,与孙组件的状态传递是通过props。
我们可以看到,当层级一多参数传递就变得复杂。使用useContext后,父组件产生的state将直接被孙组件消费。
具体实现
父组件
export const CountContext = createContext()function Example(){ const [count,setCount] = useState(0) return ( <div> <div>useContext点击我{count} 了次</div> <button onClick={()=>{setCount( count + 1 )}} >点击我</button> <CountContext.Provider value={count}> <Counter /> </CountContext.Provider> </div> )}
子孙组件
import React, { useState } from 'react'function Example(){// 声明一个值 [ 数值, 设置函数 ] 0为初始值 (声明不能存在于条件判断中)const [ count, setCount ] = useState(0); return ( <div> <div>useState点击我{count} 了次</div> <button onClick={()=>{setCount( count + 1 )}} >点击我</button> </div> )}export default Example;0
注意点
新创建的DOM节点将在Provider之外。
当我们通过document.createElement创建Dom并将我们的内容挂载上去时,该节点将不会享有上下文的状态管理(也就是该节点在Provider之外)。
useMemo(状态缓存)
useMemo 是以缓存状态的形式来对渲染上进行性能优化的手段。
我们都知道只要状态更改了,那么组件视图就会从新渲染。
查看以下案例:
父组件代码
import React, { useState } from 'react'function Example(){// 声明一个值 [ 数值, 设置函数 ] 0为初始值 (声明不能存在于条件判断中)const [ count, setCount ] = useState(0); return ( <div> <div>useState点击我{count} 了次</div> <button onClick={()=>{setCount( count + 1 )}} >点击我</button> </div> )}export default Example;1
子组件代码
import React, { useState } from 'react'function Example(){// 声明一个值 [ 数值, 设置函数 ] 0为初始值 (声明不能存在于条件判断中)const [ count, setCount ] = useState(0); return ( <div> <div>useState点击我{count} 了次</div> <button onClick={()=>{setCount( count + 1 )}} >点击我</button> </div> )}export default Example;2
我们期望只有当name发生改变时候,才触发子组件的changeName方法。但当我们改变content时,也触发了changeName。因为父组件的重新渲染也重新渲染了子组件。
使用useMemo对状态进行缓存,只有改变的时候才触发相应方法。具体代码如下
优化之后的子组件
import React, { useState } from 'react'function Example(){// 声明一个值 [ 数值, 设置函数 ] 0为初始值 (声明不能存在于条件判断中)const [ count, setCount ] = useState(0); return ( <div> <div>useState点击我{count} 了次</div> <button onClick={()=>{setCount( count + 1 )}} >点击我</button> </div> )}export default Example;3
只在 React 函数中调用 Hook
import React, { useState } from 'react'function Example(){// 声明一个值 [ 数值, 设置函数 ] 0为初始值 (声明不能存在于条件判断中)const [ count, setCount ] = useState(0); return ( <div> <div>useState点击我{count} 了次</div> <button onClick={()=>{setCount( count + 1 )}} >点击我</button> </div> )}export default Example;4
当我们文件中有两个组件都需要一个context时,简化代码的思想促使我们感觉需要将它提取出来。
发现了如上报错。这是万万不可取的。在官网中我们也可以找到相应的提示。必须在React的函数组件中使用Hooks。
useCallback(方法缓存)
和之前学的useMemo一样,useCallback也是用来进行性能优化的,不同的是useCallback缓存的是方法。在小凌刚学Hooks的时候就写过几次死循环的代码。
大家先看如下代码:
import React, { useState } from 'react'function Example(){// 声明一个值 [ 数值, 设置函数 ] 0为初始值 (声明不能存在于条件判断中)const [ count, setCount ] = useState(0); return ( <div> <div>useState点击我{count} 了次</div> <button onClick={()=>{setCount( count + 1 )}} >点击我</button> </div> )}export default Example;5
我们使用setTimeout来模拟对后端进行请求。
在这种场景下,没有useCallback什么事,组件本身是高内聚的。如果涉及到组件通讯,情况就不一样了:
import React, { useState } from 'react'function Example(){// 声明一个值 [ 数值, 设置函数 ] 0为初始值 (声明不能存在于条件判断中)const [ count, setCount ] = useState(0); return ( <div> <div>useState点击我{count} 了次</div> <button onClick={()=>{setCount( count + 1 )}} >点击我</button> </div> )}export default Example;6
App
渲染Child
,将val
和getData
传进去
Child
使用useEffect
获取数据。因为对getData
有依赖,于是将其加入依赖列表
getData
执行时,调用setVal
,导致App
重新渲染
App
重新渲染时生成新的getData
方法,传给Child
Child
发现getData
的引用变了,又会执行getData
3 -> 5 是一个死循环
在我们明确异步方法只执行一次的情况下,我们可以使用useCallback对其进行固定。
import React, { useState } from 'react'function Example(){// 声明一个值 [ 数值, 设置函数 ] 0为初始值 (声明不能存在于条件判断中)const [ count, setCount ] = useState(0); return ( <div> <div>useState点击我{count} 了次</div> <button onClick={()=>{setCount( count + 1 )}} >点击我</button> </div> )}export default Example;7
数组内的参数和之前学的useeffect一样,是它的依赖项目。
useReducer
useReducer可以说是管理useState的集合。利用了Redux的理念,将多个state合并为了一个。
案列
import React, { useState } from 'react'function Example(){// 声明一个值 [ 数值, 设置函数 ] 0为初始值 (声明不能存在于条件判断中)const [ count, setCount ] = useState(0); return ( <div> <div>useState点击我{count} 了次</div> <button onClick={()=>{setCount( count + 1 )}} >点击我</button> </div> )}export default Example;8
实现Redux
结合我们之前所学的useContext们边可以实现一个简单的Redux。
案列如下
当我们点击AddColor组件时,为文字添加颜色。当我们点击AddBack组件时,为文字添加背景色。
核心Provider
import React, { useState } from 'react'function Example(){// 声明一个值 [ 数值, 设置函数 ] 0为初始值 (声明不能存在于条件判断中)const [ count, setCount ] = useState(0); return ( <div> <div>useState点击我{count} 了次</div> <button onClick={()=>{setCount( count + 1 )}} >点击我</button> </div> )}export default Example;9
useRef
useRef主要用于建立中间值或是用来调用子组件方法。
useRef返回一个可变的ref对象,其.current属性被初始化为传入的参数(initialValue
)。返回的ref对象在组件的整个生命周期内不变。
比如我们设定一个每次按一次+1的count属性,当count为奇数时隐藏。这个显示和隐藏不能单独作为一个state,这时候我们就可以用useRef去控制。
const [ name, setName ] = useState('小凌');const isShowAge = false;if(isShowAge){ const [ age, setAge ] = useState(26);}const [ hoppy, setHobby ] = useState('做菜');0
父组件调用子组件方法
父组件
const [ name, setName ] = useState('小凌');const isShowAge = false;if(isShowAge){ const [ age, setAge ] = useState(26);}const [ hoppy, setHobby ] = useState('做菜');1
子组件
const [ name, setName ] = useState('小凌');const isShowAge = false;if(isShowAge){ const [ age, setAge ] = useState(26);}const [ hoppy, setHobby ] = useState('做菜');2
如此我们就可以调用子组件方法了。
自定义Hooks
自定义Hooks在实现上也非常简单。你需要建立一个useXXX的函数。这个函数在形式上和普通函数没有区别,你
可以传递任意参数给这个Hooks。与普通函数的区别在于内部有没有实现其他Hooks。若内部没有实现其他Hooks,这个函数就不是自定义Hooks。
如下例子,我们声明一个组件。
给调用方开启组件方法、关闭组件方法、组件内容的参数。
其中,开启关闭方法都是设置组件自己的state。
声明自定义Hooks
const [ name, setName ] = useState('小凌');const isShowAge = false;if(isShowAge){ const [ age, setAge ] = useState(26);}const [ hoppy, setHobby ] = useState('做菜');3
使用自定义Hooks
const [ name, setName ] = useState('小凌');const isShowAge = false;if(isShowAge){ const [ age, setAge ] = useState(26);}const [ hoppy, setHobby ] = useState('做菜');4
至此,所有Hooks都过完啦。官网还提供了其他的Hooks。
参考文章与项目地址
《useCallback》
本文章代码地址:rh-project
原文:https://juejin.cn/post/7099045192962932750