React(九)React Hooks

初识Hook

我们到底为什么需要hook那?

函数组件类组件存在问题

函数组件存在的问题:

javascript 复制代码
import React, { PureComponent } from 'react'

function HelloWorld2(props) {
  let message="Hello world"

  // 函数式组件存在的缺陷:
  // 1.修改message之后,组件不会重新渲染
  // 2.就算页面重新渲染:函数会被重新执行,第二次执行时,又会重新给message赋值未Hello world
  // 3.不能编写类似生命周期的回调:网络请求...
  return (
    <div>
      <h1 onClick={e =>message="你好啊"}>内容2:{message}</h1>
      <button>修改文本</button>
    </div>
  )
}
export class App extends PureComponent {
  render() {
    return (
      <div>
        <h1>App</h1>
        <hr />
        <HelloWorld2 />
      </div>
    )
  }
}

export default App

class组件存在的问题:

1.复杂组件变得难以理解

  • 随着业务增加,class组件会变得越来越复杂
  • 在挂载阶段,可能包含大量的逻辑代码:网络请求、事件监听等;还得在卸载阶段移除掉
  • 逻辑往往混在一起,class难以拆分

2.更加复杂:需要搞清this的指向到底是谁,熟练ES6class

3.组件复用难

  • 状态复用需要通过高阶组件
  • 或者类似Provider、Consumer来共享一些状态,但多次使用Consumer,会导致我们代码存在很多嵌套,难以维护

Hook的出现

hooks的出现就是为了解决上述问题:

  • 它可以在不编写class的情况下使用state以及其它React特性
  • 基本可替代之前所有使用class组件的地方
  • 并不需要直接将所有的代码重构为hooks,因为它向下兼容,可渐进式使用它
  • 只能在函数组件中使用,不能在类组件中使用

Hook实现计数器

javascript 复制代码
import React, { memo,useState } from 'react'
// useState()返回的是一个数组
// 元素一:当前状态的值(第一次为初始值),如果不设置则为undefined
// 元素二:更新当前值的函数
const CounterFunction = memo(() => {
  const [counter,setCounter] = useState(0)
  return (
    <div>
      <h2>CounterFunction:{counter}</h2>
      <button onClick={e => setCounter(counter+1)}>+1</button>
      <button onClick={e => setCounter(counter-1)}>-1</button>
    </div>
  )
})

export default CounterFunction

State/Effect

useState

1.useState来自react,需要从react中导入,它是一个hook;

  • 参数:初始化值,如果不设置为undefined;
  • 返回值:数组,包含两个元素;
  • 元素一:当前状态的值(第一调用为初始化值);
  • 元素二:设置状态值的函数;

2.点击button按钮后,会完成两件事情:

  • 调用setCount,设置一个新的值;
  • 组件重新渲染,并且根据新的值返回DOM结构;

3.Hook 就是 JavaScript 函数,这个函数可以帮助你 钩入(hook into) React State以及生命周期等特性;

4.但是使用它们会有两个额外的规则:

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用

如果逻辑太复杂,也可以单独封装一个函数修改数据

javascript 复制代码
import React, { memo,useState } from 'react'

const CounterFunction = memo(() => {
  const [message,setMessage] = useState("HelloWorld")

  function changeMessage(){
    setMessage("你好呀")
  }
  return (
    <div>
      <h2>CounterFunction:{message}</h2>
      <button onClick={changeMessage}>修改文本</button>
    </div>
  )
})

export default CounterFunction

注意:为什么是useState而不是createState?

解答:因为state只在组件首次渲染时被创建,在下次渲染时,useState返回给我们当前的state。如果每次都创建新的变量,那就不该是状态了。

useEffect

它可以帮在函数组件中完成类似于class生命周期的功能。如网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects)。因此对于完成这些功能的Hook就被称为Effect Hook

1.基本使用

我们现在来完成一个需求:使页面的title总是显示count的数字:

代码实现:

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

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

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

2.清除Effect

在类组件编写过程中,某些副作用我们需要在componentWillUnmount中进行清除,如事件总线、Redux中手动调用的subscribe等

useEffect传入的回调函数A本身有一个返回值,返回值里面是回调函数B,可在函数B中进行清除

javascript 复制代码
// 复杂告知react,在执行完当前渲染之后要执行的副作用代码
useEffect(() => {
  console.log("监听redux中数据变化");

  // 传入的回调函数,又会返回一个回调函数=> 组件被重新渲染或卸载时执行
  return () => {
    console.log("取消监听redux中的数据变化");
  }
})

