基本介绍

1. 概念及作用

  • Hooks,意为钩子,React Hooks就是一堆钩子函数,react通过这些钩子函数对函数式组件的增强
  • 让函数型组件可以存储状态,可以拥有处理副作用的能力,
  • 不同的钩子函数提供了不同的功能,让开发者在不使用类组件的情况,实现相同的功能

    2. 副作用

  • 在一个组件中,只要不将数据转换成视图的代码就属于【副作用】,

  • 如:获取 DOM 元素并为其添加事件、设置定时器、发送 ajax 请求等。
  • 在类组件中通常使用生命周期函数处理副作用,而函数组件使用 Hooks 来处理副作用

    3. 类组件的不足(hooks要解决的问题)

  1. 缺少逻辑复用机制(要说一下HOC和render Props的缺点,从而引出hooks)
    1. 为了复用逻辑增加无实际渲染效果的组件,增加了组件层级 显示十分臃肿,增加了调试的难度以及运行效率的降低
  2. 类组件经常会变的很复杂难以维护
    1. 将一组相干的义务逻辑拆分到了多个声明周期函数中
    2. 在一个生命周期函数内存在多个不相干的业务逻辑
  3. 类成员方法不能保证this指向的正确性

React Hooks使用

【钩子函数】React 通过这些钩子函数,对【函数型组件】进行增强,不同的钩子函数提供了不同的功能

2.1 useState()

  • 让函数型组件在重新渲染后还能保留 state 的状态
  • 用于为函数组件引入状态,一个函数调用完之后,里面的变量就被释放掉了,useState是使用闭包来保存状态的
  1. function App(){
  2. const [ count, setCount ] = useState(0)
  3. return (
  4. <div>
  5. <span>{count}</span>
  6. <button onClick={()=>setCount(count+1))}> + 1</button>
  7. </div>
  8. )
  9. }

特性:

  1. 接受唯一的参数(也就是状态初始值),初始值可以是任意数据类型。
  2. 参数可以是一个函数,函数返回什么,初始状态就是什么,函数只会被调用一次,用在初始值是动态值的情况。
  3. 返回值为数组,数组中存储状态值和更改状态值的方法。方法名称约定为以set开头,后面加上状态名称
  4. 方法可以被调用多次,用以保存不同的状态
  5. 设置状态方法本身是异步的,可以在参数是一个函数中拿到修改后的count
    1. function App(){
    2. const [ person, setPerson ] = useState({name:'Fanny',age:'26'})
    3. return (
    4. <div>
    5. <span>{person.name}{person.age}</span>
    6. <button onClick={()=>setPerson({...person,name:'Vicky'})}> + 1</button>
    7. // 如果不对person进行浅拷贝,那么age会为空
    8. </div>
    9. )
    10. }
    注意1: useState 传入函数设置初始值 ```

function App(){ const propsCount = props.count || 0; const [ count, setCount ] = useState(propsCount) // 每次旋绕都会获取,没有意义。只需给useState设置一个初始函数即可。 return (

{count}
) }

  1. 修改后

function App(){ const [ count, setCount ] = useState(()=>props.count||0) // 只有第一次渲染组件被执行,重新渲染不会执行。 return (

{count}
) }

  1. useState(普通值||返回普通值的初始值函数),初始值函数只会执行一次。<br />如果这个初始值是外部接受来的,最好使用初始值函数设置初始值。
  2. 注意2 setCount是异步函数<br />setCount(这里可以传入函数,直接在这个函数中处理返回值的依赖值)<br />也就是说,如果 `count` 现在是 `0` ,在 `setCount(count+1)` 后紧接着输出 `count` ,它的值仍然为 `0` ,要获取最新的 `count` 值可以在 `setCount` 中对 `count` 进行处理后得到最新的值

setCount(count=>{ const newCount = count + 1; document.title = newCount; return newCount; })

  1. <a name="jCTik"></a>
  2. #### 2.2 useReducer()
  3. useReducer是另一种让函数组件保存状态的方式<br />它的使用方式与 Redux 中的 reducer 非常相似
  4. ```javascript
  5. function App(){
  6. function reducer(state, action) {
  7. switch (action.type) {
  8. case 'increment':
  9. return state + 1;
  10. case 'decrement':
  11. return state - 1;
  12. default"
  13. return state;
  14. }
  15. }
  16. const [ count, dispatch ] = useReducer(reducer, 0)
  17. return (
  18. <div>
  19. <button onClick={()=>dispatch({type: 'increment'})}>
  20. <span>{count}</span>
  21. <button onClick={()=>dispatch({type: 'decrement'})}>
  22. </div>
  23. )
  24. }

