参考答案

1 useState

1.1 类组件

在类组件中,可以用this.state来定义类组件的状态,可以看下以下代码实现

import React from 'react' class StateClass extends React.Component{ constructor(){ super() this.state = { name: '类' } } render() { return ( <div onClick={ this.setName }> 这是一个类组件————{ this.state.name } </div> ) } setName = () => { this.setState({ name: '我通过类组件方法变成这样了' }) } } export default StateClass

1.2 函数组件中

在函数组件中,可以使用useState来定义函数组件的状态。使用useState来创建状态

  • 1.引入
  • 2.接收一个参数作为初始值
  • 3.返回一个数组,第一个值为状态,第二个值为改变状态的函数

看一下同样的功能实现,函数组件的useState的代码实现如下。相同的功能,用函数组件的useState的写法是不是方便多了呢?接下来我们继续学习另外的hooks

import React,{ useState } from 'react' function StateFunction () { const [name, setName] = useState('函数') // 类名,修改函数名 初始值 return ( <div onClick={ () => setName('我使用hooks变成这样了') }> // setName也可以写入方法,如setName( val => val+'xxxx' ) 这是一个函数式组件————{name} </div> ) } export default StateFunction

2 useEffect

2.1 简单介绍

useEffect又称副作用hooks。作用:给没有生命周期的组件,添加结束渲染的信号。执行时机:在渲染结束之后执行

  • 什么是副作用?
    • 副作用 ( side effect ): 数据获取,数据订阅,以及手动更改 React 组件中的 DOM 都属于副作用
    • 因为我们渲染出的页面都是静态的,任何在其之后的操作都会对他产生影响,所以称之为副作用
  • 使用:
    • 1.第一个参数,接收一个函数作为参数
    • 2.第二个参数,接收【依赖列表】,只有依赖更新时,才会执行函数
    • 3.返回一个函数,先执行返回函数,再执行参数函数

2.2 不接受第二个参数的情况下

如果不接受第二个参数,那么在第一次渲染完成之后和每次更新渲染页面的时候,都会调用useEffect的回调函数,所以你要考虑好使用场景。

import React,{ useEffect, useState } from 'react' function StateFunction () { const [num, setNum] = useState(0) useEffect( () => { console.log('2222函数式组件结束渲染') }) return ( <div onClick={ () => setNum( num => num+1 ) }> 这是一个函数式组件————{num} </div> ) }

2.3 接受第二个参数的情况下

useEffect( () => { console.log('2222函数式组件结束渲染') },[]) // 改变useEffect第二个参数,其余代码同上

在这,我们可以对第二个参数传入一个数组,这个数组表示的是更新执行所依赖的列表,只有依赖列表改变时(当数组中的任意一项变化的时候,useEffect会被重新执行 ),才会触发回调函数

  • 传入的为空数组[],那么即告诉useEffect不依赖于stateprops中的任意值,useEffect就只会运行一次,常用场景为页面获取数据的方法可以写入此处进行调用,以获取页面初始数据
  • 传入一个值构建的数组、或者多个值构建的数组,如[num][num,val],上述代码变更为如下。那么此时只有当数组中的值(任意一项即可)改变时,才会重新触发回调函数
useEffect( () => { console.log('2222函数式组件结束渲染') },[num]) useEffect( () => { console.log('2222函数式组件结束渲染') },[num,val]) // 改变useEffect第二个参数,其余代码同上

2.4 清除副作用

上面写的都是一些不需要清除的副作用,只是回调触发一些简单的方法,但是有一些副作用是需要清除的。例如绑定一些DOM事件,在这种情况下,清除工作是非常重要的,可以防止引起内存泄露,例如下面给出的代码对比

  • 未清除副作用的情况下。此时第一次点击正常输出一次打印当前位置,而后每一次useEffect调用都会新绑定一个updateMouse方法,那么点击一次所触发绑定的方法越来越多,那么之后点击一次就会疯狂打印打印当前位置,这也就造成了页面性能、内存泄露等问题
const [positions,setPositions] = useState({ x:0,y:0 }) useEffect( () => { console.log('2222函数式组件结束渲染') const updateMouse = (e) => { console.log('打印当前位置') setPositions({ x:e.clientX, y:e.clientY }) } document.addEventListener('click',updateMouse) }) return ( <div> <p>x:{ positions.x }</p> <p>y:{ positions.y }</p> </div> )
  • 清除副作用的情况下(仅修改部分代码,其它代码同上)。例如示例代码
    • 首次刷新或进入页面会先执行除return以外的内容,也就是会执行一个绑定的方法,然后将updateMouse方法绑定到click事件上
    • 并将改次useEffect中的事件清除返回出去,但是此时是并没有执行return中的内容的(重点注意)
    • 然后当你点击第一次的时候,就会打印设置当前鼠标页面坐标,然后先执行上一次return返回出去的内容,注意这里是执行上一次return中的清除事件绑定方法,然后执行该清除事件绑定方法,当然清除也是清除的上一个useEffect中的绑定事件
    • 然后再开始执行新的useEffect中的绑定事件方法,并再次将改次useEffect清除事件绑定的方法return返回出去,如此就形成了一个链式一样的过程
    • 当页面卸载的时候,会执行最后一次return返回出来的清除事件绑定的方法,这样也就保证了页面卸载的时候,移除了绑定添加的DOM事件方法
    • (上述写的执行过程并没有从原理出发去分析的,只是简单的描述。可能稍微有点乱,如果你理解不了,可以多看几遍并动手执行示例代码结合进行理解)
useEffect( () => { console.log('2222函数式组件结束渲染') const updateMouse = (e) => { console.log('打印当前位置') setPositions({ x:e.clientX, y:e.clientY }) } document.addEventListener('click',updateMouse) // 添加绑定方法事件(要修改依赖,绑定到依赖上) return () => { // 在每次执行useEffect之前都会执行上一次return中内容 document.removeEventListener('click',updateMouse) // 移除绑定方法事件(要修改依赖,绑定到依赖上) console.log('1111销毁') } })

2.5 useEffect中的异步

每个effect函数都属于一次特定的渲染:

  • ①useEffect调度不会阻塞浏览器更新屏幕<异步>
  • ②每次重新渲染都会生成新的effect,替换掉之前的,确保effect中获取的值是最新的,不用担心过期。如下,设置的3000毫秒内连续点击三次,那么将会一共打印4次,分别是0、1、2、30是第一次渲染结束之后自动触发的,剩下1、2、3则是点击三次每次触发时的count
function Counter() { const [count, setCount] = useState(0); useEffect(() => { setTimeout(() => { console.log(`${count}`); }, 3000); }); return ( <div> <p>你点击了{count}</p> <button onClick={() => setCount(count + 1)}> 点击我 </button> </div> ); }
  • 与类组件进行对比:如果放到类组件中,则是打印设置的时间内改变的最终值,在类组件中同等代码如下(仅给出关键代码)。打印设置的时间内改变的最终值是什么意思呢?如果你设置3000毫秒,那么渲染结束的瞬间开始计时,3000毫秒内连续点击三次,那么最终就会打印4次3,如果先等第一次componentDidMount中设置的定时器结束,再突然3000毫秒内连续点击三次,那么就会先打印第一次的0,再打印三次3因为类写法中是共用的同一个num状态值。(如果你将时间设置为0毫秒,那么其实你连续点击三次,会跟useEffect一样,也是先打印一次0,再接着打印1、2、3,因为这个变化反应很快,你感觉不到差异,可以自己动手试试哦。)
this.state = { num:0 } componentDidMount(){ setTimeout(() => { console.log(this.state.num) },3000) } componentDidUpdate(){ setTimeout(() => { console.log(this.state.num) },3000) } render() { return ( <div onClick={ this.setNum }> 这是一个类组件————{ this.state.num } </div> ) } setNum = () => { this.setState({ num: this.state.num+1 }) }

3 useLayoutEffect

一般将useLayoutEffect称为有DOM操作的副作用hooks。作用是在DOM更新完成之后执行某个操作。执行时机:在DOM更新之后执行

useEffect对比

  • 相同点
    • 1.第一个参数,接收一个函数作为参数
    • 2.第二个参数,接收【依赖列表】,只有依赖更新时,才会执行函数
    • 3.返回一个函数,先执行返回函数,再执行参数函数
    • (所以说执行过程的流程是一样的)
  • 不同点
    • 执行时机不同。useLayoutEffectDOM更新之后执行;useEffectrender渲染结束后执行。执行示例代码会发现useLayoutEffect永远比useEffect先执行,这是因为DOM更新之后,渲染才结束或者渲染还会结束
const [num, setNum] = useState(0) //在类组件中用componentWillMount生命周期来实现 useLayoutEffect( () => { console.log('useLayoutEfffect') // 也可以在此进行事件绑定 return () => { // 也可以在此进行事件绑定移除 console.log(1) } },[num]) useEffect( () => { console.log('useEffect') },[num]) return ( <div onClick={ () => setNum( num => num+1 ) }> 这是一个函数式组件————{num} </div> )

4 useMemo

使用useMemo可以传递一个创建函数和依赖项,创建函数会需要返回一个值,只有在依赖项发生改变的时候,才会重新调用此函数,返回一个新的值。简单来说,作用是让组件中的函数跟随状态更新(即优化函数组件中的功能函数)。

  • 使用:
    • 1.接收一个函数作为参数
    • 2.同样接收第二个参数作为依赖列表(可以与useEffect、useLayoutEffect进行对比学习)
    • 3.返回的是一个值。返回值可以是任何,函数、对象等都可以

4.1 复杂计算逻辑优化使用场景

  • 未优化前代码如下。当我们点击div区域时,此时触发的setAge,改变的是age,跟getDoubleNum方法其实是不相关的,但是如果你看下控制台,能看到打印了多次获取双倍Num,说明该方法不断被触发,其实是没必要触发的。如果方法内计算量大、对性能是有一定影响的,所以需要进行优化
const [num, setNum] = useState(1) const [age, setAge] = useState(18) function getDoubleNum () { console.log(`获取双倍Num${num}`) return 2 * num // 假设为复杂计算逻辑 } return ( <div onClick={ () => { setAge( age => age+1 )} }> <br></br> 这是一个函数式组件————{ getDoubleNum() } <br></br> age的值为————{ age } <br></br> </div> )
  • 使用useMemo优化后代码如下。此时getDoubleNum方法是接收一个返回的值,所以要注意注释里所写的,括号是去掉了的。使用useMemo后,再点击div区域改变age的值,此时执行返回的return 2*num以及打印只有在num更新时才会去执行,然后返回值给到getDoubleNum再渲染到视图上,这样就减少了不必要的计算达到优化的作用
const [num, setNum] = useState(1) const [age, setAge] = useState(18) const getDoubleNum = useMemo( () => { console.log(`获取双倍Num${num}`) return 2 * num // 假设为复杂计算逻辑 },[num] ) return ( <div onClick={ () => { setAge( age => age+1 ) } }> <br></br> 这是一个函数式组件————num:{ getDoubleNum } // 注意这里没括号,因为是返回值 <br></br> age的值为————{ age } <br></br> </div> )

4.2 父子组件重复渲染问题优化使用场景

  • 未优化前代码如下。子组件包裹一个memo,但是包裹了还是会重新渲染, 为什么呢?因为我们定义的infoconst定义的一个局部变量,每次重新渲染都是重新定义一个新的info,然后子组件进行浅层比较时候,info永远是不一样的,所以就会重新渲染(可以按照例子点击按钮,会发现子组件不断打印我是子组件)。如果子组件比较复杂的情况下,那么就会对页面性能产生影响
const Child = memo( () => { console.log('我是子组件') return <p>我是子组件</p> }) function Parent() { const [show,setShow] = useState(true) const info = { name: 'Even', age: 22 } return( <div> <Child info={ info } /> <button onClick={ () => setShow(!show) }>点击更新状态</button> </div> ) }
  • 使用useMemo后代码如下(只给出修改代码,其它代码同上例子)。这样子优化后,子组件只会在初始化状态时渲染一次,当我们点击按钮时,因为info其包裹的useMemo依赖并没有改变,返回值是同一个值,所以不会造成子组件重新渲染。
const info = useMemo( () => { return { name: 'Even', age: 22 } },[])

5 useCallback

useMemo讲完我们来讲一个跟其很相似的叫useCallback,作用也是让某些操作、方法跟随状态的更新而去执行。

useMemo对比。

  • 可以简单这样看作,useMemo(() => Fn,deps)相当于useCallback(Fn,deps)

不同点:

  • useCallback是对传过来的回调函数优化,返回的是一个函数;useMemo返回值可以是任何,函数,对象等都可以

相同点:

  • 在使用方法上,useMemouseCallback相同。接收一个函数作为参数,也同样接收第二个参数作为依赖列表

5.1 为何说useCallback缓存的是一个函数(重要区别)

  • useCallback虽然与useMemo相似,但其返回及缓存的是一个函数,对比以下示例代码。先说①②③三种情况的对比(可以复制代码然后分别注释①②③代码对比)
    • 当①情况时,只会打印一次获取双倍Num1,也就是首次渲染的打印,之后再点击div区域改变age的值都与其无关,所以不会执行。因为getDoubleNum已经获得了useMemo中传入的函数执行后返回的值了,获取之后,便将其缓存下来了
    • 当②情况时,首次渲染会打印一次获取双倍Num1,然后每点击一次都会打印获取双倍Num1,这是为什么呢?不是说useCallback也有缓存的功能吗?这是因为我们前面提到的,useCallback返回的是一个函数。因为useCallback中的函数是在当前组件内定义的,组件重新渲染,它自然也会重新渲染,这又会有同学说了,可是这也不能说明它缓存的是一个函数啊。那么你可以先看看③的依赖为[]情况时,那么你就能明白了。所以说复杂计算逻辑的场景不适合使用useCallback来缓存,因为传入的函数内容会不断执行
    • 当③情况时,我们结合②③处标记代码,set只能存入唯一值,我们观察打印的set的长度
      * 当useCallback依赖为空[]时,我们连续多次点击div区域,虽然useCallback中的内容会不断执行,但是我们可以看到打印出来的set的长度一直都是2,这就是因为它不断将同一个函数添加进set,所以set的长度不变
      * 而当useCallback的依赖为[num]时,我们连续多次点击div区域,可以看到打印出来的set在不断累加,1、2、3、4、5、6...。因为num在改变,所以每一次缓存的函数都是一个新的函数,所以添加进set的函数是不一样的,所以set的长度点一次加一次
const set = new Set() export default function StateFunction () { const [num, setNum] = useState(1) const [age, setAge] = useState(18) const getDoubleNum = useMemo( () => { console.log(`获取双倍Num${num}`) return 2 * num // ①假设为复杂计算逻辑 },[] ) const getDoubleNum = useCallback( () => { console.log(`获取双倍Num${num}`) return 2 * num // ②假设为复杂计算逻辑 },[] ) set.add(getDoubleNum()) // ③注意set打印的长度变化(设置Callback的依赖为[]、[num]进行对比) console.log('set.size:',set.size) return ( <div onClick={ () => { setNum( num => num+1 ) } }> <br></br> 这是一个函数式组件————num:{ getDoubleNum } //①useMemo情况下 这是一个函数式组件————num:{ getDoubleNum() } //②useCallback情况下 <br></br> age的值为————{ age } <br></br> </div> ) }

5.2 useCallback适用场景

可以对父子组件传参渲染的问题进行优化。简单来说就是,父组件的传入函数不更新,就不会触发子组件的函数重新执行

  • 通常而言,父组件更新了,那么子组件也会更新。但是如果父组件传入子组件的内容不变,那么子组件某些操作(某些操作是指需要跟随传入内容的改变而同步进行的操作)是没必要执行的,这会影响页面性能,所以我们可以对这情况进行优化。
  • 例如示例代码,我们将getDoubleNum传入子组件,此时点击div区域改变的是num的值,我们使用父组件useCallback配合子组件的useEffect来优化,只有当父组件的num改变导致传入子组件的getDoubleNum改变的时候,我们才会执行子组件某些需要更新的操作(即注释标注处代码),这样就可以避免子组件一些没必要的更新操作反复执行而影响页面性能
function Parent () { const [num, setNum] = useState(1) const [age, setAge] = useState(18) const getDoubleNum = useCallback( () => { console.log(`获取双倍Num${num}`) return 2 * num },[num] ) return ( <div onClick={ () => {setNum( num => num+1 )} }> 这是一个函数式组件————num:{ getDoubleNum() } <br></br> age的值为————age:{ age } <br></br> set.size:{set.size} <Child callback={ getDoubleNum() }></Child> </div> ) } function Child(props) { useEffect( () => { console.log('callback更新了') //这里代表的是需要跟随传入内容的改变而同步进行的操作 },[props.callback]) return ( <div> 子组件的getDoubleNum{props.callback} </div> ) }

5.3 总结及参考详解文章

简单总结使用场景判断:

  • 在子组件不需要父组件的值和函数的情况下,只需要使用memo函数包裹子组件即可
  • 如果有函数传递给子组件,使用useCallback
  • 缓存一个组件内的复杂计算逻辑需要返回值时,使用useMemo
  • 如果有值传递给子组件,使用useMemo

6 useRef

简单来说useRef就是返回一个子元素索引,此索引在整个生命周期中保持不变。作用也就是:长久保存数据。注意事项,保存的对象发生改变,不通知。属性变更不会重新渲染

  • 未使用useRef,如果我们有这样一个需求如下,需要当某个定时器自增的值达到限制条件后就清除该定时器,如下代码。此时以下的代码其实是没有办法完成给出的需求的,当num大于10后,会发现不停的打印大于10了,清除定时器,而其实是定时器没有清除掉的,所以会一直执行这两个打印内容,但是会发现打印出来的timer显示undefined,这是为什么呢?因为我们每次渲染都是通过setInterval重新返回的timertimer也在更新,也就丢失了timer这个数据,导致无法准确清除某个需要清除的定时器
const [num, setNum] = useState(0) let timer useEffect( () => { timer = setInterval( () => { setNum( num => num+1 ) },400 ) },[] ) useEffect( () => { if(num > 10){ console.log('大于10了,清除定时器') console.log('timer:',timer) // 因为每一个timer都是独立render的,所以获取不到 clearTimeout(timer) } },[num] ) return ( <div> 这是一个函数式组件————num:{ num } </div> )
  • 使用useRef后,代码如下。我们可以看到num自增到11后就打印了一次大于10了,清除定时器以及ref.current 1,然后就停止自增了,因为定时器被清除了。ref是一个对象,ref.current存储了该定时器在整个生命周期中的id值,所以当清除定时器的时候,可以准确清除这个定时器
    • 保存一个值,在整个生命周期中维持不变
const [num, setNum] = useState(0) const ref = useRef() useEffect( () => { ref.current = setInterval( () => { setNum( num => num+1 ) },400 ) // ref.current = '111' },[] ) useEffect( () => { if(num > 10){ console.log('大于10了,清除定时器') console.log('ref.current',ref.current) clearTimeout(ref.current) } },[num] ) return ( <div> 这是一个函数式组件————num:{ num } </div> )
  • 重新赋值ref.current不会主动触发页面重新渲染。当我们将代码修改成下面这样,会在控制台打印发现ref.current的值打印为111,但是页面视图上显示的还是空,这是因为ref保存的对象发生改变,不会主动通知,属性变更不会重新渲染
const [num, setNum] = useState(0) const ref = useRef() useEffect( () => { ref.current = '111' console.log('ref.current',ref.current) },[] ) return ( <div> 这是ref.current的值——ref.current:{ ref.current } <br></br> 这是一个函数式组件————num:{ num } </div> )

7 useContext

useContext是让子组件之间共享父组件传入的状态的。作用通俗地说是带着子组件去流浪。

  • 未使用useContext,我们有下列这样一个场景,我们父组件有传入一个值到不同的子组件中,示例给出的代码是2个这样的子组件,但是如果我需要添加的子组件特别多呢?总不能总是一个一个这样添加写入吧,而且如果传入的同一个变量名如果发生改变,还得一个个去改,所以我们可以用useContext优化一下代码
function StateFunction () { const [num, setNum] = useState(1) return ( <div> <button onClick={ ()=> setNum(num => num+1) }>增加num的值+1</button> <br></br> 这是一个函数式组件——num:{ num } <Item1 num={num}></Item1> <Item2 num={num}></Item2> // ...... </div> ) } function Item1 (props) { return ( <div> 子组件1 num:{ props.num } </div> ) } function Item2 (props) { return ( <div> 子组件2 num:{ props.num } </div> ) }
  • 使用useContext优化后,代码如下,这样我们只需要在子组件中使用useContext(Context句柄)来获取数据即可,添加同类子组件时不需要再关注父组件中子组件定义时的props传入值,使用方法如下
    • 需要引入useContetxcreateContext两个内容
    • 通过createContext创建一个context句柄
    • Context.Provider来确定数据共享范围
    • 通过value来分发内容
    • 在子组件中,通过useContext(Context句柄)来获取数据
    • 注意事项,上层数据发生改变,肯定会触发重新渲染(点击button按钮触发父组件更新传入的num值能看到子组件重新渲染)
const Context = createContext(null) function StateFunction () { const [num, setNum] = useState(1) return ( <div> <button onClick={ ()=> setNum(num => num+1) }>增加num的值+1</button> <br></br> 这是一个函数式组件——num:{ num } <Context.Provider value={num}> <Item3></Item3> <Item4></Item4> </Context.Provider> </div> ) } function Item3 () { const num = useContext(Context) return ( <div> 子组件3: { num } </div> ) } function Item4 () { const num = useContext(Context) return ( <div> 子组件4: { num+2 } </div> ) }

8 useReducer

以前是只能在类组件中使用Redux,现在我们可以通过useReducer在函数式组件中使用Redux。作用是可以从状态管理的工具中获取到想要的状态。

  • 如何使用useReducerRedux必须要有的内容就是仓库store和管理者reducer。而useReducer也是一样的,需要创建数据仓库store和管理者reducer,即示例代码注释处。然后我们就可以通过处的定义一个数组获取状态和改变状态的动作,触发动作的时候需要传入type类型判断要触发reducer哪个动作,然后进行数据的修改。需要注意的地方是,在reducerreturn的对象中,需要将state解构,否则状态就剩下一个num值了
const store = { age:18, num:1 } // 数据仓库 const reducer = (state, action) => { switch(action.type){ case 'add': return { ...state, num: action.num+1 } default: return { ...state } } } // 管理者 function StateFunction () { const [state,dispacth] = useReducer(reducer,store) // ① return ( <div> <button onClick={ () => { dispacth({ type: 'add', num: state.num }) } }> 增加num的值+1 </button> <br></br> 这是一个函数式组件——num:{ state.num } </div> ) }