React中的奇妙Hooks

773 阅读10分钟

Hooks的出现

每一个新技术的出现都是为了解决之前技术的难点

在React16.8之后新增特性(Hook)不编写class的情况下使用state以及其他的React特性(比如生命周期)。

Class组件的优势 (16.8之前)

  • class可以定义自己的state 用来保存组件内部的状态
    • 函数组件不可以 因为函数组件每次调用都会产生新的临时变量,变量不会多次保存起来
  • class可以有自己的生命周期 eg:componentDidMount
    • 函数组件在hook之前 是没有生命周期的 因为每一次重新渲染都会导致重新执行一遍生命周期
  • class组件可以在状态改变的时候只执行render函数以及执行componentDidUpdate
    • 函数组件重新渲染的话 只会整体重新执行

Class 存在的问题点

  • class类组件随着业务的迭代 组件会变得越来越复杂 并且难以拆分 增加代码的复杂度以及可阅读性
  • Es6中class是学习React的一个障碍 在class中必须搞清楚this指向的问题 以及原型&原型链的知识点
  • class组件的服用只能通过高阶组件来进行封装 比如在redux中的connectreact-router中的withRouter 这些都是为了让组件复用。类似于Provide以及Consumer数据的共享较多的时候也会让组件难以阅读

Hook的出现

  • 他可以不编写class组件的情况下来使用state和React其他特性
  • Hook的出现可以替代之前class组件 并且它是完全的向下兼容,并不需要来对class组件的重构
  • Hook只能在函数式组件中使用 不可以在类组件以及函数组件外使用

Hooks-useState(来自于React包 )

  • 参数:初始化值 如果不设置则为undefined
  • 返回值: 数组类型
    • 元素一:当前状态的值(第一次调用初始化值)
    • 元素二:设置状态值的函数
实现的原理

因为在函数执行的时候 他会把return返回的jsx来传递给React内部=>React.CreateElement =>ReactElement=>形成虚拟DOM。这个时候函数内部其他变量都会**"消失"**

为什么会"消失"呢? 因为在js执行过程中,V8引擎内部会产生一个执行上下文调用栈(EC stack)会在当前执行的函数 过程中创建一个函数执行上下文(FEC) 会在创建当前函数的内部的变量(VO) 包括函数的作用域链,一旦函数执行完毕 FEC就会在ECS中弹出 并且销毁它本身带的VO 所以说函数内部的变量会消失 具体js执行原理可以参照我的文章:juejin.cn/post/712414…

但是包裹useState中的变量他会被React进行保存, 他会在React内部生成一个函数 在函数内部把变量保存起来 并且赋予上初始值,当使用函数的时候 会根据函数的参数传入的值 给变量赋予最新的值 当组件重新渲染的时候 把最新的值返回出去

FAQ: 为什么叫做useState而不叫createState

  • 因为state只是在首次渲染的时候 才会被创建 下一次渲染的时候 useState返回当前state

Effect Hook

  • 可以实现class类组件中生命周期&网络请求&手动更新DOM 一切Rom更新DOM的副作用(Side Effects)
    • 通过useEffect可以告诉React在渲染完DOM之后执行某些操作
    • 要求我们传入一个回调函数 在React更新完DOM后进行回调这个函数
    • 默认情况下 无论第一次渲染还是重新渲染 都会执行回调函数 可以通过第二个参数来控制哪些state变化需要重新执行
    • 可以通过return返回值 返回一个回调函数来进行清除工作
    • type EffectCallback = () => (void | () => void | undefined)

使用场景

  • 比如手动调用Redux中的subscribe或者事件总线
  • 生命周期 componentDidMount & componentWillUnmount
  • 可以调用多个useEffect 来进行生命周期的拆分 代码逻辑的解藕
import { memo, useEffect }  from 'react'
import { useState } from 'react'

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

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

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

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

  useEffect(() => {
    console.log("发送网络请求, 从服务器获取数据")

    return () => {
      console.log("会在组件被卸载时, 才会执行一次")
    }
  }, [])

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

export default App

useContext

在开发中想要使用组件中共享的Context有两种方式

  • 类组件可以通过 类名.contextType = MyContext方式,在类中获取context;
  • 多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context;

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

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

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 context value 值。

