React-Hooks

163 阅读8分钟

一、认识和体验Hooks

1. class组件相对于函数式组件的优势

  • class组件可以定义自己的state,用来保存组件自己内部的状态;函数式组件每次调用都会产生新的临时变量;
  • class组件有自己的生命周期,可以在对应的生命周期中完成自己的逻辑;
  • class组件可以在状态改变时只会执行render函数以及生命周期函数componentDidUpdate;函数式组件在重新渲染时,整个函数都会被执行。

2. class组件存在的问题

  • 复杂组件变得难以理解;
  • 组件复用状态很难。

3. Hook的出现

Hook可以在不编写class的情况下使用state以及其他的React特性

  • Hook的使用场景
    • Hook的出现基本可以代替我们之前所有使用class组件的地方;
    • 如果是一个旧的项目,并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
    • Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用。
  • Hook的特点
    • 完全可选的;
    • 100%向后兼容的;
    • 现在可用的;
    • 代码变得非常简洁/不用考虑this相关的问题。

4. class组件和函数式组件的对比

  • class组件
import React, { PureComponent } from 'react'

export class CounterClass extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      counter: 0
    }
  }

  increment() {
    this.setState({ counter: this.state.counter + 1 })
  }
  
  decrement() {
    this.setState({ counter: this.state.counter - 1 })
  }

  render() {
    const { counter } = this.state
    return (
      <div>
        <h2>当前计数:{counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
        <button onClick={e => this.decrement()}>-1</button>
      </div>
    )
  }
}

export default CounterClass
  • 函数式组件
import React, { memo, useState } from 'react'

// 普通的函数,里面不能使用hooks
// 在自定义的hooks中,可以使用react提供的其他hooks:必须使用use开头
const CounterHook = memo(() => {
  const [counter, setCounter] = useState(0)
  
  return (
    <div>
      <h2>当前计数:{counter}</h2>
      <button onClick={e => setCounter(counter + 1)}>+1</button>
      <button onClick={e => setCounter(counter - 1)}>-1</button>
    </div>
)
})

export default CounterHook

二、State/Effect

1. useState

  • 认识useState
    • State Hook的API就是useState,来自react,需要从react中导入;
    • 会帮助我们定义一个state变量,useState一种新方法,它与class里面的this.state提供的功能完全相同。
  • 参数:初始化值,如果不设置为undefined。
  • 返回值:数组,包含两个元素
    • 参数一:当前状态的值;
    • 参数二:设置状态值的参数。
  • 使用规则
    • 只能在函数最外层调用Hook。不要在循环、条件判断或者子函数中调用;
    • 只能在React的函数组件中调用Hook。不要在其他的JavaScript函数中调用。
    • state只在组件首次渲染的时候被创建,在下一次渲染时,useState返回给我们当前的state。
import React, { memo, useState } from 'react'

const App = memo(() => {
  const [message, setMessage] = useState("Hello World")

  function changeMessage() {
    setMessage("你好啊,李银河")
  }

  return (
    <div>
      <h2>App: {message}</h2>
      <button onClick={changeMessage}>changeText</button>
    </div>
  )
})

export default App

2. useEffect

  • 认识Effect Hook
    • 事实上,类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用;
    • 对于完成这些功能的Hook被称之为Effect Hook;
    • Effect Hook可以完成一些类似于class中生命周期的功能。
  • useEffect的用法
    • 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;
    • useEffect两个参数:
      • 要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数;
      • 该useEffect在哪些state发生变化时,才重新执行。
    • 默认情况下,无论是第一次渲染,还是每次更新之后,都会执行这个回调函数。
  • 清除Effect
    • Effect会返回一个函数,这是effect可选的清除机制,每个effect都可以返回一个清除函数。
    • 会在组件更新和卸载的时候执行清除操作。
  • 使用多个Effect
    • 使用Effect Hook,可以将他们分离到不同的useEffect中;
    • Hook允许我们按照代码的用途分离它们,而不是像生命周期函数那样。
