React Hooks内部分享学习

本文探讨了React Hooks,特别是State Hook和Effect Hook的使用。它指出Hooks是React 16.8引入的新特性,旨在解决类组件的不足,如状态逻辑复用困难和生命周期函数混淆。文章通过实例解释了如何使用useState和useEffect,强调它们在简化组件代码、管理状态和处理副作用方面的优势。同时,也提到了最佳实践,如避免在条件语句中使用Hooks,以及如何优化useEffect以提高性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
(React-native 0.59,Taro 1.3开始已支持hooks)

类组件的不足

组件之间复用状态逻辑很难
  • 缺少复用机制
  • 渲染属性和高阶组件导致层级冗余
  • 趋向复杂难以维护
生命周期函数混杂不相干逻辑
  • 相干逻辑分散在不同生命周期
  • this 指向困扰

绑定this的几种方式

  • 在构造函数中绑定this(构造函数每一次渲染的时候只会执行一遍)
  • render()函数中绑定this (在每次render()的时候都会重新执行一遍函数)
  • 使用箭头函数 (每一次render()的时候,都会生成一个新的箭头函数,即使两个箭头函数的内容是一样的)

Hoosk优点

解决类组件的三个问题

  • 解决this指向问题
  • 加强逻辑复用性
  • 不同生命周期导致的复杂组件难以理解

使用State Hook

从一个例子开始

一个简单的有状态组件,点击后次数加一:

import React, {Component} from 'react'
class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
  }

  render() {
    return (
      <div>
        <p>Clicked {this.state.count} times</p>
        <button onClick={
            () => this.setState({ count: this.state.count +1})
        }>
          Click me
        </button>
      </div>
    );
  }
}

经过Hooks改造后:

import {useState} from 'react'
function App () {
    const [count,setCount] = useState(0)
    return (
        <div>
          <p>Clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
    )
}

可以看到,改造后的代码量简洁了很多,并且相比无状态组件多了个自己的state(count),并且可以更新自己的状态(setCount)。其中useState就是Hook中的一个,就是通过他使得函数内部拥有自己的state。这里有点要说明,正常来说不考虑闭包的情况下,函数中定义的变量在执行完后就会被销毁,但是上面的App却不是,每次都是拿的上一次执行完的状态值作为初始值,说明这个变量被React保留了,至于其中机制,就需要深入去理解了。(通过memorizedState去维护- 2019-12-25补充)

useState使用方式很简单,传入一个初始值state,返回一个数组,返回的数组包含当前state和一个更新state的函数。相比类组件的this.setState,这里有了setCountcount变量可以直接使用,所以说为什么Hook解决了this问题

组件有多个状态怎么办

useState是可以使用多次的,所以多个状态只需调用多次useState(),就像下面这样:

function manyState() {
    const [num, setNum] = useState(0)
    const [str, setStr] = useState('diego')
    const [bool, setBool] = useState(true)
    ....
}

其次,useState接收的初始值没有规定一定要是string/number/boolean这种简单数据类型,它完全可以接收对象或者数组作为参数。另外在类组件中,this.setState()是合并状态后返回,而useState是直接替换老状态返回新状态。

那么,如果组件内有多个usreState,那useState怎么知道哪一次调用返回哪一个state呢?

答案是,react是根据useState出现的顺序来定的(通过链表来实现)。如下:

function human() {
    //第一次渲染
    //将money初始化为0
    const [money, setMoney] = useState(0); 
    //将sex初始化为man
    const [sex,setSex] = useState('man');  
    //...
    const [info, setInfo] = useState([{ house: flse }]); 
    
    //第二次渲染
    //读取状态变量money的值(这时候传的参数0被忽略)
    const [money, setMoney] = useState(0);  
    //读取状态变量sex的值(这时候传的参数man被忽略)
    const [sex,setSex] = useState('man');  
    const [info, setInfo] = useState([{ house: flse }]); //...
}

修改以上代码

let showSex = true;
function human() {
  const [money, setMoney] = useState(0); 
  
  if(showSex) {
    const [sex,setSex] = useState('man');  
    showSex = false;
  }
 
  const [info, setInfo] = useState([{ house: false }]);
}

渲染结果如下:

//第一次渲染
//将money初始化为0
const [money, setMoney] = useState(0); 
//将sex初始化为man
const [sex,setSex] = useState('man');  
//...
const [info, setInfo] = useState([{ house: flse }]); 

