React Hooks详解

811 阅读23分钟

一、体验Hooks

1.1 Hooks介绍

1.1.1 类组件的优势

  • class组件可以定义自己的state,用来保存组件自己内部的状态

    • 函数式组件不可以,因为函数每次调用都会产生新的临时变量
  • class组件有自己的生命周期,可以在对应的生命周期中完成自己的逻辑

    • 比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次

    • 函数式组件在出现hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求

  • class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate

    • 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次

1.1.2 类组件的问题

  • 复杂组件变得难以理解:

    • 最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂。但是随着业务的增多,class组件会变得越来越复杂

    • 比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在componentWillUnmount中移除)

    • 而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度

  • 难以理解的class:

    • 在class中,必须搞清楚this的指向到底是谁
  • 组件复用状态很难:

    • 在前面为了一些状态的复用需要通过高阶组件
    • 这些代码让我们不管是编写和设计上来说,都变得非常困难

1.1.3 Hooks

  • Hook 是 React 16.8 的新增特性,能在不编写class的情况下使用state以及其他的React特性(比如生命周期)。

  • Hook的使用场景:

    • Hook的出现基本可以代替所有使用class组件的地方

    • 如果是一个旧的项目,也并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它

    • Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用

1.2 计数器案例

  • 类组件实现
import React, { PureComponent } from 'react'

export default 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>
    )
  }
}
  • 函数式组件 + Hooks
import React, { memo, useState } from 'react'

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
  • 分析

    • 函数式组件结合hooks让整个代码变得非常简洁
    • 并且再也不用考虑this相关的问题

二、State Hook

2.1 认识useState

  • useState来自react,需要从react中导入

    • 参数:初始化值,如果不设置为undefined

    • 返回值:数组,包含两个元素

      • 元素一:当前状态的值(第一调用为初始化值)

      • 元素二:设置状态值的函数

    • 点击button按钮后,会完成两件事情

      • 调用setCounter,设置一个新的值

      • 组件重新渲染,并且根据新的值返回DOM结构

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

  • 但是使用它们会有两个额外的规则

    • 只能在函数最顶层调用 Hook不要在循环、条件判断或者子函数中调用

    • 只能在 React 的函数组件中调用 Hook,不要在其他 JavaScript 函数(可以在自定义hook中使用,自定义hook必须以use开头命名)中调用

2.2 useState 分析

  • useState会帮助我们定义一个 state变量,useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同

    • 一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留
  • useState接受唯一一个参数,在第一次组件被调用时使用来作为初始化值。(如果没有传递参数,那么初始化值为undefined)。

  • useState的返回值是一个数组,可以通过数组的解构,来完成赋值会非常方便。

  • FAQ:为什么叫 useState 而不叫 createState?

    • “create” 可能不是很准确,因为 state 只在组件首次渲染的时候被创建
    • 在下一次重新渲染时,useState 返回给我们当前的 state
    • 如果每次都创建新的变量,它就不是 “state”了
    • 这也是 Hook 的名字总是以 use 开头的一个原因
  • 可以在一个组件中定义多个变量和复杂变量(数组、对象)

    • const [banners, setBanners] = useState([])

三、Effect Hook

3.1 useEffect使用

3.1.1 认识Effect Hook

  • Effect Hook 可以完成一些类似于class中生命周期的功能

  • 事实上,类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects),所以对于完成这些功能的Hook被称之为 Effect Hook

3.1.2 useEffect的解析

  • 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作

  • useEffect要求传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数

  • 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数

3.1.3 修改页面的title案例

  • 类组件实现
import React, { PureComponent } from 'react'

export default class App extends PureComponent {
  constructor() {
    super()

    this.state = {
      counter: 100
    }
  }
  componentDidMount() {
    document.title = this.state.counter
  }
  componentDidUpdate() {
    document.title = this.state.counter
  }
  increment() {
    this.setState({
      counter: this.state.counter + 1
    })
  }
  render() {
    const { counter } = this.state
    return (
      <div>
        <h2>counter: {counter}</h2>
        <button onClick={e => this.increment()}>+1</button>
      </div>
    )
  }
}
  • 函数式组件实现
import React, { memo, useEffect, useState } from 'react'

