React学习笔记2

86 阅读9分钟

一、React Hooks

1.1 什么是Hooks

  • React中,useState以及任何其他以use开头的函数都被称为Hook,所以Hooks就是代表use函数的集合,也就是钩子的集合
  • Hooks其实就是一堆功能函数,一个组件想要实现哪些功能,就可以引入对应的钩子函数,像插件一样非常方便
  • Hooks分为:内置Hooks,自定义Hooks,第三方Hooks

ReactHooks使用规则

  • 只能在组件中或者其他自定义Hook函数中调用
  • 只能在组件的顶层调用,不能嵌套在if、for、其他函数中

1.2 useRef

useRef 是一个 React Hook,它能帮助引用一个不需要渲染的值

1.2.1 用ref引用一个值做记忆功能

refstate的区别

image.png


  • 下面注释掉的代码,在连续点击按钮时,会开启多个定时器,原因是:每次setCount,会重新触发渲染,相当于每次的timer都为空,自然清除不了定时器
  • 修改:使用useRef给timer做记忆功能

image.png

import { useRef,useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  // let timer = null
  const timer = useRef(null)

  const handleClick = () => {
    setCount(count + 1)
    // clearInterval(timer)
    clearInterval(timer.current)
    // timer =
    timer.current = setInterval(() => {
      console.log(123)
    }, 1000)
  }
  return (
    <div>
      <button onClick={handleClick}>计数</button>
      <div>{count}</div>
    </div>
  )
}

export default App

1.2.2 useRef可通过ref操作DOM

  • React会自动更新DOM来匹配你的渲染输出,通常情况下不需要操作DOM
  • 但是,有时你可能需要访问由React管理的DOM元素(例如:让一个节点获得焦点,滚动到它或测量它的尺寸和位置)
import { useRef } from 'react'

function App() {
  const h2Ref = useRef()
  const handleCLick = () => {
    // 通过ref操作原生DOM
    h2Ref.current.style.color = 'red'
  }
  return (
    <div>
      <button onClick={handleCLick}>点击变色</button>
      <h2 ref={h2Ref}>2</h2>
    </div>
  )
}

export default App
  • 注意:下面所示代码eslint会报错,因为每次循环都会创建一个新的myRef引用,导致多个元素引用相同的myRef

image.png

  • 在循环中操作ref可以使用回调函数的写法,如下所示
import { useRef } from 'react'

function App() {
  const list = [
    { id: 1, text: 'aaa' },
    { id: 2, text: 'bbb' },
    { id: 3, text: 'ccc' },
  ]
  return (
    <div>
      <ul>
        {list.map((item) => {
          return (
            <li
              key={item.id}
              ref={(myRef) => { myRef.style.background = 'red' }}>
              {item.text}
            </li>
          )
        })}
      </ul>
    </div>
  )
}

export default App

1.2.3 组件设置ref需要forwardRef进行转发

当组件添加ref属性的时候,需要forwardRef进行转发,forwardRef让您的组件通过ref向父组件公开DOM节点

image.png

import { forwardRef, useRef } from 'react'

// forwardRef转发
const MyInput = forwardRef(function MyInput(props, ref) {
  return (
    <div>
      <input type='text' ref={ref} />
    </div>
  )
})

function App() {
  const ref = useRef(null)
  const handleClick = () => {
    ref.current.style.background = 'red'
  }
  return (
    <div>
      <div>App组件</div>
      <button onClick={handleClick}>点击</button>
      <MyInput ref={ref}></MyInput>
    </div>
  )
}

export default App

1.3 useImperativeHandle

useImperativeHandle 是 React 中的一个 Hook,它能让你自定义由ref暴露出来的句柄(这样就不会暴露出整个DOM 节点)

import { forwardRef, useImperativeHandle, useRef } from 'react'

