React(八):引出Hook、useState、useEffect的使用详解

一、类组件的优劣势

1.类组件的优势

之前我们一直用类组件写demo,类组件相对于函数组件有什么优势?

  • 类组件可以保存组件自己的状态,但是在函数组件中,修改数据页面不会重新渲染,而且就算重新渲染,重新执行函数又重新赋值,等于没改。
import React, { PureComponent } from 'react'

// 函数组件
function HelloHooks() {
  // 就算函数重新执行, 又会重新赋值, 无意义
  let message = "Hello Hooks"

  return(
    <div>
      <h2>{message}</h2>
    </div>
  )
}

export class App extends PureComponent {
  render() {
    return (
      <div>
        <HellWorld/>
        <HelloHooks/>
      </div>
    )
  }
}

export default App
  • 类组件有自己的生命周期钩子,比如我们在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次。但是函数组件在哪里发请求呢?如果在函数中发请求,那么每次重新渲染又重新调用函数又重新发请求,这样反复发请求很沙雕。

class组件可以在状态改变时, 只重新执行render函数以及我们希望重新调用的生命周期函数(如componentDidUpdate)函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们内部的某段代码只执行一次。

所以在Hook之前通常都用类组件写项目。

2.类组件的劣势

(1)复杂组件会难以理解

随着业务的增多,我们的class组件会变得越来越复杂;

比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在 componentWillUnmount中移除);

而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;

(2)复杂的class

最显著的问题就是this的指向问题,如果this不处理好,可能会出现一些错误。

(3)组件复用状态很难

比如我们之前使用redux中的数据需要用connect这样的高阶组件,或者如果想实现什么功能还需要自己封装高阶组件给类组件注入一些东西,这还是非常费劲的。

二、Hook初体验useState

如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
Hook只能在函数组件中使用,不能在类组件或者函数组件之外的地方使用;

1.使用Hook的计数器案例

之前用类组件的写法我们都很熟知了:

import React, { PureComponent } from 'react'

export class Counter1 extends PureComponent {
  constructor() {
    super()

    this.state = {
      counter: 10
    }
  }

  changeNumber(num) {
    this.setState({
      counter: this.state.counter + num
    })
  }

  render() {
    const { counter } = this.state

    return (
      <div>
        <h2>当前计数: {counter}</h2>
        <button onClick={() => this.changeNumber(-1)}>-1</button>
        <button onClick={() => this.changeNumber(1)}>+1</button>
      </div>
    )
  }
}

export default Counter1

我们再看看用函数式组件,这个东西怎么写:

import React, { memo, useState } from 'react';

const Counter = memo(() => {
    let [counter, setCounter] = useState(99);
    return (
        <div>
            <h2>Counter:{counter}</h2>
            <button onClick={() => setCounter(counter+1)}>点击让Counter+1</button>
        </div>
    )
})

export default Counter

真的是非常的简洁,而且没有this的指向问题。

2.详解useState

(1)猜测它的原理

它与class组件里面的 this.state 提供的功能完全相同,就是用来保存状态(数据)的。

上面的代码点击按钮会发生两件事情:
1、调用setCounter修改counter
2、组件重新渲染(函数重新执行),根据新值返回DOM结构

一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。我盲猜这里用到了闭包:setCounter函数内部保存了state中的原始变量,等到setCounter函数被调用后原始变量才会被销毁,数据更改后再重新执行函数组件并把新值传过去,此时数据就有了最新的值。(当然只是盲猜)

(2)参数和返回值是什么?

useState只有一个参数: 接收一个初始化状态的值(设置初始值),在第一次组件被调用时使用来作为初始化值(如果不设置则默认为undefined)。

useState的返回值: 返回一个数组,数组包含两个元素:

  • 元素1:当前状态的值(第一次调用为初始值)
  • 元素2:修改状态的函数

一般我们会对数组进行解构(名字当然是自己取):

 let [counter, setCounter] = useState(99);

3.使用Hook的一些规则

通过上面的讲解,可以感受到hook顾名思义就是把要用的东西钩过来用一下子。