const App = memo(() => {
  const [counter, setCounter] = useState(200)
  
  useEffect(() => {
    // 当前传入的回调函数会在组件被渲染完成后, 自动执行
    // 网络请求/DOM操作(修改标题)/事件监听
    document.title = counter
  })

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

export default App

3.2 清除Effect

  • 在class组件的编写过程中,某些副作用的代码,需要在componentWillUnmount中进行清除:

    • 比如事件总线或Redux中手动调用subscribe
    • 都需要在componentWillUnmount有对应的取消订阅
  • useEffect传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B

  • 为什么要在 effect 中返回一个函数?

    • 这是 effect 可选的清除机制
    • 每个 effect 都可以返回一个清除函数,如此可以将添加和移除订阅的逻辑放在一起
    • 它们都属于 effect 的一部分
  • React 何时清除 effect?

    • React 会在组件更新和卸载的时候执行清除操作
    • 正如上面提到的,effect 在每次渲染的时候都会执行
import React, { memo, useEffect, useState } from 'react'

const App = memo(() => {
  const [counter, setCounter] = useState(200)
  
  // 负责告知react,在执行完当前组件渲染之后要执行的副作用代码
  useEffect(() => {
    // 1.监听事件
    // const unubscribe = store.subscribe(() => {
    // })
    // function foo() {
    // }
    // eventBus.on("xxx", foo)
    console.log("监听redux中数据变化, 监听eventBus中的xxx事件")

    // 返回值: 回调函数 => 组件被重新渲染或者组件卸载的时候执行
    return () => {
      console.log("取消监听redux中数据变化, 取消监听eventBus中的xxx事件")
    }
  })

  return (
    <div>
      <h2>counter: {counter}</h2>
      <button onClick={e => setCounter(counter + 1)}>+1</button>
    </div>
  )
})
export default App

3.3 使用多个Effect

  • 使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题

    • 比如网络请求、事件监听、手动修改DOM,这些往往都会放在componentDidMount
  • Hook 允许按照代码的用途分离它们,而不是像生命周期函数那样

    • React 将按照 effect 声明的顺序依次调用组件中的每一个 effect
import React, { memo, useEffect } from 'react'
import { useState } 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中的xxx事件
    console.log("监听eventBus的xxx事件")
    return () => {
      // 取消eventBus中的xxx事件监听
    }
  })

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

export default App

3.4 Effect性能优化

  • 默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题:

    • 某些代码只是希望执行一次即可,类似于componentDidMountcomponentWillUnmount中完成的事情;(比如网络请求、订阅和取消订阅)
    • 另外,多次执行也会导致一定的性能问题
  • 如何决定useEffect在什么时候应该执行和什么时候不应该执行呢?

    • useEffect实际上有两个参数:

      • 参数一:执行的回调函数
      • 参数二:该useEffect在哪些state发生变化时,才重新执行(受谁的影响)
  • 但是,如果一个函数我们不希望依赖任何的内容时,也可以传入一个空的数组 []

    • 那么这里的两个回调函数分别对应的就是componentDidMountcomponentWillUnmount生命周期函数了
import React, { memo, useEffect } from 'react'
import { useState } from 'react'

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

  // 仅在count改变时执行
  useEffect(() => {
    console.log("修改title", count)
  }, [count])

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

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

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

  return (
    <div>
      <h2>{count}</h2>
      <button onClick={e => setCount(count+1)}>+1</button>
      <h2>{message}</h2>
      <button onClick={e => setMessage("你好,师姐")}>修改文本</button>
    </div>
  )
})

export default App

四、Context/Reducer

4.1 useContext

  • 在组件中使用共享的Context有两种方式:

    • 类组件可以通过 类名.contextType = MyContext方式,在类中获取context;

    • 多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context;

  • 多个Context共享时的方式会存在大量的嵌套:

    • Context Hook允许通过Hook来直接获取某个Context的值
  • 注意事项:

    • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的context value 值
import React, { memo, useContext } from 'react'
import { ThemeContext, UserContext } from './context'

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

  return (
    <div>
      {/* <UserContext.Consumer>
        {
          value => {
            return <h2>{value.name}</h2>
          }
        }
      </UserContext.Consumer> */}

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

export default App

4.2 useReducer

  • useReducer仅仅是useState的一种替代方案:

    • 在某些场景下,如果state的处理逻辑比较复杂,可以通过useReducer来对其进行拆分;
    • 或者这次修改的state需要依赖之前的state时,也可以使用
  • 数据是不会共享的,他们只是使用了相同的 reducer

  • 注意

    • useReducer只是useState的一种替代品,并不能替代 Redux
    • 如果状态很多,仍推荐使用 Redux
import React, { memo, useReducer } from 'react'

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 }
  
    case 'decrement':
      return { ...state, count: state.count - 1 }

    case 'add_number':
      return { ...state, count: state.count + action.num }

    case 'sub_number':
      return { ...state, count: state.count - action.num }
    
      default:
      return state
  }
}