// forwardRef转发
const MyInput = forwardRef(function MyInput(props, ref) {
  const inputRef = useRef(null)
  useImperativeHandle(ref, () => {
    return {
      // 自定义方法
      focusAndStyle() {
        inputRef.current.focus()
        inputRef.current.style.background = 'red'
      },
    }
  })
  return (
    <div>
      <input type='text' ref={inputRef} />
    </div>
  )
})

function App() {
  const ref = useRef(null)
  const handleClick = () => {
    ref.current.focusAndStyle()
    // 使用 useImperativeHandle选择暴露的句柄后,下面代码便会报错了
    // ref.current.style.background = 'red'
  }
  return (
    <div>
      <button onClick={handleClick}>点击获取焦点并变色</button>
      <MyInput ref={ref}></MyInput>
    </div>
  )
}

export default App

1.4 useEffect

useEffect 是一个 React Hook,它允许你将组件与外部系统同步

1.4.1 useEffect基本使用

纯函数

  1. 只负责自己的任务,不会更改在该函数调用前就已存在的对象或变量
  2. 输入相同,则输出相同。给定相同的输入,纯函数总数返回相同的结果

副作用

  1. 函数在执行过程中对外部造成的影响。例如:Ajax调用、DOM操作、与外部系统同步等
  2. 在React组件中,事件操作是可以处理副作用的,但有时候需要初始化处理副作用,就需要useEffect钩子

注意:初始渲染和更新渲染,都会触发useEffect(),它在当前函数组件作用域的最后时机触发

import { useRef,useEffect } from 'react'

function App() {
  const inputRef = useRef(null)
  // 副作用,符合纯函数的规范,因为事件可以处理副作用
  const handleClick = () => {
    inputRef.current.focus()
  }
  
  // 副作用,不符合纯函数的规范
  setTimeout(() => {
    inputRef.current.focus()
  }, 1000)
  
  // 可以在初始的时候进行副作用操作
  // useEffect触发时机:JSX渲染后触发
  useEffect(() => {
    inputRef.current.focus()
  })

  return (
    <div>
      <button onClick={handleClick}>点击获取焦点</button>
      <br />
      <input type='text' ref={inputRef} />
    </div>
  )
}

export default App

1.4.2 useEffect依赖项

useEffect的第二个参数为依赖项数组

  1. 不传递依赖项数组:Effect会在组件的每次单独渲染(和重新渲染)之后运行
  2. 传递依赖项数组:如果指定了依赖项,Effect在初始渲染后以及依赖项变更的重新渲染后运行
  3. 传递空依赖项数组:如果Effect没有使用任何响应式值,则它仅在初始渲染后运行,有响应值则eslint报错
  4. 注意:eslint会检测依赖项是否正确,包括propsstate,计算变量等

image.png

import { useEffect, useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const [msg, setMsg] = useState('你好')
  // 1.不传递数组,每次初次渲染和点击按钮后,都会打印一次
  // 2.传递数组
  useEffect(() => {
    console.log('A打印一次')
  }, [count])
  // 初次渲染时打印,更新时Object.js()做比较,依赖项改变才会触发
  // 这里点击按钮,只改变了count,因此不会打印C
  useEffect(() => {
    console.log('C打印一次')
  }, [msg])
  // 3.传递空数组,只在初次渲染打印,若其中使用了state,eslint会报错
  useEffect(() => {
    console.log('B打印一次')
    console.log(count) // 报错
  }, [])

  const handleClick = () => {
    setCount(count + 1)
  }
  return (
    <div>
      <button onClick={handleClick}>点击</button>
      <div>{count}</div>
    </div>
  )
}

export default App

1.4.3 useEffect中定义函数

  • 函数也可以成为计算变量,所以也要作为依赖项,虽然可以利用依赖项加useCallBack来解决,但是很麻烦
  • 解决方式:把函数定义在useEffect内部