使用Hook的规则:

1、只能在函数组件的顶层调用 Hook。不能在循环语句、条件判断语句或者子函数中调用。
2、只能在 React 的函数组件和自定义hook中调用 Hook。不能在其他 JavaScript 函数中调用。

当然,我们可以定义更多类型的数据。
也可以把修改的操作单独封装一个函数。

import React, { memo, useState } from 'react';

const Counter = memo(() => {
    let [counter, setCounter] = useState(99);
    let [newsList, setNewsList] = useState(['体育','娱乐']);
    let [userInfo, setUserInfo] = useState({name:'zzy',age:18});

    function changeNum() {
        setCounter(counter - 1);
    }
    return (
        <div>
            <h2>Counter:{counter}</h2>
            <button onClick={() => setCounter(counter+1)}>点击让Counter+1</button>
            <button onClick={changeNum}>点击让Counter-1</button>
            <h3>{newsList}</h3>

            <h3>{userInfo.name}-{userInfo.age}</h3>
        </div>
    )
})

export default Counter

三、useEffect使用详解

在类组件中是可以有生命周期函数的, 那么如何在函数组件中定义类似于生命周期这些函数呢? 答案就是:useEffect钩子

顾名思义,其实我们平时的网络请求、手动更新DOM、一些事件的监听、订阅redux数据变化等操作,都是除了更新DOM之外需要做的操作,也就是一些副作用effect

1.基本使用方式和细节

比如一个这样的案例:修改counter时,实现页面标题和counter同步变化

类组件实现:

import React, { PureComponent } from 'react'

export class App extends PureComponent {
  constructor() {
    super()

    this.state = {
      counter: 100
    }
  }

  // 进入页面时, 标题显示counter
  componentDidMount() {
    document.title = this.state.counter
  }

  // 数据发生变化时, 让标题一起变化
  componentDidUpdate() {
    document.title = this.state.counter
  }

  render() {
    const { counter } = this.state

    return (
      <div>
        <h2>{counter}</h2>
        <button onClick={() => this.setState({counter: counter+1})}>+1</button>
      </div>
    )
  }
}

export default App

不难看出,类组件想要实现这个效果,需要写两个生命周期钩子,componentDidMount只有第一次挂载完成会执行,componentDidUpdate在每次更新结束后会执行。

那么函数组件怎么实现这个效果呢?答案是使用useEffect

import React, { memo, useEffect, useState } from 'react'

const App = memo(() => {
  const [counter, setCounter] = useState(200)

  // useEffect传入一个回调函数, 在页面渲染完成后自动执行
  useEffect(() => {
    // 一般在该回调函数在编写副作用的代码(网络请求, 操作DOM, 事件监听)
    document.title = counter
  })

  return (
    <div>
      <h2>{counter}</h2>
      <button onClick={() => setCounter(counter+1)}>+1</button>
    </div>
  )
})

export default App

useEffect传入一个回调函数, 这个回调函数在每次页面渲染完成后自动执行。也就是说,每次在函数式组件执行的顺序是:

执行函数组件 => 定义初始状态 => 渲染DOM => 执行useEffect中的回调

=> 修改数据 => 重新执行函数组件 => 更新状态 => 渲染最新DOM => 执行useEffect中的回调

不难看出,其实useEffect中的回调相当于完成了componentDidMountcomponentDidUpdate做的事情。

2.清除Effect(如事件监听、订阅)

在类组件的编写过程中,某些副作用的代码,我们需要在componentWillUnmount中进行清除,比如事件总线redux的数据订阅,那这在函数式组件中怎么做呢?

useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B

useEffect(() => {
    console.log('订阅了redux,绑定了某个事件A')
    return () => {
        console.log('取消订阅redux,解绑某个事件A')
    }
})

返回的这个回调函数执行的时机有两个:组件即将更新数据、组件即将卸载。