import React, { memo, useState, useEffect } from 'react'

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

  // 负责告知react,在执行完当前组件渲染之后要执行的副作用代码
  useEffect(() => {
    // 1. 修改document的title
    console.log("修改title")
  })

  // 一个函数式组件中,可以存在多个useEffect
  useEffect(() => {
    // 2. 对redux中数据变化监听
    console.log("监听redux中的数据")
    return () => {
      // 取消redux中数据的监听
    }
  }, [])

  useEffect(() => {
    // 3. 监听eventBus中的why事件
    console.log("监听eventBus的why事件")
    return () => {
      // 取消eventBus中的why事件监听
    }
  }, [])

  useEffect(() => {
    console.log("发送网络请求,从服务器中获取数据")
    return () => {
      console.log("会在组件被卸载时,才会执行一次")
    }
  }, [])
  return (
    <div>
      <button onClick={e => setCount(count+1)}>+1({count})</button>
    </div>
  )
})

export default App

Hook指的类似于useState、useEffect这样的函数;Hooks是对这类函数的统称。

三、Context/Reducer

1. useContext

  • 类组件使用共享的Context的两种方式
    • 类组件可以通过 类名.contextType = MyContext 方式,在类中获取context;
    • 多个context或者在函数式组件中通过MyContext.Consumer方式共享context。
  • Context Hook
    • Context Hook允许我们通过Hook来获取某个Context的值。
// --- index.js ---
import React from 'react';
import ReactDOM from 'react-dom/client';
import { UserContext, ThemeContext } from "./context"

import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <UserContext.Provider value={{name: "why", level: 99}}>
    <ThemeContext.Provider value={{color: "red", size: 30}}>
      <App />
    </ThemeContext.Provider>
  </UserContext.Provider>
);
// --- context/index.js ---
import { createContext } from "react";

const UserContext = createContext()
const ThemeContext = createContext()

export {
  UserContext,
  ThemeContext
}
// --- App.jsx ---
import React, { memo, useContext } from 'react'
import { UserContext, ThemeContext } from "./context"

const App = memo(() => {
  // 使用Context
  const user = useContext(UserContext)
  const theme = useContext(ThemeContext)

  return (
    <div>
      <h2>User: {user.name}-{user.level}</h2>
      <h2 style={{color: theme.color, fontSize: theme.size}}>Theme</h2>
    </div>
  )
})

export default App

2. useReducer

  • useReducer仅仅是useState的一种替代方案:
    • 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分;
    • 或者这次修改的state需要依赖之前的state时,也可以使用。
import React, { memo, useReducer } from 'react'

function reducer(state, action) {
  switch(action.type) {
    case "increment":
      return {...state, counter: state.counter + 1}
    case "decrement":
      return {...state, counter: state.counter - 1}
    case "add_number":
      return {...state, counter: state.counter + action.num}
    case "sub_number":
      return {...state, counter: state.counter - action.num}
    default:
      return state
  }
}

const App = memo(() => {
  const [state, dispatch] = useReducer(reducer, { counter: 0})

  return (
    <div>
      <h2>当前计数:{state.counter}</h2>
      <button onClick={e => dispatch({type: "increment"})}>+1</button>
      <button onClick={e => dispatch({type: "decrement"})}>-1</button>
      <button onClick={e => dispatch({type: "add_number", num: 5})}>+5</button>
      <button onClick={e => dispatch({type: "sub_number", num: -5})}>-5</button>
      <button onClick={e => dispatch({type: "add_number", num: 100})}>+100</button>
    </div>
  )
})

export default App

四、Callback/Memo

1. useCallback

  • useCallback实际的目的是为了进行性能的优化。
  • 如何进行性能优化:
    • useCallback会返回一个函数的记忆的值;
    • 在依赖不变的情况下,多次定义的时候,返回的值是相同的。