const App = memo(() => {
  // const [count, setCount] = useState(100)
  const [state, dispatch] = useReducer(reducer, { count: 0, frinends: [], 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.count}</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

5.1 useCallback

  • useCallback实际的目的是为了进行性能的优化

  • 如何进行性能的优化呢?

    • useCallback会返回一个函数的 memoized(记忆的) 值

    • 在依赖不变的情况下,多次定义的时候,返回的值是相同的

5.1.1 函数式组件中的多次函数定义

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

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

  // 每次调用都会 重新创建一个新的函数
  const increment = () => {
    setCount(count + 1)
  }

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

export default App

5.1.2 useCallback 记忆值的性能

  • 闭包陷阱
    • foo返回的函数 依赖上层的 name
function foo(name) {
  return function () {
    console.log(name)
  }
}

const bar = foo('李雷')
bar() // 李雷
bar() // 李雷

const baz = foo('韩梅梅')
baz() // 韩梅梅

bar() // 李雷
  • useCallback

    • useCallback第一个参数是一个回调函数

    • 第二个参数是数组,当前依赖的内容

      • 空数组:不传入依赖时,修改 count,会有闭包陷阱,原因是:useCallback返回的函数在依赖未发生改变时不会变化,执行的仍然是上一次的函数,依赖的count是0,结果仍然是1
      • 传入值时,依赖的值发生变化,useCallback返回一个新函数
import React, { memo, useCallback, useState } from 'react'

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

  // 闭包陷阱
  // const increment = useCallback(function() {
  //   setCount(count + 1)
  // }, [])

  const increment = useCallback(function() {
    setCount(count + 1)
  }, [count])

  return (
    <div>
      <h2>计数: {count}</h2>
      <button onClick={increment}>+1</button>
    </div>
  )
})
export default App
  • 将useCallback生成的函数传入子组件

    • 依赖的count发生变化时,useCallback生成的函数也会发生变化,会触发子组件的重新渲染;如果这里传入的是普通的函数,每次修改count,都会导致子组件的重新渲染,当子组件有很多内容的时候就会有很大的性能浪费
    • 未依赖的message发生变化时,不会触发子组件的渲染
import React, { memo, useCallback, useState } from 'react'

// props的属性发生改变时,组件本身就会被重新渲染
const Home = memo((props) => {
  const increment = props.increment
  console.log('Home 被渲染')

  return <button onClick={increment}>increment+1</button>
})

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

  const [message, setMessage] = useState('hello')

  // 每次调用都会 重新创建一个新的函数
  // const increment = () => {
  //   setCount(count + 1)
  // }

  // 闭包陷阱
  // const increment = useCallback(function() {
  //   setCount(count + 1)
  // }, [])

  const increment = useCallback(function() {
    setCount(count + 1)
  }, [count])

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

      <h2>message: {message}</h2>
      <button onClick={e => setMessage(Math.random())}>修改message</button>
    </div>
  )
})
export default App
  • useCallback的进一步性能优化

    • 当 count 发生变化时,也使用同一个函数
      1. 将count依赖移除,缺陷:闭包陷阱

      2. useRef:在组件多次渲染时,返回的是同一个值

        • 使用 countRef 保存每次修改的count的值
import React, { memo, useCallback, useRef, useState } from 'react'

// props的属性发生改变时,组件本身就会被重新渲染
const Home = memo((props) => {
  const increment = props.increment
  console.log('Home 被渲染')

  return <button onClick={increment}>increment+1</button>
})

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

  const [message, setMessage] = useState('hello')

  // 每次调用都会 重新创建一个新的函数
  // const increment = () => {
  //   setCount(count + 1)
  // }

  // const increment = useCallback(function() {
  //   setCount(count + 1)
  // }, [count])

  // 进一步优化:count发生变化时,也使用同一个函数
  const countRef = useRef()
  countRef.current = count
  const increment = useCallback(function() {
    console.log('increment')
    setCount(countRef.current + 1)
  }, [])

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

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

export default App