import { useEffect, useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const [msg, setMsg] = useState('')
  const foo = () => {
    console.log(count)
  }
  // 因为Object.is(function(){},function(){} -> false)
  // 就算只修改msg,还是会执行useEffect中的foo()
  useEffect(() => {
    foo()
  }, [foo])
  
  // 解决方式,把函数定义在useEffect内部
  useEffect(() => {
    const foo = () => {
      console.log(count)
    }
    foo()
  }, [count])

  return (
    <div>
      <div>{count}</div>
    </div>
  )
}

export default App

1.4.4 useEffect的清理操作

  • 当卸载组件或更新组件的时候,可以通过useEffect来实现一些清理工作
  • 严格模式下,会检测useEffect是否实现了清理操作
  • 初始化数据时,要注意清理操作,更简洁的方法是使用第三方,例如ahooks中的useRequest

image.png

import { useEffect, useState } from 'react'

// 初始打印进入,点击按钮打印退出
function Chat() {
  useEffect(() => {
    console.log('进入')
    // useEffect的清理工作,卸载和更新的时候都会触发
    return () => {
      console.log('退出')
    }
  })
  return (
    <div>
      <div>hello chat</div>
    </div>
  )
}

function App() {
  const [show, setShow] = useState(true)
  const handleClick = () => {
    setShow(false)
  }
  return (
    <div>
      <div>hello app</div>
      <button onClick={handleClick}>关闭聊天室</button>
      {show && <Chat></Chat>}
    </div>
  )
}

export default App

1.5 useLayoutEffect

useLayoutEffectuseEffect的一个版本,在浏览器重新绘制屏幕之前触发

useEffect的区别

  • useEffect()dom修改前调用,微任务调用,页面渲染之后执行
  • useLayoutEffect()dom修改后调用,同步调用,页面渲染前执行

大部分情况用useEffect(),性能更好,但当你的useEffect里面的操作需要处理dom,并且会改变页面样式,就需要使用useLayoutEffect,否则会出现闪屏问题

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

function App() {
  const [msg, setMsg] = useState('你好')
  // 会出现闪屏问题,useEffect为异步
  useEffect(() => {
    for (let i = 0; i < 100000000; i++) {}
    setMsg('你好啊')
  }, [msg])
  // 不会出现闪屏问题,useLayoutEffect为同步
  useLayoutEffect(() => {
    for (let i = 0; i < 100000000; i++) {}
    setMsg('你好啊')
  }, [msg])
  return (
    <div>
      <h2>{msg}</h2>
    </div>
  )
}

export default App

1.6 useReducer

  • 对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序令人不知所措。

  • 对于这种情况,你可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫做reducer

  • Reducer是处理状态的另一种方式

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

// state表示状态,action表示信号,根据不同的信号,就可以针对性地修改状态
// reducer 是管理员的意思,要修改状态必须通过reducer
// 管理员根据信号进行状态的修改
// 修改状态的流程:1)对于state进行深copy  2)修改更新state  3)返回修改后的state
const reducer = (state, action) => {
  let newState = JSON.parse(JSON.stringify(state)) // 1)对于state进行深copy
  // 根据信号更新state
  switch (
    action.type // action是一个对象,对象中有一个type,不同的type表示不同的信号
  ) {
    case 'NUM_ADD':
      newState.num += 1
      break
    case 'NUM_SUB':
      newState.num -= 1
      break
  }
  return newState
}
// 定义初始值
const initState = {
  num: 1,
  list: ['a', 'b'],
  falg: true,
}

const A = (props) => {
  // dispatch 是用来派发一个action,管理员就可以收到action
  const [state, dispatch] = useReducer(reducer, initState)
  return (
    <div>
      <h2>函数组件</h2>
      <h3>{state.num}</h3>
      <h3>{state.list}</h3>
      <button onClick={() => dispatch({ type: 'NUM_ADD' })}>+1</button>
      <button onClick={() => dispatch({ type: 'NUM_SUB' })}>-1</button>
    </div>
  )
}
export default A