import React, { memo, useState, useCallback, useRef } from 'react'

const HYIncrement = memo((props) => {
  const { increment } = props
  return  (
    <div>
      <button onClick={increment}>increment+1</button>
    </div>
  )
})

const App = memo(() => {
  const [count, setCount] = useState(0) 
  
  // 做法一:将count依赖移除掉,缺点:闭包陷阱
  // const increment = useCallback(function foo() {
  //   setCount(count + 1)
  // }, [count])
  
  // 做法二:useRef,在组件多次渲染时,返回的是同一个值
  const countRef = useRef()
  countRef.current = count
  const increment = useCallback(function foo() {
    console.log("increment")
    setCount(countRef.current + 1)
  }, [])
  return (
    <div>
      <h2>计数:{count}</h2>
      <button onClick={increment}>+1</button>
      <HYIncrement increment={increment}/>
    </div>
  )
})

export default App

2. useMemo

  • useMemo实际的目的也是为了性能的优化。
  • 如何进行性能优化:
    • useCallback返回也是一个函数的记忆的值;
    • 在依赖不变的情况下,多次定义的时候,返回的值是相同的。
import React, { memo, useCallback, useMemo, useState } from 'react'

const HelloWorld = memo(function(props) {
  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)

  // 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 info={info} />
    </div>
  )
})

export default App

五、Ref/LayoutEffect

1. useRef

  • useRef返回的是ref对象,返回的ref对象在组件的整个生命周期保持不变。
  • 最常用的两种用法:
    • 用法一:引入DOM元素;
    • 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变。
import React, { memo, useState, useRef } from 'react'

const App = memo(() => {
    const [count, setCount] = useState(0)
    
  // 1. 引入DOM元素
  const inputRef = useRef()

  function showTitleDom() {
    inputRef.current.focus()
  }
  
  // 2. 使用ref保存某值
  const countRef = useRef()
  countRef.current = count

  const increment = useCallback(() => {
    setCount(countRef.current + 1)
  }, [])
  return (
    <div>
      <input type="text" ref={inputRef} />
      <button onClick={showTitleDom}>查看title的dom</button>
      
      <h2>Hello World: {count}</h2>
      <button onClick={e => setCount(count+1)}>+1</button>
      <button onClick={increment}>+1</button>
    </div>
  )
})

export default App

2. useImperativeHandle

  • 通过useImperativeHandle可以只暴露子组件固定的操作。
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() {
    inputRef.current.focus()
    inputRef.current.setValue("hahaha")
  }
  return (
    <div>
      <h2 ref={titleRef}>哈哈哈</h2>
      <HelloWorld ref={inputRef}/>
      <button onClick={handleDOM}>DOM操作</button>
    </div>
  )
})

export default App

3. useLayoutEffect

  • useEffect和useLayoutEffect的区别:
    • useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
    • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新。
import React, { memo, useLayoutEffect,useEffect, useState } from 'react'

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

  // 组件已经被渲染但是尚未显示到屏幕上之前执行
  useLayoutEffect(() => {
    console.log("useLayoutEffect")
  })

  // 组件被渲染且已经显示到屏幕上之后执行
  useEffect(() => {
    console.log("useEffect")
  })

  console.log("App render")
  

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

export default App

六、Redux中的hooks

1. useSelector

  • useSelector的作用是将state映射到组件上。
  • 两个参数:
    • 将state映射到需要的数据中;
    • 可以进行比较来决定是否组件重新渲染(shallowEqual可进行浅层比较)。

2. useDispatch

  • useDispatch就是直接获取dispatch函数,之后在组件中直接使用即可。
import React, { memo } from 'react'
import { useSelector, useDispatch, shallowEqual } from "react-redux"
import { addNumberAction, changeMessageAction, subNumberAction } from './store/modules/counter'

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))
    }
  }

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

export default App