5.1.3 useCallback 总结

  • useCallback性能优化的点:

    • 当需要将一个函数传递给子组件时, 最好使用useCallback进行优化, 将优化之后的函数, 传递给子组件
  • 修改 count,如何使用同一个函数?

    • useCallback传入空依赖的数组
    • 借助 useRef 保存修改的count值

5.2 useMemo

  • useMemo实际的目的也是为了进行性能的优化

    • 通常使用useCallback的目的是不希望子组件进行多次渲染,并不是为了函数进行缓存
  • 如何进行性能的优化呢?

    • useMemo返回的也是一个 memoized(记忆的) 值
    • 在依赖不变的情况下,多次定义的时候,返回的值是相同的

5.2.1 具体用法

  • 模拟场景:需要计算 从 1 到 某个值 的和,我们封装一个函数,用来求和,但是每次修改count,组件会重新渲染,会导致这个求和函数频繁执行,实际上,求和的结果并没有变化

  • 使用 useMemo,传入求和的函数,不依赖其他值,就不会频繁执行求和函数

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

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)

  // 修改count会重新计算
  // const result = calcNumTotal(50)

  const result = useMemo(() => {
    return calcNumTotal(50)
  }, [])

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

export default App
  • 对子组件传递一个对象,修改count,当前组件重新渲染,会创建一个新的对象,这样就导致了子组件的重新渲染,可以使用useMemo优化
import React, { memo, useMemo, useState } from 'react'

// 子组件
const HelloWorld = memo(() => {
  console.log('Hello World 被渲染!')
  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)

  // 修改count会重新计算
  // const result = calcNumTotal(50)

  // 不依赖任何值
  const result = useMemo(() => {
    return calcNumTotal(50)
  }, [])

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

  // 对子组件渲染进行优化,修改count,子组件不会重新渲染
  // const info = { name: '独孤月' }
  const info = useMemo(() => ({ name: '独孤月' }), [])

  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

5.2.2 与 useCallback的对比

  • useCallback 优化的是函数,返回一个 有记忆 的 回调函数
  • useMemo 是优化函数的返回值,返回一个 记忆的
  • useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

5.2.3 useMemo 总结

  • 使用场景

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

六、Ref/LayoutEffect

6.1 useRef

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

  • 最常用的ref是两种用法:

    1. 引入DOM(或者组件,但是需要是class组件)元素
    2. 保存一个数据,这个对象在整个生命周期中可以保存不变

6.1 获取DOM

import React, { memo, useRef } from 'react'

const App = memo(() => {

  const titleRef = useRef()
  const inputRef = useRef()

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

  function inputFocus() {
    inputRef.current.focus()
  }

  return (
    <div>
      <h2 ref={titleRef}>Hello World</h2>
      <button onClick={showTitleDom}>查看title的dom</button>
      <div>
        <input type="text" ref={inputRef} />
        <button onClick={inputFocus}>获取焦点</button>
      </div>
    </div>
  )
})
export default App

6.2 保存数据

import React, { memo, useCallback, useRef, useState } from 'react'

let obj = null

const App = memo(() => {

  const [count, setCount] = useState(0)

  // 验证 ref的值是否是 同一个数据
  const nameRef = useRef()
  // 组件第一次渲染为 false, 修改count后 两者相等为 true
  console.log(obj === nameRef)
  obj = nameRef

  const countRef = useRef()
  countRef.current = count

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

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

export default App

6.2 useImperativeHandle

  • 回顾 refforwardRef 结合使用

    • 通过forwardRef可以将ref转发到子组件
    • 子组件拿到父组件中创建的ref,绑定到自己的某一个元素中
  • forwardRef的做法本身没有什么问题,但是这样就将子组件的DOM直接暴露给了父组件

    • 直接暴露给父组件带来的问题是某些情况的不可控
    • 父组件可以拿到DOM后进行任意的操作
    • 事实上,有时候我们并不希望父组件能够随意的操作DOM
  • 通过useImperativeHandle可以值暴露固定的操作:

  • useImperativeHandle 可以在使用 ref 时自定义暴露给父组件的实例值,也就是只暴露ref绑定的Dom固定的操作

    • 通过useImperativeHandle的Hook,将传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起
    • 所以在父组件中,使用 inputRef.current时,实际上使用的是返回的对象
    • 比如下面的代码中调用了 focus函数,setValue函数
import React, { memo, useRef, forwardRef, useImperativeHandle } from 'react'

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

  // 使用组件内部的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(titleRef.current)
    // console.log(inputRef.current)
    inputRef.current.focus()
    // inputRef.current.value = ''
    inputRef.current.setValue('hello ref')
  }

  return (
    <div>
      <h2 ref={titleRef}>哈哈哈</h2>
      <FancyInput ref={inputRef}/>
      <button onClick={handleDom}>DOM操作</button>
    </div>
  )
})
export default App