useReducer VS useState

  • useReducer 可以合并多个 setState 方法,也就是可以对变量做出多种改变,
  • useState 只能对变量作一种改变,每次改变需要定义相应的方法,但结构相对简单
  • useReducer 可以把一堆改变变量的方法(dispatch)直接传递给子组件,子组件瞬间 buff(比如子组件需要修改富组件状态,可以把dispatch传递下去)

2.3 useContext()

在【跨组件层级获取数据时,简化获取数据的代码

  1. import React ,{createContext} from 'react';
  2. const countContext = createContext();
  3. function App() {
  4. return (
  5. <countContext.provider value={100}>
  6. <Foo />
  7. </countContext.provider>
  8. )
  9. }
  10. function Foo() {
  11. // 繁琐的写法,需要优化
  12. return <countContext.Consumer>
  13. {
  14. value =>{
  15. return <div>{value}</div>
  16. }
  17. }
  18. </countContext.Consumer>
  19. }
  20. export default App;
  1. import React ,{createContext} from 'react';
  2. const countContext = createContext();
  3. function App() {
  4. return (
  5. <countContext.provider value={100}>
  6. <Foo />
  7. </countContext.provider>
  8. )
  9. }
  10. function Foo() {
  11. const value = useContext(countContext)
  12. return <div>{value}</div>
  13. }
  14. export default App;

优点

  • 跨多个层级的组件也能获取到上层组件传递的值,不用一级一级传递

缺点

  • 只能获取值,无法修改

    2.4 useEffect()

  • class 组件使用【生命周期函数】来处理【副作用】

  • useEffect 就是用来在函数组件中处理副作用的,根据不同的参数,等价于类组件的componentDidMount, componentDidUpdate,componentWillUnMount 生命周期

    1. useEffect(()=>{}) => componentDidMount, componentDidUpdate
    2. useEffect(()=>{}, []) => componentDidMount
    3. useEffect(()=>()=>{}) => componentWillUnMount

    useEffect 的执行时机:

  • 组件挂在完成后

  • 组件数据更新后
  • 组件被卸载前

2.4.1. 监听所有 observable 变量,一旦发生变化就会执行

  1. // 组件挂载完成之后执行 组件数据更新完成之后执行
  2. useEffect(()=>{
  3. console.log('123');
  4. })
  • useEffect可以写多个,可以把不同的用途分开来写
  • 将同一组业务逻辑写在一个生命周期函数中,比如挂载和卸载卸载一起

2.4.2.挂载后执行一次,再也不会执行

  1. useEffect(()=>{
  2. console.log('挂载后执行一次,再也不会执行');
  3. }, [])

2.4.3.组件被卸载之前执行一次

  1. useEffect(()=>{
  2. return ()=>{
  3. console.log('组件被卸载了')
  4. }
  5. })

怎样卸载 App 这个组件?
image.png
从 ‘react’ 中引入 ReactDOM 调用方法,传入 root 节点进行卸载

2.4.4. 指定数据发生变化时才会触发 effect

  1. useEffect(()=>{
  2. document.title = count
  3. }, [count])

2.4.5. 在 useEffect 中执行异步函数:IIFE

  • useEffect 中第一个参数必须为一个函数,而且不能是异步函数,
  • 因为 useEffect 要返回清理资源的函数,异步函数则会默认返回 Promise ```javascript // 不可以,因为useEffect的return是一个卸载的回调函数,而下面这种写法是返回一个promise useEffect(async ()=> { const res = await axios.get() })

// 正确用法 // 在useEffect中写一个自执行函数 useEffect(()=> { (async ()=> { const res = await axios.get() })() })

  1. <a name="B1MIJ"></a>
  2. #### useEffect VS 生命周期函数
  3. - useEffect 可以被多次调用,根据不用的调用方式产生不同的【生命周期】效果,代码更加简洁、灵活
  4. - useEffect 可以将多个【生命周期】逻辑整合,如【初次更新】和【数据更新】后代码逻辑类似,整合之后去除重复代码使代码逻辑更清晰
  5. - 类中的【生命周期函数】往往需要将多个业务逻辑写在同一个生命周期函数中,或需要将一个逻辑拆分在不同的生命周期函数中
  6. <a name="fsCXJ"></a>
  7. #### 2.5 useMemo()
  8. - useMemo 的行为类似 Vue 中的【计算属性】,可以监测某个值的变化,根据变化值计算新值
  9. - useMemo 会缓存计算结果,如果监测值没有发生变化,即使组件重新渲染也不会重新计算;此行为可以有助于避免在某个渲染上进行昂贵的计算
  10. ```javascript
  11. const result = useMemo(()=> {
  12. // 如果count变化,重新执行这个函数
  13. return result
  14. }, [count])