3.使用多个Effect

将这些处理逻辑都放在同一个函数里面难维护,要清除也比较混乱,在使用中我们可将逻辑分离到不同的useEffect中:React 将按照 effect 声明的顺序依次调用组件中的每一个 effect;

如下图:

但是有一个问题,难道每渲染一次页面,我们的所有effect就要重新调用一次吗?

某些逻辑只需要执行一次即可,怎样才能解决这个问题嘞?

实际上,Effect有两个参数:

  • 参数一:执行的回调函数;
  • 参数二:该useEffect在哪些state发生变化时,才重新执行;(受谁的影响)
javascript 复制代码
import React, { memo, useEffect, useState } from 'react'

const App = memo(() => {
  const [count,setCount] = useState(0)
  const [message,setMessage] = useState("HelloWorld")

  useEffect(() => {
    console.log("修改title");
  },[count])

  useEffect(() => {
    console.log("监听redux中的数据");
    return () => {
    }
  },[])

  useEffect(() => {
    console.log("监听eventBus的why事件");
    return () => {
    }
  },[])

  // 我希望它第一次渲染时执行一次即可
  useEffect(() => {
    console.log("发生网络请求");
    return () => {
      console.log("会在我们组件被卸载时才会执行");
    }
  },[]) 

  return (
    <div>
      <h2>Count-{count} </h2>
      <button onClick={e => setCount(count+1)}>+1</button>
      <button onClick={e => setMessage("你好啊")}>message({message})</button>
    </div>
  )
})

export default App

打印结果如下图:

Context/Reducer

useContext

我们之前在组件中使用类名.contextType = MyContext或MyContext.Consumer来共享数据

但是当多个Context共享时就会存在大量的嵌套:

javascript 复制代码
import React, { Component } from 'react'
import { UserContext,ThemeContext } from './context'

export default class App extends Component {
  // 使用Conetxt
  render() {
    return (
      <div>
        <UserContext.Consumer>
          {
            value => {
              return (
                <h2><ThemeContext.Consumer>
                  {value => <span>{value.color}</span>}
                  </ThemeContext.Consumer> </h2>
              )
            }
          }
        </UserContext.Consumer>
      </div>
    )
  }
}

Context Hook允许我们通过Hook来直接获取某个Context的值

javascript 复制代码
import React, { memo ,useContext} from 'react'
import { UserContext,ThemeContext } from './context'

const App = memo(() => {
  // 使用Conetxt
  const user = useContext(UserContext)
  const theme = useContext(ThemeContext)
  return (
    <div>
      <div>
        <span>用户信息-{user.name}-{user.age} </span>
        <span>主题色-{theme.color} </span>
      </div>
    </div>
  )
})

export default App

注意:当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的context value 值。因此,该数据的更新也是响应式的。

Callback/Memo(性能优化)

useCallback

通常使用useCallback是不希望子组件进行多次渲染,并不是为了进行函数缓存

1.存在问题

javascript 复制代码
import React, { memo,useCallback,useState } from 'react'

const HYHome = memo(function(props) {
  const { increment } = props
  console.log("HYHome被渲染")
  return (
    <div>
      <button onClick={increment}>increment+8</button>

      {/* 100个子组件 */}
    </div>
  )
})

const App = memo(() => {
  const [count, setCount] = useState(0)

  const increment = function() {
    console.log("increment");
    setCount(count + 8)
  }

  return (
    <div>
      <h2>App-{count}</h2>
      <button onClick={increment} >+8</button>
      <HYHome increment={increment}/>
    </div>
  )
})

export default App

我希望当子组件没有变化时,不需要重新渲染:

useCallback()返回一个函数的记忆值,在依赖不变的情况下,多次定义返回值是相同的

javascript 复制代码
import React, { memo, useState, useCallback, useRef } from 'react'

// useCallback性能优化的点:
// 1.当需要将一个函数传递给子组件时, 最好使用useCallback进行优化, 将优化之后的函数, 传递给子组件------这样的话在某些情况下,子组件就不会重新渲染

// props中的属性发生改变时, 组件本身就会被重新渲染
const HYHome = memo(function(props) {
  const { increment } = props
  console.log("HYHome被渲染")
  return (
    <div>
      <button onClick={increment}>increment+1</button>

      {/* 100个子组件 */}
    </div>
  )
})

