useState和useEffect

Hooks

Hooks是React16.8.0版本推出的api,用来解决函数组件中功能不足的问题

组件:无状态组件(函数组件)、类组件

类组件中的麻烦:

  1. this指向问题
  2. 繁琐的生命周期
  3. 其它问题

Hook专门用于增强函数组件的功能(Hook在类组件中是不能使用的),使之理论上可以成为类组件的替代品

官方强调:没有必要更改已经完成的类组件,目前没有计划取消类组件

Hook(钩子)本质上是一个函数(命名上总是以use开头),该函数可以挂载任何功能

Hook种类:

  1. useState(解决函数组件中无法使用状态的问题)
  2. useEffect(解决函数组件中无法使用生命周期的问题)
  3. 其它...

注意:使用Hook的时候,如果没有严格按照Hook的规则进行,eslint的一个插件eslint-plugin-react-hooks会报出警告

State Hook

用法

State Hook是一个在函数组件中使用的函数(useState),用于在函数组件中使用状态

useState函数有一个参数,这个参数的值表示状态的默认值

  • 函数有一个参数,这个参数的值表示状态的默认值
  • 函数的返回值是一个数组,该数组一定包含两项
    • 第一项: 状态的值
    • 第二项: 一个函数,用来改变状态

基本用法1:

import React, { useState } from 'react'

export default function App() {
  // 使用一个状态,该状态的默认值是0
  const [count, setCount] = useState(0)
  
  return (
    <div>
            <span>{ count }</span>
            <button onClick={ () => setCount(count + 1) }>+</button>
        </div>
  )
}

一个函数组件中可以有多个状态,这种做法有利于横向切分关注点

import React, { useState } from 'react'

export default function App() {
  // 使用一个状态,该状态的默认值是0
  const [count, setCount] = useState(0)
  // 是否可见
  const [visible, setVisible] = useState(true)
  return (
    <div>
      <p style={{ display: visible ? "block" : "none" }}>
        <span>{ count }</span>
        <button onClick={() => setCount(count + 1)}>+</button>
      </p>
      <button onClick={() => setVisible(!visible)}>显示/隐藏</button>
    </div>
  )
}

原理

state-hook.png

注意的细节

  1. useState最好写到函数的起始位置,便于阅读,如果某些状态之间没什么必然的联系,应该分化为不同的状态(解耦)
  2. useState严禁出现在判断、循环的代码块中(这样会导致状态表对应不上)
  3. useState返回的函数,为节约内存空间,引用不会变化
  4. 和类组件setState不同,useState中使用函数改变数据,若数据和之前的数据完全相等(使用Object.is比较),不会导致重新渲染,这样可以提升性能
  5. 和类组件setState不同,使用函数改变数据,传入的值不会和原来的数据进行合并,而是直接替换
  6. 和类组件一样,千万不要直接改变状态,而应该使用相应的api
  7. 和类组件一样,函数组件中改变状态可能是异步的(在DOM事件中),多个状态变化会合并,以提高性能。此时不能信任之前的状态,而应该使用回调函数的方式改变状态。
  8. 如果要实现强制刷新组件
    1. 类组件:使用forceUpdate函数(不会运行shouldComponentUpdate)
    2. 函数组件:可以使用空对象的useState

异步问题:

import React, { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <span>{count}</span>
      <button
        onClick={() => {
          // 异步 不会立即改变 事件运行完成之后一起改变
          setCount(count + 1)
          // 此时 count的值仍然是0 
          setCount(count + 1)
          // 点击事件执行完成后 发现多个setCount调用 合并多个setCount为一个 
          // 也就会导致后面的状态改变覆盖前面的 最终使得点击一次 count只会加1
        }}
      >+</button>
    </div>
  )
}

解决异步问题:

import React, { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <span>{count}</span>
      <button
        onClick={() => {
          // 使用回调函数 解决异步问题
          // 这里调用两次setCount 但是最终只会render一次(调用setCount一次)
          // 原因还是会合并这两次状态 只不过隐式使用中间变量存储了每次改变count的结果
          // 最后进行一次setCount 最终使得点击一次 count只会加2
          setCount(prevCount => prevCount + 1)
          setCount(prevCount => prevCount + 1)
        }}
      >+</button>
    </div>
  )
}