6.3 useLayoutEffect

  • useLayoutEffect看起来和useEffect非常的相似,区别如下:

    • useEffect在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新
    • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新
  • 下面代码的执行顺序:

    • render -> useLayoutEffect -> useEffect
    import React, { memo, useEffect, useLayoutEffect } from 'react'
    
    const App = memo(() => {
      useEffect(() => {
        console.log('useEffect')
      })
    
      useLayoutEffect(() => {
        console.log('useLayoutEffect')
      })
    
      console.log('render')
      return (
        <div>App</div>
      )
    })
    export default App
    
  • 如果希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect

    • 比如 现在我想 设置一个 count,当count为0时,给它设置一个随机数,这个时候如果使用useEffect屏幕会有闪烁的效果,可以使用useLayoutEffect
    import React, { memo, useEffect, useLayoutEffect, useState } from 'react'
    
    const App = memo(() => {
    
      const [count, setCount] = useState(100)
    
      useEffect(() => {
        console.log('useEffect')
        // if(count === 0) {
        //   setCount(Math.random() + 99)
        // }
      }, [count])
    
      useLayoutEffect(() => {
        console.log('useLayoutEffect')
        if(count === 0) {
          setCount(Math.random() + 99)
        }
      }, [count])
    
      console.log('render')
    
      return (
        <div>
          <h2>count: {count}</h2>
          <button onClick={e => setCount(0)}>设置为0</button>
        </div>
      )
    })
    
    export default App
    
  • 官方建议:尽可能使用标准的 useEffect 以避免阻塞视觉更新

七、自定义Hooks使用

自定义Hook本质上只是一种函数代码逻辑的抽取

  • 注意:自定义hook的函数名称必须以 use 开头

7.1 打印生命周期

  • 想要打印每个组件创建的销毁的时机
import React, { memo, useState, useEffect } from 'react'

function useLofLife(cName) {
  useEffect(() => {
    console.log(cName + "组件被创建")
    return () => {
      console.log(cName + "组件被销毁")
    }
  }, [cName])
}

const Home = memo(() => {
  useLofLife("Home")
  return <h1>Home Page</h1>
})

const About = memo(() => {
  useLofLife("About")
  return <h1>About Page</h1>
})

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

  useLofLife("App")

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

export default App

7.2 获取Context

  • 有两个Context:userContext、TokenContext

    import { createContext } from "react";
    
    const UserContext = createContext()
    const TokenContext = createContext()
    
    export {
      UserContext,
      TokenContext
    }
    
  • index.js 使用 Context

import React from 'react';
import ReactDOM from 'react-dom/client';

import { UserContext, TokenContext } from './context';

import App from './App'

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <UserContext.Provider value={{name: 'zhangsan', age: 20}}>
    <TokenContext.Provider value={'token'}>
      <App />
    </TokenContext.Provider>
  </UserContext.Provider>
);
  • 多个组件都需要获取 Context的值

    import React, { memo } from 'react'
    import { useUserToken } from './hooks'
    
    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
    
  • 封装一个 useUserTokenContext

    // useUserToken.js
    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
    

7.3 监听窗口滚动位置

  • 封装hook

    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
    
  • 组件中使用

    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
    