const App = memo(function() {
  const [count, setCount] = useState(0)
  const [message, setMessage] = useState("hello")

  // 闭包陷阱: useCallback
  // const increment = useCallback(function foo() {
  //   console.log("increment")
  //   setCount(count+1)
  // }, [count])

  // 进一步的优化: 当count发生改变时, 也使用同一个函数(了解)
  // 做法一: 将count依赖移除掉, 缺点: 闭包陷阱
  // 做法二: useRef, 在组件多次渲染时, 返回的是同一个值
  const countRef = useRef()
  countRef.current = count
  const increment = useCallback(function foo() {
    console.log("increment")
    setCount(countRef.current + 1)
  }, [])

  // 普通的函数
  // const increment = () => {
  //   setCount(count+1)
  // }

  return (
    <div>
      <h2>计数: {count}</h2>
      <button onClick={increment}>+1</button>

      <HYHome increment={increment}/>

      <h2>message:{message}</h2>
      <button onClick={e => setMessage(Math.random())}>修改message</button>
    </div>
  )
})

export default App

useMemo

1.使用场景:

  • 有大量逻辑计算时,若逻辑输入值无变化,则无需重新计算
  • 对子组件传递相同内容对象时,若无改变则不重新执行

2.函数传入两个参数:

参数一:回调函数

参数二:逻辑执行依赖的变量

3.useMemo和useCallback的对比

useMemo拿到的是传入回调函数的返回值,useCallback是回调函数本身

javascript 复制代码
import React, { memo, useCallback } from 'react'
import { useMemo, useState } from 'react'


const HelloWorld = memo(function(props) {
  console.log("HelloWorld被渲染~")
  return <h2>Hello World</h2>
})


// 计算数字
function calcNumTotal(num) {
  // console.log("calcNumTotal的计算过程被调用~")
  let total = 0
  for (let i = 1; i <= num; i++) {
    total += i
  }
  return total
}

const App = memo(() => {
  const [count, setCount] = useState(0)

  // const result = calcNumTotal(50)

  // 1.不依赖任何的值, 进行计算
  const result = useMemo(() => {
    return calcNumTotal(50)
  }, [])

  // 2.依赖count
  // const result = useMemo(() => {
  //   return calcNumTotal(count*2)
  // }, [count])

  // 3.useMemo和useCallback的对比
  function fn() {}
  // const increment = useCallback(fn, [])
  // const increment2 = useMemo(() => fn, [])


  // 4.使用useMemo对子组件渲染进行优化
  // 重新渲染时会定义一个新对象,传入值是没有什么区别的
  // const info = { name: "why", age: 18 }
  const info = useMemo(() => ({name: "why", age: 18}), [])

  return (
    <div>
      <h2>计算结果: {result}</h2>
      <h2>计数器: {count}</h2>
      <button onClick={e => setCount(count+1)}>+1</button>

      <HelloWorld result={result} info={info} />
    </div>
  )
})

export default App

Ref/LayoutEffect

useRef

1.获取DOM

javascript 复制代码
import React, { memo, useRef } from 'react'

const App = memo(() => {
  const titleRef = useRef()
  const inputRef = useRef()

  function showTitleDom() {
    console.log(titleRef.current)
    inputRef.current.focus()
  }

  return (
    <div>
      <h2 ref={titleRef}>Hello World</h2>
      <input type="text" ref={inputRef} />
      <button onClick={showTitleDom}>查看title的dom</button>
    </div>
  )
})

export default App

2.useRef解决闭包陷阱问题

useRef返回一个ref对象,返回的ref对象再组件的整个生命周期保持不变

javascript 复制代码
import React, { memo, useRef } from 'react'
import { useCallback } from 'react'
import { useState } from 'react'

let obj = null

const App = memo(() => {
  const [count, setCount] = useState(0)
  // useRef返回的是一个不变的对象(地址不变)
  const nameRef = useRef()
  console.log(obj === nameRef)
  obj = nameRef

  // 通过useRef解决闭包陷阱
  const countRef = useRef()
  countRef.current = count

  const increment = useCallback(() => {
    setCount(countRef.current + 1)
  }, [])

  return (
    <div>
      <h2>Hello World: {count}</h2>
      <button onClick={e => setCount(count+1)}>+1</button>
      <button onClick={increment}>+1</button>
    </div>
  )
})

export default App

useImperativeHandle/useLayoutEffect(了解)

useImperativeHandle

通过forwardRef可以将子组件的DOM直接暴露给父组件,但是这样也产生了一些问题:

父组件可以拿到子组件的DOM进行任意操作,这样就有点危险了,我们有什么办法解决吗?

  • 可以通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起
  • 在父组件中,使用 inputRef.current时,实际上使用的是返回的对象