优点

  • 缓存昂贵计算值
  • 它的返回值可以参与视图渲染
  • 可以监听其他数值变化产生衍生计算(副作用)

    2.6 memo()

  • 【性能优化】如果组件中的数据没有发生变化, memo(组件) 可以阻止组件更新;

  • 类似 class 组件中的 PureComponentshouldComponentUpdate ```javascript const Foo = memo(function Foo() { return
    xxx
    })
  1. `memo(Component)` 的返回值是一个新的组件,这个新的组件会判断传入的组件数据有没有发生变化,例<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1359495/1630064301418-64650caf-2e31-4b07-985b-cb32a3115625.png#clientId=u3dc73b03-5cd2-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=551&id=ubb2cf98e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1102&originWidth=1500&originalType=binary&ratio=1&rotation=0&showTitle=false&size=672160&status=done&style=none&taskId=ub4155c6b-453c-457f-baa6-f91c2ff84fd&title=&width=750)
  2. <a name="zmCbK"></a>
  3. ####
  4. <a name="eMvgJ"></a>
  5. #### 2.7 useCallback()
  6. 性能优化,缓存函数,使组件重新渲染使得到相同函数实例<br />函数传递给子组件时,每次重新渲染都得到了一个新的函数实例,这样子组件就又会重新渲染
  7. ```javascript
  8. function App(){
  9. const [count, setCount] = useState(0)
  10. //function resetCount(){
  11. // setCount(0)
  12. // }
  13. const resetCount = useCallback(()=> setCount(0), [setCount])
  14. return (
  15. <div>
  16. <Foo resetCount={resetCount} />
  17. </div>
  18. )
  19. }
  20. const Foo = memo(function Foo(props) {
  21. return <div><button onClick={props.resetCount}></button></div>
  22. })
  23. // 这里foo组件重新渲染了,因为count值发生变化,app组件重新渲染,(重新渲染每次都生成了不同的resetCount实例)重新渲染之后resetCount就不是之前的
  24. // 函数实例了,值发生了改变,所以导致foo组件也跟着更新

image.png

优化后的 resetCount 函数↓,只有当 setCount 发生变化才会重新取得新的 ()=>setCount(0) 函数
image.png

2.8 useRef()

2.8.1 用于【获取 DOM 元素对象】

  1. import React, { useRef } from 'react';
  2. function App(){
  3. const username = useRef();
  4. const handler = ()=>console.log(username) // {current:input}
  5. return (
  6. <input ref={username} onChange={handler}/>
  7. )
  8. }

input 元素上添加 ref 属性后, username 这个对象上的 current 键会指向 input 这个 DOM 元素,达到取值效果

2.8.1 用于【跨组件周期保存数据】

  • 即使组件重新渲染,使用 useRef 保存的数据仍然存在
  • 保存的数据被更改不会触发组件重新渲染

例:
下面这段代码无法在点击事件之后清除 interval 定时器

  1. import React, { useState,useEffect} from 'react';
  2. function App(){
  3. const [count, setCount ] = useState(0);
  4. let timerId = null; // 重新渲染后timerId为null
  5. useEffect(()=>{
  6. timerId = setInterval(()=>{
  7. setCount(count=>count+1)
  8. },1000) // 每次count更新都会促发组件的重新渲染
  9. },[]);
  10. const stopCount = ()=>{
  11. clearInterval(timerId)
  12. // 这里的timerId 为 null
  13. }
  14. return <div>
  15. {count}
  16. <button onClick={stopCount}> stop </button>
  17. </div>
  18. }

解决方法:

  1. import React, { useState,useEffect,useRef } from 'react';
  2. function App(){
  3. const [count, setCount ] = useState(0);
  4. let timerId = useRef(); // {current:null}
  5. useEffect(()=>{
  6. timerId.current = setInterval(()=>{
  7. setCount(count=>count+1)
  8. },1000)
  9. },[]);
  10. const stopCount = ()=>{
  11. console.log(timerId) // {current: 定时器ID}
  12. clearInterval(timerId.current)
  13. }
  14. return <div>
  15. {count}
  16. <button onClick={stopCount}> stop </button>
  17. </div>
  18. }