1.7 useContext

  • 在react函数式组件中,如果组件的嵌套层级很深,当父组件想把数据共享给最深的子组件时,传统的方法是使用props,一层层向下传递

  • 但是props层层传递数据的维护性太差,可以使用createContext() + useContext实现多层组件的数据传递

image.png

import { createContext, useContext, useState } from 'react'
// 参数:是默认值
const Context = createContext(0)

function App() {
  const [count, setCount] = useState(123)
  const handleClick = () => {
    setCount(count + 1)
  }
  return (
    <div>
      <button onClick={handleClick}>点击+1</button>
      <div>我是爷爷</div>
      <Context.Provider value={count}>
        <Father></Father>
      </Context.Provider>
    </div>
  )
}

function Father() {
  return (
    <div>
      <div>我是爸爸</div>
      <Son></Son>
    </div>
  )
}

function Son() {
  const value = useContext(Context)
  return (
    <div>
      <div>我是儿子</div>
      <div>这是爷爷的数据{value}</div>
    </div>
  )
}

export default App

1.8 Reducer配合Context实现共享状态管理

  • Reducer可以整合组件的状态更新逻辑。Context可以将信息深入传递给其他组件。组合使用二者可以实现管理一个复杂页面的状态

  • 更复杂的状态管理,可以采用第三方库:Redux、Mobx、Zustand等

1.9 useMemo

useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。

import { memo, useMemo, useState } from 'react'

const Head = memo(function Head() {
  return <div>hello Head,{Math.random()}</div>
})

function App() {
  const [count, setCount] = useState(0)
  const [msg, setMsg] = useState('hello react')
  // const list = [msg.toLowerCase(), msg.toUpperCase()]
  // 缓存后,点击不会更新随机数
  const list = useMemo(() => [msg.toLowerCase(), msg.toUpperCase()], [msg])
  const handleClick = () => {
    setCount(count + 1)
  }
  return (
    <div>
      <button onClick={handleClick}>点击</button>
      <Head list={list}></Head>
    </div>
  )
}

export default App

1.10 useCallback

  • 用于性能优化,缓存一个函数声明

useCallback(fn,dps)相当于useMemo(()=>fn,deps)

  // 二者效果相同
  const fn = useMemo(()=>()=>{console.log(msg)},[msg])
  const fn = useCallback(()=>{console.log(msg)},[msg])

1.11 useTransition

  • React18之前,渲染是一个单一的、不间断的、同步的事务,一旦渲染开始,就不能被中断
  • React18引入并发模式,它允许你将标记更新作为一个transitions,这会告诉React它们可以被中断执行。这样可以把紧急的任务先更新,不紧急的任务后更新

useTransition可以让你在不阻塞UI的情况下更新状态,返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数

1.12 useDeferredValue

接受一个值,并返回该值的新副本,该副本将延迟到更紧急的更新之后

1.13 useId

useId是一个React Hook,可以生成传递给无障碍属性的唯一ID

1.14 自定义Hook函数

自定义Hook是以use打头的函数通过自定义Hook函数可以实现逻辑的封装和复用

封装自定义Hook的通用思路

  1. 声明一个use开头的函数
  2. 在函数体中封装可复用的逻辑
  3. 把函数中用到的状态或者回调return出去(以对象或数组)
  4. 在哪个组件中用到这个逻辑,就执行这个函数,解构出来状态和回调进行使用

import { useState } from 'react'

// 自定义hook——点击切换
function useToggle() {
  const [show, setShow] = useState(true)
  const handleClick = () => {
    setShow(!show)
  }
  return {
    show,
    handleClick,
  }
}

function App() {
  const { show, handleClick } = useToggle()
  return (
    <div>
      {show && <div>哈啊哈</div>}
      <button onClick={handleClick}>toggle</button>
    </div>
  )
}

export default App