代码示例:

javascript 复制代码
import React, { memo, useRef, forwardRef, useImperativeHandle } from 'react'



const HelloWorld = memo(forwardRef((props, ref) => {

  const inputRef = useRef()

  // 如果直接绑定它,权限有点多,对于子组件来说太危险了------我希望只给它保留特定的权限
  // 子组件对父组件传入的ref进行处理
  useImperativeHandle(ref, () => {
    return {
      focus() {
        console.log("focus")
        inputRef.current.focus()
      },
      setValue(value) {
        inputRef.current.value = value
      }
    }
  })

  return <input type="text" ref={inputRef}/>
}))


const App = memo(() => {
  const titleRef = useRef()
  const inputRef = useRef()

  function handleDOM() {
    // console.log(inputRef.current)
    inputRef.current.focus()
    // inputRef.current.value = ""
    inputRef.current.setValue("哈哈哈")
  }

  return (
    <div>
      <h2 ref={titleRef}>哈哈哈</h2>
      <HelloWorld ref={inputRef}/>
      <button onClick={handleDOM}>DOM操作</button>
    </div>
  )
})

export default App

useLayoutEffect

useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:

  • useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
  • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;

如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect。

javascript 复制代码
import React, { memo, useEffect, useLayoutEffect, useState } from 'react'

const App = memo(() => {
  const [count, setCount] = useState(100)

  // 点击按钮后,会出现闪烁现象-先变成0,再变成99.xxxx,
  useEffect(() => {
    console.log("useEffect")
    if (count === 0) {
      setCount(Math.random() + 99)
    }
  })

  // 不会出现闪烁现象,直接变成99.xxxx
  useLayoutEffect(() => {
    console.log("useLayoutEffect")
    if (count === 0) {
      setCount(Math.random() + 99)
    }
  })

  console.log("App render")

  return (
    <div>
      <h2>count: {count}</h2>
      <button onClick={e => setCount(0)}>设置为0</button>
    </div>
  )
})

export default App

自定义Hook

本质:对函数代码的抽取,实现复用,减少代码逻辑

需求一:所有的组件在创建和销毁时都进行打印

javascript 复制代码
import React, { memo, useEffect, useState } from 'react'

// 自定义Hook
function useLogLife(cName) {
  useEffect(() => {
    console.log(cName + "组件被创建")
    return () => {
      console.log(cName + "组件被销毁")
    }
  }, [cName])
}

const Home = memo(() => {
  useLogLife("home")

  return <h1>Home Page</h1>
})

const About = memo(() => {
  useLogLife("about")

  return <h1>About Page</h1>
})

const App = memo(() => {
  const [isShow, setIsShow] = useState(true)

  useLogLife("app")

  return (
    <div>
      <h1>App Root Component</h1>
      <button onClick={e => setIsShow(!isShow)}>切换</button>
      { isShow && <Home/> }
      { isShow && <About/> }
    </div>
  )
})

export default App

需求二:Context的共享

1.有两个自定义的context

javascript 复制代码
import { createContext } from "react";

const UserContext = createContext()
const TokenContext = createContext()

export {
  UserContext,
  TokenContext
}

2.通过.Provider去包裹根组件

javascript 复制代码
root.render(
  <userContext.Provider value={{name:'dimple', age: 22}}>
    <tokenContext.Provider value={'dimple555'}>
      <App />
    </tokenContext.Provider>
  </userContext.Provider>
);

3.封装一个hook方法,只要一调用就能拿到共享的数据

javascript 复制代码
import { useContext } from "react"
import { UserContext, TokenContext } from "../context"

function useUserToken() {
  const user = useContext(UserContext)
  const token = useContext(TokenContext)

  return [user, token]
}

export default useUserToken

4.在子组件中使用

javascript 复制代码
import React, { memo } from 'react'
import { useUserToken } from "./hooks"

// User/Token

const Home = memo(() => {
  const [user, token] = useUserToken()

  return <h1>Home Page: {user.name}-{token}</h1>
})

const About = memo(() => {
  const [user, token] = useUserToken()

  return <h1>About Page: {user.name}-{token}</h1>
})

const App = memo(() => {
  return (
    <div>
      <h1>App Root Component</h1>
      <Home/>
      <About/>
    </div>
  )
})

export default App

需求三:获取鼠标滚动位置

1.定义一个hook

javascript 复制代码
import { useState, useEffect } from "react"