自定义Hook

目的

在组件之间实现资源共享,共享的通常是一套逻辑,如发送请求获取博客文章,有很多组件需要做这件事,就可以把这段逻辑封装成一个 Hook。自定义hook是标准的封装和共享逻辑的方式。

固定格式

自定义hook是一个函数,其名称以use开头
自定义hook其实就是逻辑和内置hook的组合
它可以返回任何需要的数据和方法

  1. // 例1
  2. function useGetPost() {
  3. const [post, setPost] = userState({})
  4. useEffect(()=> {
  5. axios.get(xxx).then(res=> setPost(res.data))
  6. }, [])
  7. return [post, setPost]
  8. }
  9. function App() {
  10. const [post, setPost] = useGetPost()
  11. return (
  12. <div>{post.title}</div>
  13. )
  14. }
  1. // 例2
  2. function useUpdateInput (initialValue) {
  3. const [value, setValue] = useState(initialValue)
  4. return {
  5. value,
  6. onChange: event => setValue(event.target.value)
  7. }
  8. }
  9. function App() {
  10. const username = useUpdateInput('')
  11. const password = useUpdateInput('')
  12. const submitForm = event => {
  13. event.preventDefault();
  14. console.log(username.value)
  15. console.log(password.value)
  16. }
  17. return (
  18. <form onSubmit={submitForm}>
  19. <input type='text' {...username} />
  20. <input type='password' {...password} />
  21. <input type='submit'>
  22. </form>
  23. )
  24. }

React路由Hooks

react-router-dom 提供的路由钩子函数

  1. import { useHistory, useLocation, useRouteMatch, useParams } from 'react-router-dom'
  2. export default function Home(props) {
  3. console.log(props)
  4. console.log(useHistory())
  5. console.log(useLocation())
  6. console.log(useRouteMatch())
  7. console.log(useParams())
  8. }
  • useHistory:获取 history 对象
  • useLocation:获取 location 对象
  • useRouteMatch:获取 match 对象
  • useParams:获取 match 对象下的 params 对象(params 对象存储路由参数)

    useState 原理

  • 初始值(任意类型)

  • 初始值就是只初始化一次—-脱离useState函数存在,判断是否有值,
  • 返回值为数组[变量,设置变量的方法]
  • useState可以调用多次—-使用数组这个数据结构保存起来,闭包缓存index值
  1. import React from 'react'
  2. import ReactDOM from 'react-dom'
  3. // 这些必须脱离函数存在,否则组件重新渲染后,函数内的变量会被重新初始化
  4. let state = []
  5. let setters = []
  6. // 声明下标
  7. let stateIndex = 0
  8. function render() {
  9. // 这里需要清零是因为:重新渲染后 stateIndex 不会自动归零,而是继续累加
  10. // 如果不清零,在重新渲染后 useState 中的 value 和 setter 便无法取得渲染前对应的 state 数组和 setter 数组中的值
  11. stateIndex = 0
  12. // 重新渲染视图
  13. ReactDOM.render(<App />, document.getElementById('root'))
  14. }
  15. function createSetter(index) {
  16. // 通过闭包【保留索引】不被释放
  17. // 下面这个函数中的 index 永远都是上面这个函数被调用时传进去的那个 index 值
  18. return function (newState) {
  19. // 所以这里修改的也永远是这个 index 对应的 state
  20. state[index] = newState
  21. // 重新渲染视图,所以每次调用 setState 都会重新渲染组件
  22. render()
  23. }
  24. }
  25. function useState(initialState) {
  26. // 1. 能取就取存下来的值,没有就用初始值
  27. state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState
  28. // 2. 重新渲染后都要【按顺序】重新创建【所有的】 setter,所以 useState 声明的顺序不能随意变动
  29. setters.push(createSetter(stateIndex))
  30. // 3. 在数组中取值:确保【对应顺序】
  31. let value = state[stateIndex]
  32. let setter = setters[stateIndex]
  33. // 4. 为下一个使用 useState 的情况做准备
  34. stateIndex++
  35. // 5. 以数组形式返回
  36. return [value, setter]
  37. }
  38. function App() {
  39. const [count, setCount] = useState(0)
  40. const [name, setName] = useState('Mary')
  41. return (
  42. <div>
  43. {count}
  44. <button onClick={() => setCount(count + 1)}>setCount</button>
  45. {name}
  46. <button onClick={() => setName('James')}>setName</button>
  47. </div>
  48. )
  49. }
  50. export default App