强制刷新:

import React, { useState } from 'react'

export default function App() {
  const [, forceUpdate] = useState({})
  return (
    <div>
      <button onClick={() => forceUpdate({})}>强制刷新</button>
    </div>
  )
}

Effect Hook

Effect Hook: 用于在函数组件中处理副作用

副作用:

  1. ajax请求
  2. 计时器
  3. 其它异步操作
  4. 更改真实DOM对象
  5. 本地存储
  6. 其它会对外部产生影响的操作

函数useEffect,接收一个函数作为参数,接收的函数就是需要进行副作用操作的函数。

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

export default function App() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    // 操作页面标题 有副作用
    document.title = count
  })

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => { setCount(count + 1)}}>
        +
      </button>
    </div>
  )
}

注意的细节

  1. 副作用函数的运行时间点,是在页面完成真实的UI渲染之后。因此它的执行是异步的,不会阻塞浏览器。
    1. 与类组件中componentDidMountcomponentDidUpdate的区别
      1. componentDidMountcomponentDidUpdate更改了真实DOM,但是用户还没有看到UI更新,同步的
      2. useEffect中的副作用函数,更改了真实DOM,并且用户已经看到了UI更新,异步的
  2. 每个函数组件中,可以多次使用useEffect,但是不要放入判断、循环等代码块中
  3. useEffect中副作用函数,可以有返回值,返回值必须是一个函数,该函数叫做清理函数
    1. 首次渲染组件不会运行
    2. 清理函数运行时间点,每次在运行副作用函数之前
    3. 组件被销毁时一定会运行
  4. useEffect函数,可以传递第二个参数
    1. 第二个参数是一个数组
    2. 数组中记录该副作用的依赖数据
    3. 当组件重新渲染后,只有依赖数组中的数据与上一次不一样时,才会执行副作用
    4. 所以,当传递了依赖数据之后,如果数据没有发生变化
      1. 副作用函数只在第一次页面渲染完成后运行
      2. 清理函数只在卸载组件后运行
  5. useEffect函数,不传递第二个参数,则副作用函数默认每次render后都会运行
  6. useEffect闭包
  7. 副作用函数在每次注册时,会覆盖掉之前的副作用函数,因此,尽量保证副作用函数稳定。否则控制起来会比较复杂

useEffect的返回值是一个清理函数:

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

// 点击两次button 输出如下:
// render
// 我是副作用函数
// render
// 我是清理函数
// 我是副作用函数
// render
// 我是清理函数
// 我是副作用函数
export default function App() {
  const [count, setCount] = useState(0)
  console.log('render')
  useEffect(() => {
    console.log('我是副作用函数')
    // 操作页面标题 有副作用
    document.title = count
    return () => {
      console.log('我是清理函数')
    }
  })

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => { setCount(count + 1)}}>
        +
      </button>
    </div>
  )
}

useEffect传递第二个参数(依赖数组为空时可以实现副作用函数只执行一次的效果):

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

// 点击两次button 输出如下:
// render
// 我是副作用函数
// render
// render
export default function App() {
  const [count, setCount] = useState(0)
  console.log('render')
  useEffect(() => {
    console.log('我是副作用函数')
    // 操作页面标题 有副作用
    document.title = count
    return () => {
      console.log('我是清理函数')
    }
  }, []) // 依赖数组为空 

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => { setCount(count + 1)}}>
        +
      </button>
    </div>
  )
}

useEffect形成闭包:

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

// 程序运行 快速点击button5下 输出如下:
// 0
// 1
// 2
// 3
// 4
// 5
export default function App() {
  // 使用一个状态,该状态的默认值是0
  const [count, setCount] = useState(0)
  useEffect(() => {
    setTimeout(() => {
      console.log(count)
    }, 5000)
  })

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  )
}

写一个倒计时组件:

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

// props接收一个beginTime属性
function Countdown(props) {
  const [count, setCount] = useState(props.beginTime)
  useEffect(() => {
    if (count === 0)  return
    setTimeout(() => {
      setCount(count - 1)
    }, 1000)
  }, [count])

  return <span>{count}</span>
}

export default function App() {
  return <Countdown beginTime={10}/>
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354