7.4 localStorage数据存储

  • 实现localStorage 和 state结合起来

    // useLocalStorage.js
    import { useEffect } from "react"
    import { useState } from "react"
    
    function useLocalStorage(key) {
      // 1.从localStorage中获取数据, 并且数据数据创建组件的state
      const [data, setData] = useState(() => {
        const item = localStorage.getItem(key)
        if (!item) return ""
        return JSON.parse(item)
      })
    
      // 2.监听data改变, 一旦发生改变就存储data最新值
      useEffect(() => {
        localStorage.setItem(key, JSON.stringify(data))
      }, [key, data])
    
      // 3.将data/setData的操作返回给组件, 让组件可以使用和修改值
      return [data, setData]
    }
    
    
    export default useLocalStorage
    
  • 组件中使用

    import React, { memo } from 'react'
    // import { useEffect } from 'react'
    // import { useState } from 'react'
    import useLocalStorage from './hooks/useLocalStorage'
    
    const App = memo(() => {
      // 通过key, 直接从localStorage中获取一个数据
      // const [token, setToken] = useState(localStorage.getItem("token"))
      // useEffect(() => {
      //   localStorage.setItem("token", token)
      // }, [token])
      const [token, setToken] = useLocalStorage("token")
      function setTokenHandle() {
        setToken("james")
      }
    
      // const [avatarUrl, setAvatarUrl] = useState(localStorage.getItem("avatarUrl"))
      // useEffect(() => {
      //   localStorage.setItem("avatarUrl", avatarUrl)
      // }, [avatarUrl])
      const [avatarUrl, setAvatarUrl] = useLocalStorage("avatarUrl")
      function setAvatarUrlHandle() {
        setAvatarUrl("http://www.james.com/cba.png")
      }
    
      return (
        <div className='app'>
          <h1>App Root Component: {token}</h1>
          <button onClick={setTokenHandle}>设置token</button>
          <h1>avatarURL: {avatarUrl}</h1>
          <button onClick={setAvatarUrlHandle}>设置新头像地址</button>
        </div>
      )
    })
    
    export default App
    

八、Redux的hook

8.1 介绍

  • 在之前的redux开发中,为了让组件和redux结合起来,我们使用了react-redux中的connect:

    • 但是这种方式必须使用高阶函数结合返回的高阶组件

    • 并且必须编写:mapStateToPropsmapDispatchToProps映射的函数

  • 在Redux7.1开始,提供了Hook的方式,我们再也不需要编写connect以及对应的映射函数了

  • useSelector的作用是将state映射到组件中:

    • 参数一:将state映射到需要的数据中

    • 参数二:可以进行比较来决定是否组件重新渲染

  • useSelector默认会比较我们返回的两个对象是否相等

    • 如何比较呢? const refEquality = (a, b) => a === b

    • 也就是我们必须返回两个完全相等的对象才可以不引起重新渲染

  • useDispatch非常简单,就是直接获取dispatch函数,之后在组件中直接使用即可

  • useStore来获取当前的store对象

8.2 代码案例

8.2.1 创建store(使用toolkit)

  • index.js

    import { configureStore } from "@reduxjs/toolkit"
    import counterReducer from "./modules/counter"
    
    const store = configureStore({
      reducer: {
        counter: counterReducer
      }
    })
    
    export default store
    
  • counter.js

    import { createSlice } from "@reduxjs/toolkit"
    
    const counterSlice = createSlice({
      name: "counter",
      initialState: {
        count: 99,
        message: "Hello World"
      },
      reducers: {
        addNumberAction(state, { payload }) {
          state.count = state.count + payload
        },
        subNumberAction(state, { payload }) {
          state.count = state.count - payload
        },
    
        changeMessageAction(state, { payload }) {
          console.log(payload)
          state.message = payload
        }
      }
    })
    
    export const { addNumberAction, subNumberAction, changeMessageAction } = counterSlice.actions
    export default counterSlice.reducer
    

8.2.2 传统用法

import React, { memo } from 'react'
import { connect } from "react-redux"
import { addNumberAction, subNumberAction } from './store/modules/counter'

const App = memo((props) => {
  const { count, addNumber, subNumber } = props

  function addNumberHandle(num, isAdd = true) {
    if (isAdd) {
      addNumber(num)
    } else {
      subNumber(num)
    }
  }

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

const mapStateToProps = (state) => ({
  count: state.counter.count
})

const mapDispatchToProps = (dispatch) => ({
  addNumber(num) {
    dispatch(addNumberAction(num))
  },
  subNumber(num) {
    dispatch(subNumberAction(num))
  }
})

export default connect(mapStateToProps, mapDispatchToProps)(App)

8.2.3 使用hook

import React, { memo } from 'react'
import { useSelector, useDispatch } from "react-redux"
import { addNumberAction, subNumberAction } from './store/modules/counter'

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

  // 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(1)}>+1</button>
      <button onClick={e => addNumberHandle(6)}>+6</button>
      <button onClick={e => addNumberHandle(6, false)}>-6</button>
    </div>
  )
})

export default App

8.2.4 shallowEqual

  • 使useSelector对store进行浅层比较,能够提高性能
    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
    

九、其他hooks补充

9.1 useId