什么意思呢?如果我们在useEffect回调中绑定了某个事件,那么每次更新数据,每次都要渲染DOM,每次渲染DOM就会引起每次useEffect回调重新执行,这样的话相当于只要更新数据就反复绑定事件,嘚儿嘚儿嘚儿绑定一堆事件,这显然有点奥里给啊。

所以说我们可以协商这个返回的回调函数,每次更新数据前先执行回调解绑事件,再更新再绑定事件,也就是同样的事件每次只绑定一个就够啦。

3.可以使用多个useEffect

使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题:

比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount中;

一个函数组件中可以使用多个Effect Hook,我们可以将逻辑分离到不同的useEffect中:

import React, { memo } from 'react';
import { useState, useEffect } from 'react';

const Counter = memo(() => {
    let [counter, setCounter] = useState(99);
    useEffect(() => {
        //当前回调会在页面渲染完成后重新执行,每次都重新执行
        document.title = counter;
    })

    useEffect(() => {
        console.log('订阅了redux,绑定了某个事件A')
        return () => {
            console.log('取消订阅redux,解绑某个事件A')
        }
    })

    useEffect(() => {
        console.log('发送网络请求的逻辑')
    })
    
    return (
        <div>
            <h2>Counter:{counter}</h2>
            <button onClick={() => setCounter(counter + 1)}>点击让Counter+1</button>
        </div>
    )
})
export default Counter

React将按照 effect 声明的顺序依次调用组件中的每一个 effect;

4.决定useEffect的执行次数

默认情况下,useEffect的回调函数会在每次渲染都重新执行,但是某些代码我们只是希望执行一次即可(比如网络请求,绑定事件、订阅redux)。另外,多次执行也会导致一定的性能问题。

我们如何决定useEffect在什么时候应该执行和什么时候不应该执行呢?

useEffect实际上有两个参数:

  • 参数1:回调函数,上面已经提到了
  • 参数2:一个数组,表示该useEffect在哪些state发生变化时,才重新执行(受谁的影响才会重新执行)

我们来看下面这个案例,就明白了:

import React, { memo } from 'react';
import { useState, useEffect } from 'react';

const Counter = memo(() => {
    let [counter, setCounter] = useState(99);
    let [message, setMessage] = useState('DJ drop');
    useEffect(() => {
        //当前回调会在页面渲染完成后重新执行,每次都重新执行
        document.title = counter;
        console.log('counter的值变了,我瞅见了!')
    },[counter])

    useEffect(() => {
        console.log('message的值改变了!我瞅见了!')
    },[message])

    useEffect(() => {
        console.log('counter和message的值都变了!我瞅见了!')
    },[counter,message])
    return (
        <div>
            <h2>Counter:{counter}</h2>
            <button onClick={() => setCounter(counter + 1)}>点击让Counter+1</button>
            <button onClick={() => setMessage('the beat')}>改变message</button>
        </div>
    )
})
export default Counter

在上面这个案例中,页面第一次渲染每个useEffect都会先执行,然后第一个useEffect只有当counter的值改变时会重新调用,第二个useEffect只有当message的值改变时会重新调用,第三个useEffectcountermessage的值任意一个改变时会重新调用。

当然啊,如果要模拟生命周期如componentDidMount这种只在挂载时执行一次,那么第二个参数写个空数组[]就行了,表示我只调用第一次,后面任何数据的修改都不会引起我的重新调用。

import React, { memo } from 'react';
import { useState, useEffect } from 'react';


const Counter = memo(() => {
    let [counter, setCounter] = useState(99);

    useEffect(() => {
        console.log('订阅了redux,绑定了某个事件A')
        return () => {
            console.log('取消订阅redux,解绑某个事件A')
        }
    },[])

    useEffect(() => {
        console.log('发送网络请求的逻辑')
    },[])
    
    return (
        <div>
            <h2>Counter:{counter}</h2>
            <button onClick={() => setCounter(counter + 1)}>点击让Counter+1</button>
        </div>
    )
})
export default Counter

那么这样的话,其实回调中返回值的那个回调,也只有在组件即将销毁时调用,相当于componentWillUnmount了,非常奈斯。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值