useReducer 不建议使用 让逻辑拆分的更加难以阅读

useReducer并不是Redux的某个替代品它仅仅只是useState一种替代方案

  • 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分;
  • 或者这次修改的state需要依赖之前的state时,也可以使用;
import React, { memo, useReducer } from 'react'
// import { useState } 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
  }
}

// useReducer+Context => redux

const App = memo(() => {
  // const [count, setCount] = useState(0)
  const [state, dispatch] = useReducer(reducer, { counter: 0, friends: [], user: {} })

  // const [counter, setCounter] = useState()
  // const [friends, setFriends] = useState()
  // const [user, setUser] = useState()

  return (
    <div>
      {/* <h2>当前计数: {count}</h2>
      <button onClick={e => setCount(count+1)}>+1</button>
      <button onClick={e => setCount(count-1)}>-1</button>
      <button onClick={e => setCount(count+5)}>+5</button>
      <button onClick={e => setCount(count-5)}>-5</button>
      <button onClick={e => setCount(count+100)}>+100</button> */}

      <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

useCallback 性能优化

  • useCallback 其实他内部是对返回的函数进行了memoized(记忆)的效果
  • 在依赖不变化的情况下 多次定义的时候 返回值都是相同的
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>
  )
})


// function foo(name) {
//   function bar() {
//     console.log(name)
//   }
//   return bar
// }

// const bar1 = foo("why")
// bar1() // why
// bar1() // why

// const bar2 = foo("kobe")
// bar2() // kobe

// bar1() // why

export default App
  1. 首先他并不是没有新生成一个函数内存的角度来讲 在编译期间就已经定义生成了一个新的函数, 而useCallback是通过判断依赖的state是否有变化而进行选择记忆的函数,还是返回新生成的函数
  2. 如果不设置依赖值 就会出现闭包陷阱,也就是当我获取记忆中的函数时 因为内部的变量不会进行重定义 所以就会导致当修改变量的时候 记忆中的函数不会进行更新
  3. 但是可以通过useCallback来进行对子组件进行性能优化,当组件重新刷新的时候 通过依赖state没有变化来返回记忆的函数 从而达到子组件不进行重新Render
  4. 通常情况下使用useCallback的目的是不希望子组件进行多次渲染 而不是对函数进行缓存操作

useMemo & useCallback

性能优化 跟useCallback 效果类似 只不过它跟回调函数的返回值进行记忆

  • 对子组件传递相同内容的对象时,使用useMemo进行性能的优化
  • 进行大量的计算操作,是否有必须要每次渲染时都重新计算
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

useRef

  • useRef返回一个ref对象,返回的ref对象再组件的整个生命周期保持不变。
  • 引入DOM(或者组件,但是需要是class组件)元素 绑定DOM
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
  • 保存一个数据,这个对象在整个生命周期中可以保存不变
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)
  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

  • 在父子组件中 可以通过ref和forwardRef结合从父组件传递给子组件ref值
  • 这种做法本身没问题 但是缺点是子组件的DOM直接全部暴露给父组件了。这样父组件可以任意修改子组件DOM
  • 可以通过useImperativeHandle 来进行暴露固定哪些操作
    • 通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起;
    • 所以在父组件中,使用 inputRef.current时,实际上使用的是返回的对象;
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

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

image.png

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

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

  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

redux hooks(useSelector|useDispatch)

在开发redux中 使用了react-redux 中的connect 来进行组件中使用redux的state 和 dispatch

  • 但是这种方式必须使用高阶函数结合返回的高阶组件;
  • 并且必须编写:mapStateToPropsmapDispatchToProps映射的函数;

在Redux7.1之后 提供了Hook的方式 来代替connect以及映射函数了

  • 参数一:将state映射到需要的数据中;
  • 参数二:可以进行比较来决定是否组件重新渲染;可以使用react-redux中提供的shallowEqual方法
import React, { memo } from 'react'
import { useSelector, useDispatch, shallowEqual } from "react-redux"
import { addNumberAction, changeMessageAction, subNumberAction } from './store/modules/counter'


// memo高阶组件包裹起来的组件有对应的特点: 只有props发生改变时, 才会重新渲染
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