9.1.1 SSR相关知识

  • SPA页面的缺点

    • 不利于SEO优化
    • 首屏渲染速度比较慢
  • 什么是SSR?

    • SSR(Server Side Rendering,服务端渲染),指的是页面在服务器端已经生成了完成的HTML页面结构,不需要浏览器通过执行JS代码,创建页面结构

    • 对应的是CSR(Client Side Rendering,客户端渲染),SPA页面通常依赖的就是客户端渲染

9.1.2 同构应用

  • 开发出来一个应用程序的代码, 既可以在服务器端运行也可以在客户端运行

  • 同构是一种SSR的形态,是现代SSR的一种表现形式。

    • 当用户发出请求时,先在服务器通过SSR渲染出首页的内容

    • 但是对应的代码同样可以在客户端被执行

    • 执行的目的包括事件绑定等以及其他页面切换时也可以在客户端被渲染

    image.png

  • 客户端运行的过程中: hydration

    • 在进行 SSR 时,页面会呈现为 HTML。但仅 HTML 不足以使页面具有交互性。例如,浏览器端 JavaScript 为零的页面不能是交互式的(没有 JavaScript 事件处理程序来响应用户操作,例如单击按钮)。

    • 为了使页面具有交互性,除了在 Node.js 中将页面呈现为 HTML 之外,UI 框架(Vue/React/...)还在浏览器中加载和呈现页面。(它创建页面的内部表示,然后将内部表示映射到在 Node.js 中呈现的 HTML 的 DOM 元素。),这个过程称为hydration

9.1.3 useId

  • useId 是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook

    • useId是用于react的同构应用开发的,前端的SPA页面并不需要使用它

    • useId可以保证应用程序在客户端和服务器端生成唯一的ID,这样可以有效的避免通过一些手段生成的id不一致,造成hydration mismatch

  • 简单演练

    import React, { memo, useId, useState } from 'react'
    
    const App = memo(() => {
      const [count, setCount] = useState(0)
    
      const id = useId()
      console.log(id)
    
      return (
        <div>
          <button onClick={e => setCount(count+1)}>count+1:{count}</button>
    
          <label htmlFor={id}>
            用户名:<input id={id} type="text" />
          </label>
        </div>
      )
    })
    
    export default App
    

9.2 useTransition

  • 官方解释:返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数

  • 理解:告诉react对于某部分任务的更新优先级较低,可以稍后进行更新

  • [pending, startTransition]

    import React, { memo, useState, useTransition } from 'react'
    import namesArray from './namesArray'
    
    const App = memo(() => {
      const [showNames, setShowNames] = useState(namesArray)
      const [ pending, startTransition ] = useTransition()
    
      function valueChangeHandle(event) {
        startTransition(() => {
          const keyword = event.target.value
          const filterShowNames = namesArray.filter(item => item.includes(keyword))
          setShowNames(filterShowNames)
        })
      }
    
      return (
        <div>
          <input type="text" onInput={valueChangeHandle}/>
          <h2>用户名列表: {pending && <span>data loading</span>} </h2>
          <ul>
            {
              showNames.map((item, index) => {
                return <li key={index}>{item}</li>
              })
            }
          </ul>
        </div>
      )
    })
    
    export default App
    
  • faker-js:随机生成信息的 js 库

    • 随机生成10000个人员
    import { faker } from '@faker-js/faker';
    
    const namesArray = []
    
    for (let i = 0; i < 10000; i++) {
      namesArray.push(faker.name.fullName())
    }
    
    export default namesArray
    

9.3 useDeferredValue

  • 官方解释useDeferredValue 接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后。

  • 理解:跟useTransition类似,useDeferredValue也可以让更新延迟

  • const copyData = useDeferredValue(数据)

    import React, { memo, useState, useDeferredValue } from 'react'
    import namesArray from './namesArray'
    
    const App = memo(() => {
      const [showNames, setShowNames] = useState(namesArray)
      const deferedShowNames = useDeferredValue(showNames)
    
      function valueChangeHandle(event) {
        const keyword = event.target.value
        const filterShowNames = namesArray.filter(item => item.includes(keyword))
        setShowNames(filterShowNames)
      }
    
      return (
        <div>
          <input type="text" onInput={valueChangeHandle}/>
          <h2>用户名列表: </h2>
          <ul>
            {
              deferedShowNames.map((item, index) => {
                return <li key={index}>{item}</li>
              })
            }
          </ul>
        </div>
      )
    })
    
    export default App