useEffect 原理

  1. import React from 'react'
  2. import ReactDOM from 'react-dom'
  3. let state = []
  4. let setters = []
  5. let stateIndex = 0
  6. function render() {
  7. stateIndex = 0
  8. // 每次渲染都会归零
  9. effectIndex = 0
  10. ReactDOM.render(<App />, document.getElementById('root'))
  11. }
  12. function createSetter(index) {
  13. return function (newState) {
  14. state[index] = newState
  15. render()
  16. }
  17. }
  18. function useState(initialState) {
  19. state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState
  20. setters.push(createSetter(stateIndex))
  21. let value = state[stateIndex]
  22. let setter = setters[stateIndex]
  23. stateIndex++
  24. return [value, setter]
  25. }
  26. // 记录上一次的依赖值(useEffect 第二个参数中的变量值)
  27. let prevDepsAry = []
  28. let effectIndex = 0
  29. function useEffect(callback, depsAry) {
  30. if (Object.prototype.toString.call(callback) !== '[object Function]') {
  31. // 验证 callback 是不是【函数】
  32. throw new Error('useEffect函数的第一个参数必须是函数')
  33. }
  34. // 判断 depsAry 有没有被传递
  35. if (typeof depsAry === 'undefined') {
  36. // 没有传递
  37. callback()
  38. } else {
  39. if (Object.prototype.toString.call(depsAry) !== '[object Array]') {
  40. // 验证 depsAry 是不是【数组】
  41. throw new Error('useEffect函数的第二个参数必须是数组')
  42. }
  43. // 获取上一次的依赖值
  44. let prevDeps = prevDepsAry[effectIndex]
  45. // 将当前的依赖值和上一次的依赖值做对比 如果有变化(即:depsAry.every === false) 则调用callback
  46. let hasChanged = prevDeps ? depsAry.every((dep, index) => dep === prevDeps[index]) === false : true
  47. if (hasChanged) {
  48. // 依赖值有变化
  49. callback()
  50. }
  51. // 同步依赖值
  52. prevDepsAry[effectIndex] = depsAry
  53. // 为下一次调用 useEffect 做准备
  54. effectIndex++
  55. }
  56. }
  57. function App() {
  58. const [count, setCount] = useState(0)
  59. const [name, setName] = useState('Mary')
  60. useEffect(() => {
  61. console.log('count')
  62. }, [count])
  63. useEffect(() => {
  64. console.log('name')
  65. }, [name])
  66. return (
  67. <div>
  68. {count}
  69. <button onClick={() => setCount(count + 1)}>setCount</button>
  70. {name}
  71. <button onClick={() => setName('James')}>setName</button>
  72. </div>
  73. )
  74. }
  75. export default App

useReducer 原理

  1. import React from 'react'
  2. import ReactDOM from 'react-dom'
  3. let state = []
  4. let setters = []
  5. let stateIndex = 0
  6. function render() {...
  7. }
  8. function createSetter(index) {...
  9. }
  10. function useState(initialState) {...
  11. }
  12. let prevDepsAry = []
  13. let effectIndex = 0
  14. function useEffect(callback, depsAry) {...
  15. }
  16. // useState 的增强版本
  17. function useReducer(reducer, initialState) {
  18. // 初始值其实是为 useState 传入的
  19. const [state, setState] = useState(initialState)
  20. // 精妙的设计 dispatch
  21. function dispatch(action) {
  22. // 调用传入的 reducer 方法
  23. const newState = reducer(state, action)
  24. // 使用 reducer 返回的值来为 state 重新赋值
  25. setState(newState)
  26. }
  27. // 以数组形式返回
  28. return [state, dispatch]
  29. }
  30. function App() {
  31. // 定义传入 useReducer 的第一个参数,reducer 会被 useReducer 中定义的 dispatch 调用
  32. function reducer(state, action) {
  33. // 为同一个 state 以不同的 action 方式进行变换
  34. switch (action.type) {
  35. case 'increment':
  36. // 返回什么,新的 state 就是什么
  37. return state + 1
  38. case 'decrement':
  39. // 返回什么,新的 state 就是什么
  40. return state - 1
  41. default:
  42. return state
  43. }
  44. }
  45. const [count, dispatch] = useReducer(reducer, 0)
  46. return (
  47. <div>
  48. {count}
  49. <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
  50. <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
  51. </div>
  52. )
  53. }
  54. export default App