//第二次渲染

//读取状态变量money的值(这时候传的参数0被忽略)
const [money, setMoney] = useState(0);  

// const [sex,setSex] = useState('man');

// 读取到状态变量Sex的值,报错
const [info, setInfo] = useState([{ house: flse }]); 

react规定我们必须把hooks写在函数的最外层,不能写在ifelse等条件语句当中,来确保hooks的执行顺序一致。并且为了防止我们使用useState不当,React提供了一个ESlint 插件帮助我们检查。

优化点

useState有个默认值,因为是默认值,所以在不同的渲染周期去传入不同的值是没有意义的,如下面代码,只有第一次传入的才有效:

...
const defaultValue = props.value || 0
const [value, setValue] = useState(defaultValue)
...

state的默认值是基于props,在组件每次渲染的时候const defaultValue = props.value || 0都会运行一次,如果它复杂度比较高的话,那么浪费的性能肯定是较大的。

useState()支持传入函数,来延迟初始化,就像setState回调那样:

...
const [value, setValue] = useState(() => {
    return props.value || 0
}) 
    ...

使用Effect Hook

使用过类组件的都知道,它提供了一套生命周期用于在不同情况下进行对应的操作,比如发送网络请求(componentDidMount),更新数据(componentDidUpdate),组件销毁(componentWillUnmount)等,这些在react中被统称作 ‘副作用’,同时也会遇到类似以下的情况,在最开始的例子中加入一个功能,每次点击后更新页面的title:

  ...
  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }
  ...

可以看到需要在两个生命周期函数中编写重复的代码,即使提取了公共方法,也需要在两地地方去调用,这个时候怎么办呢?于是Effect Hook应用而生,作为componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的集合,可以使用它在函数组件中解决副作用操作。同样以上面那个例子,经过改造后的代码如下

...
useEffect(() => {
    document.title = `You clicked ${count} times`;
});
...
  • useEffect做了什么?

    useEffect标准上是在组件每次渲染之后调用,并且会根据自定义状态来决定是否调用还是不调用。
    第一次调用就相当于componentDidMount,后面的调用相当于 componentDidUpdate

  • useEffect怎么解绑一些副作用

    useEffect还可以返回另一个回调函数,这个函数的执行时机很重要。作用是清除上一次副作用遗留下来的状态。

  • useEffect使用时机

    按照上面的说法,每次渲染都要执行这些副作用函数,如果只是这样那明显是不合理啊?好在useEffect提供了第二个参数,第二个参数是一个可选的数组参数,只有数组的每一项都不变的情况下,useEffect才不会执行。第一次渲染之后,useEffect 肯定会执行。如果我们传入的空数组,空数组与空数组是相同的,这样一来useEffect只会在第一次执行一次。

按照以上思路再次对例子进行改造:

...
useEffect(() => {
    document.title = `You clicked ${count} times`;
}, [count]); //只有当count的值发生变化时,才会重新执行`document.title`这一句
...
useEffect实际应用
function initVal() {
  const arr = []
  for (let i = 0; i < 10; i++) {
    arr.push({
      id: i
    })
  }
  return arr;
}

const Test =  (props) => {
  const [list, setList] = useState(() => {
    return props.list || initVal()
  })
  
  const doScroll = () => {
    // 滚动高度
    let scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop
    // 文档高度
    let scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight
    // 可视高度
    let clientHeight = document.documentElement.clientHeight || document.body.clientHeight
    if (scrollTop + clientHeight > scrollHeight - 50) {
      getList()
    }
  }

  const getList = () => {
      const newList = [].concat(list).concat(initVal())
      setList(newList)
  }

  // 相当于componentDidMount 只执行一次
  useEffect(() => {
    console.log(222)
    getList()
  },[])

  // 每次list更新都执行
  useEffect(() => {
    console.log(111)
    window.addEventListener('scroll', doScroll, false)
    return () => {
      console.log(333)
      window.removeEventListener('scroll', doScroll, false)
    }
  }, [list])

  return (
    <div>
      {
        list.map( (item, index) => {
          return (
            <div style={{height: '200px','textAlign':'center','borderBottom': '1px solid #000','fontSize': '30px'}} key={index}>{item.id}</div>
          )
        })
      }
    </div>

  )
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值