function useScrollPosition() {
  const [ scrollX, setScrollX ] = useState(0)
  const [ scrollY, setScrollY ] = useState(0)

  useEffect(() => {
    function handleScroll() {
      // console.log(window.scrollX, window.scrollY)
      setScrollX(window.scrollX)
      setScrollY(window.scrollY)
    }

    window.addEventListener("scroll", handleScroll)
    //使用完销毁
    return () => {
      window.removeEventListener("scroll", handleScroll)
    }
  }, [])

  return [scrollX, scrollY]
}

export default useScrollPosition

2.在各个组件中使用

javascript 复制代码
import React, { memo } from 'react'
import useScrollPosition from './hooks/useScrollPosition'
import "./style.css"

const Home = memo(() => {
  const [scrollX, scrollY] = useScrollPosition()

  return <h1>Home Page: {scrollX}-{scrollY}</h1>
})

const About = memo(() => {
  const [scrollX, scrollY] = useScrollPosition()

  return <h1>About Page: {scrollX}-{scrollY}</h1>
})

const App = memo(() => {
  return (
    <div className='app'>
      <h1>App Root Component</h1>
      <Home/>
      <About/>
    </div>
  )
})

export default App

redux hooks

在redux开发中,我们为了让组件和redux结合起来,我们使用了react-redux的connect,这种方式必须使用高阶函数结合返回的高阶组件,且必须编写mapStateToProps和mapDispatchToProps映射的函数,这样其实挺麻烦的

Redux7.1开始,可获取仓库的数据和方法

useSelector:将store映射到组件中

参数一:传入一个回调函数,参数为state,返回需要的数据对象

参数二:通过比较决定组件是否重新渲染

javascript 复制代码
import React, { memo } from 'react'
import { useSelector, useDispatch, shallowEqual } from "react-redux"
import { addNumberAction, changeMessageAction, subNumberAction } from './store/modules/counter'


// memo高阶组件包裹起来的组件有对应的特点: 只有props发生改变时, 才会重新渲染
//它默认会比较比较返回的两个对象是否相等,也就是必须返回两个完全相等的对象才可以不引起重新渲染
//那比如message属性值没发生改变就不该渲染该组件的,所以加了shallowEqual
const Home = memo((props) => {
  const { message } = useSelector((state) => ({
    message: state.counter.message
  }), shallowEqual)

  const dispatch = useDispatch()
  function changeMessageHandle() {
    dispatch(changeMessageAction("你好啊, 师姐!"))
  }

  console.log("Home render")

  return (
    <div>
      <h2>Home: {message}</h2>
      <button onClick={e => changeMessageHandle()}>修改message</button>
    </div>
  )
})


const App = memo((props) => {
  // 1.使用useSelector将redux中store的数据映射到组件内
  const { count } = useSelector((state) => ({
    count: state.counter.count
  }), shallowEqual)

  // 2.使用dispatch直接派发action
  const dispatch = useDispatch()
  function addNumberHandle(num, isAdd = true) {
    if (isAdd) {
      dispatch(addNumberAction(num))
    } else {
      dispatch(subNumberAction(num))
    }
  }

  console.log("App render")

  return (
    <div>
      <h2>当前计数: {count}</h2>
      <button onClick={e => addNumberHandle(1)}>+1</button>
      <button onClick={e => addNumberHandle(6)}>+6</button>
      <button onClick={e => addNumberHandle(6, false)}>-6</button>

      <Home/>
    </div>
  )
})

export default App
相关推荐
前端之虎陈随易3 小时前
编程语言级别的Skill市场,AI Agent 的未来形态
前端·vue.js·人工智能·typescript·node.js
一路向北he4 小时前
字节钢铁军团--“提供情境,而非控制”
java·开发语言·前端
kyriewen4 小时前
豆包和千问同时关了智能体,我用它们搭的 3 个自动化全废了——迁移方案整理
前端·javascript·ai编程
前端一小卒4 小时前
我用 TypeScript 从零手写了一个 Claude Code,然后发现它的核心只有 30 行
前端·agent
大圣编程6 小时前
Python中continue语句的用法是什么?
开发语言·前端·python
yuhaiqiang6 小时前
随手 vibecoding 的浏览器插件已经 6000 多次下载,聊聊他的产品设计
前端·后端·面试
之歆6 小时前
Vue商品详情与放大镜组件
前端·javascript·vue.js
再吃一根胡萝卜7 小时前
如何把小米 MiMo 接入 CodeBuddy,打造私有 Agent
前端
负责的蛋挞8 小时前
异步HttpModule的实现方式
java·服务器·前端
YFF菲菲兔9 小时前
其他 Hooks 解析
react.js